简介:资源为Minecraft 1.20.1版本配套的Forge模组开发工具包(MDK),面向准备在该版本开展模组开发、或希望深入学习Forge工程结构的玩家和Java开发者,无论是初次接触模组开发的新手,还是需要在该版本上进行功能迭代的老手,都能以此作为起点。压缩包整体约111KB,共17个文件,类型以txt、gradle、properties、java等为主:txt涵盖变更日志、开源许可与贡献名单,Gradle构建文件与属性文件用来定义依赖、构建逻辑和参数,gradlew及bat脚本支持跨平台自动编译打包。已有1470人学习下载,说明这体积小巧、结构完整的MDK在实际上手场景中具有很高实用价值。使用者在导入工程后,可获得规范的模组目录和调试环境;借助Gradle包装器一键拉取依赖并生成可运行模组;再结合src目录中的Java源文件与toml、mcmeta元数据做调整,即可完整走通环境配置、模组入口编写、构建产物产出等关键流程。
1. 为什么大家都在锚定 forge-1.20.1:先别急着写代码
每次有新入行的模组开发者问我「该从哪个版本入手」,我的答案基本固定:forge-1.20.1。不是因为它是最新的,恰恰相反,1.20.1 是整个 1.20 家族里生态沉淀最厚的一条稳定线。Forge 加载器在这个版本上的 API 已经成熟,不新不旧,社区问答、教程、现成整合包、前置模组基本都齐了。而 1.20.2 之后生态断过一次,1.21 的注册方式和组件系统改动又让很多旧经验失效——只有 1.20.1 处在「旧习惯能用、新写法也兼容」的舒适区。
这篇文章不教你照着抄某个现成模组,而是带你从 Forge 1.20.1 的角度把完整链路走一遍:环境怎么搭、骨架文件怎么摆、注册一个方块并让它真正出现在游戏里、发布前哪些坑会让你白忙一周。适合有一定 Java 基础但第一次碰 Forge 的开发者,也适合那些已经在跑服务器、想自己维护一个专用模组的服主。我先说结论:Forge 开发 90% 的时间不在写逻辑,而在处理版本、路径和映射这三件事。
2. 环境准备:JDK 17 与 Forge MDK 的正确打开方式
进入 Forge 1.20.1 开发的第一步不是写@Mod注解,而是把 JDK 和 Gradle 环境钉死在正确的版本上。很多新人卡在环境上两三天,其实不是操作复杂,而是电脑里 JDK 版本太杂:系统默认的是 Java 8,IDE 里跑的是 Java 21,命令行里又冒出来一个 Java 11。Forge 1.20.1 官方要求 Java 17,这是个硬约束,不是建议。
2.1 先检查 JDK:一条命令看出版本不一致
打开终端,逐个环境确认当前生效的 Java 版本。Windows 上要注意 PATH 里可能有多个 Java 路径,用where才能看全。
java -version echo $JAVA_HOME # Windows 下追加执行: where java看输出的关键点:java -version必须显示 17。如果你的输出是 1.8 或 21,先不要继续。JAVA_HOME指向的目录必须是 JDK 17 的安装目录,而不是 JRE 目录。这个变量对 Gradle 尤其重要,因为 ForgeGradle 插件启动时会读取JAVA_HOME来决定用哪个 Java 运行构建。
如果默认版本不对,直接把 JAVA_HOME 指过去就行。Windows 用户可以在管理员终端里用setx设置,macOS 和 Linux 用户在 shell 配置文件里写 export。
# Windows setx JAVA_HOME "C:\Program Files\Java\jdk-17" # macOS / Linux export JAVA_HOME=/usr/lib/jvm/java-17-openjdk export PATH=$JAVA_HOME/bin:$PATH设置完后重新开一个终端再跑一次java -version。这里血泪经验是:IDE 里的 Gradle JVM 和系统 JAVA_HOME 是两套配置。即使终端里已经是 17,IDE 里如果给 Gradle 指定了 21,构建照样报UnsupportedClassVersionError。我第一次跑 MDK 就是卡在这——系统 17、IDE 的 Gradle JVM 21,报错内容提到的是某个内部类版本过高,排查了很久才发现是两套配置打架。
2.2 MDK 解压后的改动:只改三个地方
从 Forge 官网的 Downloads 页面找到 1.20.1 对应的 MDK(Mod Development Kit),下载后是一个 zip。解压后你会看到build.gradle、gradle.properties、src/main/java、src/main/resources这些标准结构。用 IDE 打开build.gradle后等待 Gradle 同步,第一次会比较久,别急着操作。
同步完成后打开gradle.properties,这是最值得先看的一个文件。
# gradle.properties org.gradle.jvmargs=-Xmx3G -XX:MaxMetaspaceSize=1G org.gradle.daemon=true org.gradle.parallel=true # 模组基本信息,改成自己的 group=com.example archivesBaseName=examplemod mod_id=examplemod mod_version=1.0.0 mod_display_name=Example Mod重点说org.gradle.jvmargs这行。ForgeGradle 首次构建要做反编译、映射转换、资源重映射,内存需求远大于普通 Java 项目。默认 2G 堆内存经常在构建到一半时 OOM,直接把-Xmx提到 3G 或 4G 能省掉很多莫名其妙的失败。mod_id这里定义的变量在后面 build.gradle 里会被引用,注意它必须是全小写字母加下划线,不能有连字符。
如果网络条件不理想,在build.gradle的repositories里可以加镜像仓库加速依赖解析。这不是绕过什么,只是让构建更稳妥的方式。
repositories { maven { name = 'AliyunMaven' url = 'https://maven.aliyun.com/repository/public' } mavenCentral() }镜像配置要注意顺序:镜像仓库放在最前面,Gradle 会优先从它拉取。如果没有这个需求可以不配,但如果你发现同步永远卡在某个依赖上下不来,第一反应应该是来这里加镜像,而不是反复清理缓存重试。
2.3 首次同步:三个高频失败的识别方法
失败一:报Could not resolve net.minecraftforge:forge:1.20.1。这是网络问题,Maven 仓库连接不稳定。先检查镜像配置有没有生效,再看~/.gradle/caches目录下是否有残留的损坏缓存。保险做法是删掉caches里对应的modules-2子目录再重新同步。
失败二:Gradle 同步停在 99% 不动。这不是卡死,是 ForgeGradle 在下载 Minecraft 客户端 jar 和映射表,文件很大。观察右下角或终端输出,只要在走就耐心等。这时候最忌讳反复重启 Gradle daemon——每重启一次,之前下载的进度虽然会缓存,但中断容易留下半截文件。
失败三:java.lang.UnsupportedClassVersionError直接抛出。这是 Gradle JVM 版本不对,回到 2.1 检查 IDE 的 Gradle JVM 设置。确认 IDE 里 Gradle JVM 指向 JDK 17,同时确认gradlew --version输出的 JVM 也是 17。我一般会在刚解压的目录里先跑一次./gradlew --version确认版本,再打开 IDE,这样能快速区分是命令行环境问题还是 IDE 配置问题。
3. 从模组骨架到第一个逻辑:mods.toml、主类与资源文件
环境通了以后,MDK 已经给你生成了一套可编译的空模组。接下来要做的不是马上写游戏逻辑,而是先把三个骨架文件对齐:mods.toml声明身份,主类承载入口,资源文件让方块不再是紫黑块。这三样东西的路径是硬约束,摆错位置编译不会报错,但进游戏一定出问题。
3.1 mods.toml:版本声明的门面文件
mods.toml在src/main/resources/META-INF/mods.toml,它告诉 Forge 这个 jar 叫什么、依赖哪个加载器版本、需要哪些前置。模组加载失败一半以上是这个文件的字段写错,尤其是 modId 不匹配。
modLoader = "javafml" loaderVersion = "[47,)" issueTrackerURL = "https://example.invalid" [[mods]] modId = "examplemod" version = "1.0.0" displayName = "Example Mod" authors = "example" description = "A demo mod for forge 1.20.1" [[dependencies.examplemod]] modId = "forge" mandatory = true versionRange = "[47,)" ordering = "NONE" side = "BOTH"字段逐个说。loaderVersion = "[47,)"表示要求 Forge 加载器版本大于等于 47,这个版本号对应 1.20.1 的 Forge 版本线。[[mods]]下的modId必须和主类@Mod注解里的值完全一致,大小写也要一致。[[dependencies.examplemod]]声明了对 Forge 的依赖,mandatory = true表示没有 Forge 就不加载,versionRange用 Maven 版本区间语法,[47,)表示大于等于 47 且无上限。
需要特别注意的是 versionRange 的写法。很多人把[1.20.1,1.21)这种 Minecraft 版本号直接填进来,这是错的——Forge 依赖声明里填的是 Forge 加载器版本,不是 Minecraft 版本。填反了的结果是游戏加载时提示找不到满足条件的前置,而实际上你自己就是这个模组的作者,找半天才发现是版本区间写错了。
3.2 主类与注册:DeferredRegister 是 1.20.1 的标准姿势
写一个最简主类,包含 mod 入口、事件订阅、一个方块加对应物品的注册。这段代码可以直接跑,但请把包名改成你自己的。
package com.example.examplemod; import net.minecraft.world.item.BlockItem; import net.minecraft.world.item.Item; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockBehaviour; import net.minecraft.world.level.material.Material; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.event.entity.player.PlayerEvent; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.registries.DeferredRegister; import net.minecraftforge.registries.ForgeRegistries; import net.minecraftforge.registries.RegistryObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Mod("examplemod") public class ExampleMod { public static final String MODID = "examplemod"; public static final Logger LOGGER = LoggerFactory.getLogger(MODID); public static final DeferredRegister<Block> BLOCKS = DeferredRegister.create(ForgeRegistries.BLOCKS, MODID); public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, MODID); public static final RegistryObject<Block> EXAMPLE_BLOCK = BLOCKS.register("example_block", () -> new Block(BlockBehaviour.Properties.of(Material.STONE).strength(1.5f))); public static final RegistryObject<Item> EXAMPLE_BLOCK_ITEM = ITEMS.register("example_block", () -> new BlockItem(EXAMPLE_BLOCK.get(), new Item.Properties())); public ExampleMod() { IEventBus modBus = FMLJavaModLoadingContext.get().getModEventBus(); BLOCKS.register(modBus); ITEMS.register(modBus); MinecraftForge.EVENT_BUS.register(this); } @SubscribeEvent public void onPlayerLogin(PlayerEvent.PlayerLoggedInEvent event) { LOGGER.info("Welcome from forge 1.20.1 example mod"); } }这段代码里有几个关键点。DeferredRegister.create(ForgeRegistries.BLOCKS, MODID)的第一个参数是注册表类型,第二个是模组 ID;它不会立刻注册,而是在 mod 事件总线触发时统一执行,这是避免注册顺序冲突的标准做法。RegistryObject是懒加载的容器,EXAMPLE_BLOCK.get()才真正拿方块实例。
注意构造函数里的两个总线。FMLJavaModLoadingContext.get().getModEventBus()是 mod 总线,BLOCKS.register(modBus)往 mod 总线注册,MinecraftForge.EVENT_BUS.register(this)把游戏事件订阅到游戏总线。如果混淆了这两个总线——把@SubscribeEvent的方法挂到 mod 总线上监听游戏事件——方法永远不会被调用。这是很隐蔽的翻车点,不报错,只是没反应。
3.3 方块要显示出来:blockstates、model、texture 三件套
很多人在注册完方块后直接进游戏,结果发现物品栏里是紫黑格子。这是因为只注册了对象,没有给 Minecraft 提供渲染所需的资源文件。1.20.1 的方块渲染需要三个文件配合。目录结构必须是:
src/main/resources/ ├── META-INF/ │ └── mods.toml └── assets/ └── examplemod/ ├── blockstates/ │ └── example_block.json ├── models/ │ ├── block/ │ │ └── example_block.json │ └── item/ │ └── example_block.json ├── textures/ │ └── block/ │ └── example_block.png └── lang/ └── en_us.jsonblockstates 文件决定方块在某个 blockstate 下用哪个模型。对没有状态的简单方块,写一个空 variants 即可:
{ "variants": { "": { "model": "examplemod:block/example_block" } } }model 文件决定模型外观。最省事的写法是继承 Minecraft 原版的全方块模型,然后指定贴图:
{ "parent": "minecraft:block/cube_all", "textures": { "all": "examplemod:block/example_block" } }textures 路径解析规则是assets/examplemod/textures/block/example_block.png。贴图文件必须是 PNG,而且尺寸建议是 16 的整数倍,否则 mipmap 生成会异常,远处看着闪烁。
还有一个容易漏的是models/item/example_block.json。方块物品在物品栏里的显示走的是 item 模型,没有它,手持方块或查看物品栏时会显示紫黑。内容通常和 block 模型一样,或者用 parent 引用:
{ "parent": "examplemod:block/example_block" }lang 文件不做也不会崩,但没写的话游戏内显示的是未本地化的 key 字符串。en_us.json的键格式是block.examplemod.example_block:
{ "block.examplemod.example_block": "Example Block" }这三件套的路径、文件名、JSON 里的 model 引用,三处必须完全一致。Windows 上大小写不敏感,Linux 服务器上敏感——本地能跑、上服务器紫黑的案例,一半是这原因。
4. 避坑:Forge 1.20.1 开发里最常翻车的五个细节
尽管文档齐全,但 Forge 开发里总有一些问题是你搜索很久也找不到直接答案的。这一章把我在维护和教学过程中遇到过的高频问题整理成五个典型现场,按「现象 → 原因 → 解决」讲清楚,照着排查能省下大量时间。
4.1 modId 对不上:启动直接 ModLoadingException
很多人的第一个模组都会遇到这个:runClient一启动,日志刷出一长串,最后是ModLoadingException,提示某个 mod 无法加载,原因写着 mod ID 找不到对应的主类。我见过最典型的写法是@Mod("my_mod")和mods.toml里的modId = "mymod",一个带下划线一个不带,Forge 视为两个不同的 ID。
原因就是两处 ID 不一致。Forge 加载时以mods.toml的modId为准,去找@Mod注解中同 ID 的类;找不到就报错。解决方法是统一命名。我的习惯是先定一个短 ID,所有地方复制粘贴,不手打,避免输入错误。小写下划线是最安全的格式,连字符虽然也能用,但某些工具链解析时有边界问题。
4.2 客户端显示方块,服务端却是空气
这个现场经常出现在多人服务器上:客户端进游戏能看到方块,也能拿在手上,但服务端玩家一靠近就掉下去,方块像不存在一样,破坏后也不掉物品。原因基本是注册逻辑写进了客户端专用的方法里——最常见的是把BLOCKS.register(modBus)写进了@OnlyIn(Dist.CLIENT)装饰的初始化方法。服务端加载时跳过了这段代码,导致方块注册缺失,但客户端又有注册数据,两边状态不一致。
解决方法是把注册逻辑放在主类的构造函数或FMLCommonSetupEvent里,这些是 common 阶段的入口。@OnlyIn只用来修饰客户端渲染类、GUI、按键绑定这类明显只能在客户端存在的东西。判断标准很简单:如果一段代码影响注册表或游戏逻辑,绝不能挂@OnlyIn。
4.3 路径全对,还是紫黑方块:贴图尺寸和大小写
紫黑方块是最磨人的问题,因为它不崩服,只是外观异常,日志里的错误也很隐蔽。排查从三个方向走。第一,看日志里有没有Unable to load texture之类的提示,它给出的路径就是正在找的贴图路径,检查这个路径下有没有文件。第二,检查 JSON 里的引用路径是否和实际文件位置一致,重点是blockstates里的 model 引用——它指向的是assets/examplemod/models/block/example_block.json,路径里不需要写models/前缀,直接从examplemod:开始。第三,确认贴图 PNG 尺寸是 16 的整数倍,我用过一次 64×64 的也能显示,但有次导入 48×48 的图就开始隔一个区块刷紫黑,排查半天才定位到尺寸问题。
4.4 改了代码重新构建,进游戏还是旧行为
这个现象特别具有迷惑性:代码明明改了,gradlew build也显示成功,但进游戏行为完全没变,像是没编译。原因是 Gradle 的增量构建缓存把旧的产物拿来做增量,而 IDE 的构建缓存和 Gradle 缓存不完全一致,导致打出来的 jar 还是旧文件。
解决方法是养成一个固定习惯:改动后先./gradlew clean build,再运行。clean会清掉build/classes、build/resources和build/libs里的旧产物。另外要确认 IDE 没有启用「委托 IDE 构建」以外的构建方式,比如 IDE 自己把类文件输出到了out/目录而你跑的是 Gradle,两者互相覆盖,属于最常见的假编译成功。
4.5 引入第三方库后,Mixin 注入失败
1.20.1 的模组生态里,很多辅助库都依赖 Mixin 注入。这类问题的现场是:加入库后游戏启动阶段直接抛异常,信息类似Mixin apply failed,指向某个已经被 Forge 核心类加载的类。原因是部分库的 Mixin 配置目标类和 Forge 自己的类加载顺序产生冲突,特别是在客户端启动早期被 mixin 的类会在后续 Forge 处理时二次转换,直接炸掉。
解决思路按优先级来。首先看库有没有发布 Forge 专属版本,优先选用明确声明支持 Forge 1.20.1 的版本。其次,查看库的 mixin json 里的minVersion和compatibilityLevel,确认没有设置过高的版本要求。最后,确实冲突时要在 build.gradle 里排除依赖中的重复字节码库,而不是硬扛。Mixin 冲突的排查很费时,我的经验是集成任何库前先查它是否声明了 Forge 支持,不支持就换方案,不要自己做适配。
5. 构建与发布:Gradle 任务链路和那道 SRG 映射坎
开发环境里跑通不等于能发布。模组在开发环境下加载的是一套映射名,真实玩家启动游戏加载的是另一套混淆名。这一章把构建命令、产物路径和映射问题讲透,避免本地正常、发布包崩溃的尴尬。
5.1 build、runClient、runData 分别干嘛
ForgeGradle 提供了几个常用任务,功能完全不同。开发期间,./gradlew runClient启动开发环境客户端,./gradlew runServer启动服务端。发布时只用./gradlew build。还有一个容易被忽略的./gradlew runData,它跑数据生成器,很多模组用它自动生成模型和语言文件。
# 开发期:启动客户端 ./gradlew runClient # 发布期:清理并构建 ./gradlew clean build # 数据生成(如果配置了生成器) ./gradlew runDataclean build构建完的 jar 在build/libs/下。注意这个目录会同时生成两个文件:一个普通 jar 和一个带-sources后缀的源码 jar。发布时只放不带-sources的那个。很多人连源码 jar 一起丢到服务端 mods 目录,不影响加载但纯属多余。
5.2 SRG 映射:为什么本地能跑,发布包就崩
这是 Forge 开发入门到进阶的一道分水岭。Minecraft 官方在 1.17 之后发布的是 Mojang 映射名,而 Forge 运行时用的是 SRG 混淆名,开发环境通过 ForgeGradle 做反向映射,让你在代码里写item.getDescriptionId(),编译后自动转换。问题出在字符串引用上:如果你在代码里硬编码了一个类名或字段名,比如反射获取某个方法,开发环境里正确,但发布后目标方法在运行时叫的是 SRG 名,反射找不到。
// 危险写法:发布后字段名会被重映射 Field f = YourClass.class.getDeclaredField("someField"); // 安全写法:用 Forge 提供的字段/方法引用机制 // 或直接避免反射,通过正常 API 获取解决策略很简单:尽量不要使用按名字查找的反射 API。Forge 本身提供的注册表、能力系统、事件系统基本覆盖了绝大多数需求,没必要碰反射。如果第三方库内部使用了反射且不兼容 SRG,就要找库作者发布的重映射版本——这就是为什么很多库会在发布页标明「Forge 版」。发布前用一个干净环境测试是唯一可靠的验证手段。
5.3 发布前的自检清单
打包发布前,花两分钟按表过一遍,能挡住大部分问题。
| 检查项 | 怎么验证 | 常见问题 |
|---|---|---|
| mods.toml 存在 | 查看 jar 内 META-INF/mods.toml | 打包时把 resources 目录漏了 |
| loaderVersion 范围 | 打开 mods.toml 查看 | 写成了 Minecraft 版本号 |
| 资源文件在 jar 内 | 查看 assets/ 目录结构 | 路径大小写不对,Linux 上加载失败 |
| 无 sources jar | 检查 build/libs | 把源码包发给了玩家 |
| 无本地路径引用 | 搜索 jar 内是否有绝对路径 | 开发机路径被写进配置 |
检查 jar 内容用 unzip 命令即可:
unzip -l build/libs/examplemod-1.0.0.jar | grep -E "mods.toml|assets/examplemod"输出里至少要有META-INF/mods.toml和贴图、模型文件。如果只有 class 文件没有资源,说明build.gradle里的资源目录没包含src/main/resources,这是能编译但完全不能用的情况。
6. 把别人的模组源码适配到 forge-1.20.1:三条快速判断法则
在 1.20.1 这个版本上,最常被问的一件事是「这个模组的源码我能不能直接拿过来改」。这里分享我判断源码兼容性的三个习惯。第一,看build.gradle里minecraft配置块中的mappings行——如果是official_mojang或parchment,移植到 1.20.1 的成本相对低;如果是某个固定版本的内部分支映射,基本只能整体改 API。第二,看mods.toml里loaderVersion的下限,如果要求的 Forge 版本远高于 1.20.1 的 47.x,说明对方可能用了新版本 API,要谨慎。第三,在主类里搜几个 1.21 才出现的关键 API,搜到就直接放弃移植,那不是在适配,是在重写。
我现在的日常流程是先跑./gradlew build确认空白骨架成功,再开始写业务代码,每加一个功能就 build 一次。这个习惯帮我躲过了大量「写了一大堆后一次构建全都报错」的处境,构建日志里 200 个错误和 20 个错误的排查心态完全不一样。Forge 1.20.1 的价值在于它是一个处在稳定期的版本,API 不会突然变动,社区积累的问答足够覆盖绝大多数诡异情况。掌握框架的边界之后,你会慢慢发现版本焦虑是多余的——在 1.20.1 上沉淀一套可以复用的注册模板和资源目录结构,换版本时只是改 API 调用而已。希望这些踩坑记录能让你少走几步弯路。
本文还有配套的精品资源,点击获取