Minecraft模组开发这两年越来越火,身边不少朋友从玩整合包开始转向自己动手写模组,但十个人里有六七个都栽在了第一步:框架搭不起来。Forge作为老牌模组加载器,生态最全、教程也最多,可真正从零开始把构建脚本、主类加载、注册机制、资源文件这一整套流程串起来的完整教程反而少见,多数人照着碎片文章折腾两三天,最后连一个自定义物品都加不进去。这篇文章我直接用Minecraft 1.20.1 + Forge 47.2.0这套组合为例,手把手带你把Forge模组框架从空目录搭起来,Wizard风格逐步走通,配套完整源码我已经同步到Gitee仓库,你可以边看源码边操作,每一步都清楚自己在干什么。内容适合刚接触Forge开发、被零散教程搞晕的新手,也适合想沉淀一套可复用模板、后面专注写玩法逻辑的开发者。
1. 为什么还要自己搭一套Forge模组框架
1.1 现成工具与自定义框架的取舍
先说点掏心窝的话。现在做模组最快的方式确实是MCreator,拖拖拽拽就能生成一个能跑的模组,很多玩家用它做出的内容也挺漂亮,如果你只是想做个简单的装备或维度,MCreator完全够用。但它有个绕不开的短板:生成出来的代码是固定的,你想做自定义渲染、复杂合成逻辑、多人联机数据同步、服务端与客户端行为区分,模板就很难伸展,最后经常要手动去翻生成的一大堆类和混淆映射,反而更痛苦。
另一个常见路子是找个开源模组仓库,把别人的骨架直接拷过来改。这办法我也用过,但别人的骨架通常和具体玩法强耦合,物品、方块、结构生成、音效、成就全揉在一起,你要删干净得一层层剥,很多时候删着删着就删出崩溃。拿过来改的代码风格也不是自己的,维护起来心态很容易崩。所以我后来的习惯是:把"模组开发中所有模组都通用的那部分"抽出来,单独搭成一套框架,比如主类、注册入口、事件总线订阅、配置系统、日志、资源文件目录结构,这些基础结构稳定之后,后面每次做新模组都从这套骨架上长肉,只写增量内容,不碰基础设施。
1.2 一套合格模组框架必须具备的核心模块
从我的实操经验看,一个"能反复用"的Forge模组框架至少要包含六块内容。
- 主类与生命周期入口:用
@Mod注解标记的入口类,负责在Mod构造阶段初始化DeferredRegister、订阅FML生命周期事件。 - 注册体系:基于
DeferredRegister的物品、方块、实体、音效等注册入口,延迟注册、按命名空间隔离,这是整套框架的地基。 - 事件总线接入:区分
ModBus(模组加载阶段事件)和ForgeBus(游戏运行阶段事件),框架需要把这两个总线的订阅方式固定下来,形成约定。 - 资源文件骨架:
mods.toml、语言文件、模型文件、材质目录、配方目录都要预先铺好,避免后续每次新加物品都要手动建目录。 - 配置文件雏形:Forge自带的
ForgeConfigSpec配置系统,框架阶段就接上,后面加数值、开关只需往里追加字段。 - 构建脚本与运行环境:
build.gradle、gradle.properties、run目录约定、minecraft扩展块里的客户端/服务端运行配置。
我见过不少人跳过这些直接写玩法,写到一半发现不知道物品怎么注册到创造模式标签页,或者模型文件路径错一个字母导致全是紫黑方块。框架的意义是把这些约定的东西一次配好、反复使用,所以这一步不能省,省掉的后面都会以更难看的方式还回来。
1.3 为什么选1.20.1 + Forge 47.2.0
版本选择这件事我会单独说两句。目前社区里模组数量最多、教程最全、兼容性最稳的版本之一就是1.20.1,Forge对应版本是47.2.x系列,API相对稳定,很多成熟模组都基于它,踩坑参考资源也最多。选1.20.1还有个现实原因:它要求Java 17,这个版本是长期支持版本,配置比Java 8时代干净很多,而且ForgeGradle插件对Java 17的支持已经很成熟,不会遇到老版本那种反复切JDK的窘境。如果你打算做联机模组,1.20.1的服务端表现也比较可靠,玩家群落仍然很大,不会出现"模组做完了没人玩"这种尴尬。
2. 开工前的环境准备与工具链选型
2.1 JDK、Gradle、IDE的组合选择
这套组合我在多台机器上实测下来很稳,直接照抄即可:
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 17(建议用发行版的LTS版本) | 1.20.1强制要求Java 17,过低会编译失败 |
| Gradle | 8.1.1左右 | 由ForgeGradle插件管理,手动装8.x即可 |
| ForgeGradle | 6.0.x | 在settings.gradle里通过插件声明 |
| Forge | 47.2.0 | 对应Minecraft 1.20.1 |
| IDE | IntelliJ IDEA社区版(免费) | 也可以用Eclipse,但IDEA对Gradle工程支持更好 |
小知识点:Forge 1.20.1用的是官方映射表(official mappings),所以你在代码里看到的是minecraft、ServerPlayer这类正常方法名,而不是远古时期那种func_12345混淆名。这也是我推荐新版本的原因之一,看源码、搜教程都友好得多。
2.2 Gitee源码仓库的结构说明与导入
标题里说了这套源码已经放在Gitee,我先把仓库目录结构摆出来,方便你对照。
forge-mod-framework/ ├── build.gradle # 构建脚本核心 ├── settings.gradle # 插件仓库配置 ├── gradle.properties # 版本号集中管理 ├── .gitignore # 排除build、run、.idea └── src/main/ ├── java/com/example/myframework/ │ ├── ModMain.java # @Mod主类 │ ├── registration/ModItems.java # 物品注册入口 │ ├── item/ExampleItem.java # 示例物品类 │ └── config/FrameworkConfig.java # 配置文件雏形 └── resources/ ├── META-INF/mods.toml # 模组元信息 └── assets/myframework/ ├── lang/en_us.json ├── lang/zh_cn.json ├── models/item/example_item.json └── textures/item/example_item.png从Gitee拉取有两种方式:一个是直接git clone整个仓库到本地,然后在IDEA里用"Open or Import"选中build.gradle,以Gradle项目导入;另一个是下载ZIP包,解压后导入。无论哪种,注意导入后要等右下角Gradle同步完成,第一次同步要下载大量依赖,耗时取决于网络状况。仓库里的.gitignore我已经配好,不会把build、run这些运行产生的临时目录提交上去,你fork之后也建议保持这个习惯。
2.3 国内网络环境下Gradle依赖下载的优化
首次同步Gradle时卡在下载是几乎每个新手都会撞见的问题。ForgeGradle会从Maven仓库拉取Minecraft、Forge、映射表等一大堆文件,总量经常高达几百MB甚至更多,网速稍慢就卡到怀疑人生。我的做法是在settings.gradle的pluginManagement和repositories里加上阿里云Maven镜像,具体见后面的构建脚本章节。这些配置不属于Forge框架本身,但属于"你没它活不下去"的基础设施,所以我提前放在这里强调:如果你发现同步一直失败或极慢,八成要先检查仓库URL配置,而不是怀疑JDK装错了。
3. 手把手从零搭建:完整实操流程
3.1 第一步:生成基础工程有两种路径
搭建起点有两条路,我建议你优先选第一条。
路线A:直接用Gitee仓库里的完整工程
这是最省事的方式,仓库里已经是配置好、能直接Sync的完整Gradle项目。你clone下来之后只需要三步:打开IDEA导入、等待Gradle同步完成、确认JDK是17。这条路线适合想快速跑通、后面慢慢理解细节的人。
路线B:从官方MDK手动搭
如果你想像我一样彻底掌握每一层配置,可以去Forge官网下载对应版本的MDK(Mod Development Kit),解压后得到官方预设的build.gradle、src、gradle/目录,然后按下一节的顺序手动替换配置。这条路线多花半小时,但对后面排错帮助巨大,我强烈建议至少完整走一遍。
我自己的习惯是先走一遍B、理解全部配置后,后续新项目直接改A里的模板。你可以根据自己的基础选,但如果上来就是纯小白,别硬刚B,直接从A开始,跑通了再回头研究细节。
3.2 第二步:配置构建脚本(build.gradle)
这是整个框架里最核心的配置文件。我提供一个已经精简过、能直接用的版本,关键位置都有注释。
plugins { id 'eclipse' id 'idea' id 'maven-publish' id 'net.minecraftforge.gradle' version '[6.0,6.2)' } version = '1.0.0' group = 'com.example.myframework' base { archivesName = 'myframework' } java.toolchain.languageVersion = JavaLanguageVersion.of(17) minecraft { mappings channel: 'official', version: '1.20.1' runs { client { workingDirectory project.file('run') property 'forge.logging.markers', 'REGISTRIES' property 'forge.logging.console.level', 'debug' mods { myframework { source sourceSets.main } } } server { workingDirectory project.file('run') property 'forge.logging.markers', 'REGISTRIES' property 'forge.logging.console.level', 'debug' mods { myframework { source sourceSets.main } } } data { workingDirectory project.file('run') property 'forge.logging.markers', 'REGISTRIES' property 'forge.logging.console.level', 'debug' args '--mod', 'myframework', '--all', '--output', file('src/generated/resources/'), '--existing', file('src/main/resources/') mods { myframework { source sourceSets.main } } } } } repositories { maven { name = 'Aliyun' url = 'https://maven.aliyun.com/repository/public' } maven { url = 'https://jitpack.io' } } dependencies { minecraft "net.minecraftforge:forge:1.20.1-47.2.0" } jar { manifest { attributes([ 'Specification-Title' : 'myframework', 'Specification-Vendor' : 'example', 'Specification-Version' : '1', 'Implementation-Title' : project.name, 'Implementation-Version': project.jar.archiveVersion, 'Implementation-Vendor' : 'example' ]) } }几个关键点我拆开讲一下。
mappings channel: 'official'这行决定你用官方映射还是MCP映射。新手直接用official最省心,代码里的方法名都是你看得懂的名字;如果选parchment这类带Javadoc注释的映射,写起来更舒服,但需要额外配置,这里不展开。
runs块里定义了三种运行配置:client启动客户端、server启动服务端、data用于Gradle数据生成器生成配方和模型文件。注意mods块里的名称myframework要和后面mods.toml里的modId一致,否则运行时Forge会报找不到mod源,这个问题我见人踩过很多次。
依赖配置里minecraft行指定Forge版本,格式是Minecraft版本-Forge版本,注意是47.2.0不是47.2,括号里多了个0,写错会直接搜不到依赖。如果你从Gitee仓库fork,版本号集中在gradle.properties里管理,改起来更方便。
3.3 第三步:配置模组元信息(mods.toml)
src/main/resources/META-INF/mods.toml是Forge识别你模组的身份证。文件格式是TOML,看起来简单,但几个字段的大意你得搞清楚。
modLoader="javafml" loaderVersion="[47,)" license="MIT" [[mods]] modId="myframework" version="1.0.0" displayName="My Framework" authors="YourName" description=''' A clean Minecraft Forge mod framework. ''' [[dependencies.myframework]] modId="forge" mandatory=true versionRange="[47,)" ordering="NONE" side="BOTH" [[dependencies.myframework]] modId="minecraft" mandatory=true versionRange="[1.20.1,1.21)" ordering="NONE" side="BOTH"modLoader固定为javafml,loaderVersion写[47,)表示要求Forge加载器版本大于等于47。[[mods]]段里modId是全模组的唯一标识,我用的myframework,你在自己的项目里一定要改成独特的名字,因为玩家同时装很多模组时,modId冲突会导致注册混乱甚至崩溃。
[[dependencies.myframework]]段声明这个模组依赖Forge和Minecraft,mandatory=true意思是缺了就不让启动,side="BOTH"表示客户端和服务端都需要。如果你以后写只影响客户端的模组,这里可以写side="CLIENT"。
3.4 第四步:创建主类与生命周期入口
主类是整个模组启动的钩子。Forge加载模组时,会通过modId找到有@Mod注解的类并实例化,构造方法里执行的所有初始化逻辑都属于"模组加载阶段"。
package com.example.myframework; import com.example.myframework.registration.ModItems; import com.mojang.logging.LogUtils; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.slf4j.Logger; @Mod(ModMain.MODID) public class ModMain { public static final String MODID = "myframework"; public static final Logger LOGGER = LogUtils.getLogger(); public ModMain() { IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); ModItems.ITEMS.register(modEventBus); MinecraftForge.EVENT_BUS.register(this); LOGGER.info("My Framework initialized"); } }这段代码有四个动作值得关注。
@Mod(ModMain.MODID)注解里的值是modId,必须和mods.toml里的modId完全一致。- 构造方法里先把
modEventBus取出来,这是模组加载阶段的专用事件总线。 ModItems.ITEMS.register(modEventBus)把我们后面要写的物品注册表挂载到事件总线上,这一步是注册机制的心脏。MinecraftForge.EVENT_BUS.register(this)把主类自身注册到游戏运行阶段的Forge总线,以后可以直接在主类里写@SubscribeEvent方法监听游戏事件。
我见过不少新手在构造方法里写@SubscribeEvent方法,然后发现事件永远触发不了,原因是@SubscribeEvent只能用在已经注册到总线上的对象里,而主类构造阶段总线还没完成注册。如果你只在主类里写@SubscribeEvent,必须同时调用MinecraftForge.EVENT_BUS.register(this),否则事件静默丢失。
3.5 第五步:编写配置类框架
配置文件这一步很多人会跳过,但我建议框架阶段就接上,后面加东西真的会方便很多。下面是一个最小的ForgeConfigSpec配置类。
package com.example.myframework.config; import net.minecraftforge.common.ForgeConfigSpec; public class FrameworkConfig { public static final ForgeConfigSpec.Builder BUILDER = new ForgeConfigSpec.Builder(); public static final ForgeConfigSpec SPEC; public static final ForgeConfigSpec.BooleanValue EXAMPLE_TOGGLE; public static final ForgeConfigSpec.DoubleValue EXAMPLE_RATE; static { BUILDER.comment("Framework example settings").push("general"); EXAMPLE_TOGGLE = BUILDER .comment("Enable example feature") .define("exampleToggle", true); EXAMPLE_RATE = BUILDER .comment("Example rate value") .defineInRange("exampleRate", 1.0, 0.0, 10.0); BUILDER.pop(); SPEC = BUILDER.build(); } }使用的时候在主类构造方法里注册:
ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, FrameworkConfig.SPEC);这个配置系统会在run/config目录下生成一个myframework-common.toml文件,玩家可以直接编辑。框架阶段就把配置接口铺好,后面做玩法时只需要往BUILDER里追加字段,省很大事。
4. 框架核心机制拆解:注册、事件与生命周期
4.1 两条事件总线千万别搞混
Forge的事件系统是模组开发最容易绕晕的部分,因为它有两条总线:一条是FMLJavaModLoadingContext.get().getModEventBus(),简称Mod总线;另一条是MinecraftForge.EVENT_BUS,简称Forge总线。简单理解,Mod总线处理的是模组加载过程中发生的事,比如注册物品、注册方块、注册实体、配置加载等,这些事件发生在游戏启动早期,顺序敏感;Forge总线处理的是游戏运行过程中持续发生的事,比如玩家加入、怪物生成、方块破坏、按键点击等。
新手最常见的错误是把两类事件监听放错总线。比如用@SubscribeEvent监听RegisterEvent(属于Mod总线),却注册到了Forge总线上,结果你发现注册逻辑永远不执行。反过来,如果你把玩家登录事件挂到Mod总线上,那个方法在启动阶段就结束了,玩家上线时根本不会触发。我的经验是:注册相关事件永远走Mod总线,游戏玩法相关事件永远走Forge总线,写代码前先想清楚自己要监听的是"加载时"还是"运行时"。
4.2 注册机制:DeferredRegister的工作方式
很多刚接触Forge的开发者会困惑:为什么新版本模组里看不到RegistryEvent.Register<Item>这种老式注册方法了?因为Forge在新版推荐使用DeferredRegister,它其实是个延迟注册的封装:你调用ITEMS.register("example_item", () -> new Item(...))时,并不会立刻注册物品,而是把工厂函数存起来,等到Mod总线上的注册事件真正触发时(也就是游戏启动、注册表准备就绪的时刻)才批量执行。
这样做有两个好处:一是代码顺序和解耦,你可以在任意位置声明注册,不用关心注册时机;二是RegistryObject返回值是延迟填充的,你可以在注册之前就把物品引用赋值给静态字段,Forge会在合适的时机关联真实对象。下面是我们框架里的物品注册入口,和主类配合就能跑通整个注册流程。
package com.example.myframework.registration; import com.example.myframework.ModMain; import net.minecraft.world.item.Item; import net.minecraftforge.registries.DeferredRegister; import net.minecraftforge.registries.ForgeRegistries; import net.minecraftforge.registries.RegistryObject; public class ModItems { public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, ModMain.MODID); public static final RegistryObject<Item> EXAMPLE_ITEM = ITEMS.register("example_item", () -> new Item(new Item.Properties())); }注意DeferredRegister.create第一个参数传的是ForgeRegistries.ITEMS,这是Forge提供的物品注册表;第二个参数是命名空间,也就是modId。注册出来的物品完整注册名是myframework:example_item,中间的冒号在Minecraft里是命名空间与路径的分隔符,所有模组内容都要遵守这个约定,这也是多个模组能共存而不冲突的底层机制。
4.3 物品、方块、实体的注册范式对比
物品注册最简单,方块和实体稍微复杂一点。方块注册通常要同时注册方块本身和方块对应的物品形式,实体则要注册实体类型和渲染器。我把三种最典型的注册范式放在一起类比:
| 注册目标 | DeferredRegister的类型 | 额外步骤 |
|---|---|---|
| 物品 | ForgeRegistries.ITEMS | 无需额外步骤,直接new Item |
| 方块 | ForgeRegistries.BLOCKS | 注册Block时再注册BlockItem |
| 实体 | ForgeRegistries.ENTITY_TYPES | 客户端还要注册EntityRenderer |
以方块为例,你会在方块注册类里看到类似这样的代码:
public static final RegistryObject<Block> EXAMPLE_BLOCK = BLOCKS.register("example_block", () -> new Block(BlockBehaviour.Properties.copy(Blocks.STONE))); public static final RegistryObject<Item> EXAMPLE_BLOCK_ITEM = ITEMS.register("example_block", () -> new BlockItem(EXAMPLE_BLOCK.get(), new Item.Properties()));BlockItem就是玩家手里拿着能放置的方块物品,它和方块是两个注册条目,但注册名通常保持一致,这样Minecraft才认得这个物品对应哪个方块。实体的流程更复杂,涉及EntityType.Builder、属性、生成规则,后面做玩法时再扩展,框架阶段先把这种"一个内容多条注册链"的思路建立起来就够用了。
4.4 客户端与服务端的安全分离
写模组时一定会遇到一个经典问题:客户端独有的类(渲染、按键、GUI)在服务端不存在,如果你在共用代码里直接引用,服务端启动会直接崩。Forge的约定是用@OnlyIn(Dist.CLIENT)注解标记客户端专用方法或类,同时用DistExecutor.safeRunWhenOn(Dist.CLIENT, ...)做安全调用。
以客户端事件监听为例,官方推荐的方式是单独建一个客户端事件类,并在类上直接标注@Mod.EventBusSubscriber(modid = ModMain.MODID, bus = Mod.EventBusSubscriber.Bus.FORGE, value = Dist.CLIENT)。这个注解省去了手动注册的步骤,Forge发现这个类后会自动把它注册到对应总线上,同时只会在客户端环境加载。我在Gitee源码里放的框架预留了一个event/ClientEvents.java空类,就带了这个注解,方便你以后直接往里填客户端逻辑。
这种约定说起来很简单,但新手经常在图省事时直接在共用类里调客户端方法,导致服务端启动即崩。框架阶段把分离机制铺好,能省下一堆调试时间。
5. 用示例物品验证框架,顺便打通资源文件
5.1 创建一个简单示例物品类
框架搭好之后必须验证全链路能跑通,否则空转没有意义。我选择的验证方式就是往框架里加一个自定义物品,因为一个物品从注册到模型、材质、语言文件,跨越了几乎全部关键环节,任何一环断了都会有明显表现。这里创建一个带右键效果的示例物品,简单但足够验证机制。
package com.example.myframework.item; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResultHolder; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Item; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; public class ExampleItem extends Item { public ExampleItem(Properties properties) { super(properties); } @Override public InteractionResultHolder<ItemStack> use(Level level, Player player, InteractionHand hand) { if (!level.isClientSide) { player.sendSystemMessage(net.minecraft.network.chat.Component.literal("Hello from My Framework!")); } return InteractionResultHolder.sidedSuccess(player.getItemInHand(hand), level.isClientSide); } }注意use方法里最后返回的sidedSuccess,第一个参数是玩家手里的物品栈,第二个参数是level.isClientSide()的判断结果。这样客户端显示动画、服务端执行逻辑,两边不会重复触发。如果你忘记这个判断,右键物品时消息可能会在客户端和逻辑端各发一次,双端联机时会出现重复消息。
然后把ModItems里的匿名Item改成使用这个类:
public static final RegistryObject<Item> EXAMPLE_ITEM = ITEMS.register("example_item", () -> new ExampleItem(new Item.Properties().stacksTo(16)));stacksTo(16)限制堆叠上限,这是物品属性里很常用的配置,食物、工具、材料各有不同的堆叠策略,框架阶段先用一个普通物品验证即可。
5.2 模型与材质文件:紫黑方块的克星
物品注册成功但模型缺失时,游戏里会渲染成经典紫黑方块,同时日志里刷一堆找不到模型/材质的警告。这是新手最常撞见的视觉问题,九个里有八个都是路径写错。Minecraft的模型加载遵循严格的路径规则,以命名空间myframework为例,物品模型文件必须放在assets/myframework/models/item/example_item.json,材质文件放在assets/myframework/textures/item/example_item.png,模型的纹理字段引用路径时也要保持一致。
最简模块化物品模型文件长这样:
{ "parent": "minecraft:item/generated", "textures": { "layer0": "myframework:item/example_item" } }这里"parent": "minecraft:item/generated"表示使用最通用的平面物品渲染,纹理通过layer0指定。"myframework:item/example_item"会自动映射到assets/myframework/textures/item/example_item.png。你只要保证四个路径一致:JSON文件名等于未注册路径、材质文件名等于JSON里引用的纹理路径、JSON文件路径正确、材质文件真实存在,紫黑方块就会消失。
材质文件本身建议用带透明通道的PNG,尺寸通常是16x16像素,命名全部小写字母加下划线。很多人直接用一个占位材质,后面再做像素画,这是很聪明的做法,先把链路跑通,美术和玩法是后话。
5.3 语言文件:给物品起个正常名字
物品虽然能显示,但名字没配语言文件的话,游戏里会显示成item.myframework.example_item这种原始注册名,非常丑陋。语言文件放在assets/myframework/lang/en_us.json和zh_cn.json,内容格式如下。
{ "item.myframework.example_item": "Example Item" }{ "item.myframework.example_item": "示例物品" }键名的格式是item.+ 命名空间 +.+ 注册路径,这是一个固定的拼接规则。方块对应block.开头,实体对应entity.开头,模组名等一般用itemGroup.开头。语言文件支持UTF-8编码,但在Windows下保存时容易被改成GBK,导致游戏里中文变乱码,我的经验是IDE里设置File Encoding为UTF-8,JSON文件另存时也显式选UTF-8,不要用系统默认。
6. 常见问题与排查技巧实录
6.1 环境与构建阶段的高频问题
这一节我专门把实际跑这个框架时真会遇到的报错整理成速查表,每一类都附上我自己的排查思路。
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
| Gradle同步卡在Download | 依赖仓库访问慢 | 配置阿里云Maven镜像,或等待重试 |
| 编译报错"cannot find symbol: class Logger" | 依赖没同步完整 | 先清Gradle缓存再重新同步 |
@Mod类找不到或modId不匹配 | mods.toml与主类注解不一致 | 核对modId大小写,必须完全一致 |
| 启动游戏时提示缺少依赖mod | mods.toml的dependencies配置错误 | 检查[[dependencies.myframework]]段 |
| 运行时黑屏闪退 | 事件监听放错总线 | 检查@SubscribeEvent注册的总线 |
物品名字显示成item.xxx | 语言文件缺失或键名错误 | 核对lang目录及键名拼写 |
| 物品显示紫黑方块 | 模型文件或材质路径错误 | 按5.2节核对四条路径 |
6.2 日志与崩溃报告怎么看
遇到问题第一反应不是瞎猜,而是看日志。Forge的运行日志默认写在run/logs/latest.log,崩溃信息会写一个独立的crash-reports/目录,文件名带时间戳。我的习惯是崩溃后先看第一屏的"Description"和"Exception",再把堆栈里带mods.或com.example前缀的行单独抠出来分析,这通常就是出错位置。大量模组玩家上传的崩溃报告其实都是这种半截日志,真正有用的只有前几十行和最后三十行。
日志里还有一个坑:INFO级日志很多,真正的错误往往混在中间。建议先在IDEA里直接搜ERROR、Exception、Caused by这三个关键词。Caused by后面经常跟着真正的原因,比如NoSuchMethodError,那八成是版本不匹配,FileNotFoundException才是路径问题。排查时按"先版本、再路径、后逻辑"这个顺序走,能省很多时间。
6.3 团队协作与源码管理建议
Gitee源码仓库同步这件事我还要啰嗦几句。如果你把这个框架用于团队或长期项目,建议从一开始就维护好.gitignore,至少排除build/、run/、.gradle/、.idea/、*.iml这几个目录。我之前见过团队里有人把run目录整个提交上去,里面带着几百MB的世界存档,仓库瞬间臃肿,克隆一次痛苦一次。
另外建议在gradle.properties里统一管理Minecraft版本、Forge版本、模组版本号,不要散落在各个文件里。这样切换Minecraft版本时只改一处,重新同步Gradle即可。如果你基于Gitee仓库fork了这套框架,记得改掉group、archivesName、modId这些标识字段,否则发布到社区时会和其他人的模组冲突,低级但致命。
6.4 几个调试习惯和心态建议
踩过足够多的坑之后,我总结了几个调试习惯,分享给你参考。
第一,每次新加内容先跑一次客户端确认加载无误,再继续加下一个,不要一口气写完几十个类再启动,否则报错时压根不知道是哪一环断了。第二,善用LOGGER.debug而不是System.out.println,日志系统自带时间和等级过滤,比控制台打印好排查得多。第三,改动build.gradle、mods.toml这种配置文件后,要重新同步Gradle再启动,直接点运行会加载旧配置,表现就是改了没反应,很多人被这个假象骗了很长时间。
还有一点心态上的体会:模组开发绝大多数报错都是路径错误、版本失配、总线挂错这三类,没有任何一个是你"学不会"的。遇到报错先按这三类排查,90%的问题都能自己解决,剩下10%再把日志贴到社区求助,但记得带上完整日志和版本信息,别只问"为什么崩了"。把框架搭稳、把日志看懂,模组开发的自由度会突然大很多,后面做玩法和内容就都不难了。