☰
Forge 1.20.1 模组开发:Java 工程化实践与 Gradle 构建指南
2026/10/27 20:59:43 网站建设 项目流程

简介:本资源是适用于Minecraft 1.20.1版本的Forge模组开发工具包(MDK),面向Java初学者、游戏模组开发者及开源技术爱好者,解决Minecraft模组环境搭建、项目初始化与基础构建自动化等核心问题。压缩包共17个文件,涵盖4个关键txt文档(含变更日志、许可证与致谢)、2个Gradle配置文件(build.gradle与settings.gradle)、2个Java源码模板、2个properties配置(gradle.properties与wrapper配置)、1个bat脚本(Windows构建入口)及1个jar(Gradle Wrapper核心),整体仅111KB,轻量易部署。已有1506人学习下载,说明其作为入门级开发起点具备高实用性与社区认可度。用户可直接基于该结构创建首个Forge模组:开箱即用的Gradle构建体系支持一键编译、打包与测试;标准目录组织(src/main/java/resources)符合Mojang官方规范;配套changelog.txt与README.txt便于快速掌握版本适配要点与开发流程,是理解Forge生态与实践Java游戏开发的理想起点。

1. 这不是 Minecraft 启动器,而是你本地 Java 工程的「模组开发沙盒」:Forge 1.20.1 是一套可调试、可断点、可单元测试的 Minecraft 模组构建框架,专为需要深度定制游戏逻辑、复用现有 Java 生态(如 Netty、Jackson、SLF4J)或对接企业级 CI/CD 流水线的开发者设计。它不封装 Minecraft 原生代码,而是通过 ASM 字节码重写 + Mixin 注入,在保留 Mojang 官方 jar 结构的前提下,为你暴露完整的net.minecraft.*包访问权和生命周期钩子。如果你正卡在「想改合成表但找不到注册入口」「想加个 REST API 却被游戏线程锁死」「CI 构建时 Gradle 总报Could not resolve :minecraft」——那这不是版本选错了,是你还没把 Forge 当成标准 Java 项目来管。它适合两类人:一是从 Spring Boot 转过来、习惯用@EventListener写事件驱动逻辑的后端工程师;二是高校某实验室做数字孪生仿真时,需把 Minecraft 作为三维可视化前端、后台跑物理引擎的跨学科团队。


2. 从零初始化一个可立即编译、可远程调试、带完整 IDE 支持的 Forge 1.20.1 工程

Forge 1.20.1 的核心价值不在“能运行”,而在“能工程化”。它早已脱离早期“解压 jar 改 class”的野路子,转为基于 Gradle 的标准化构建流程。关键在于:你不是在配置一个游戏模组,而是在初始化一个依赖了minecraft-1.20.1-client和minecraft-1.20.1-server两个特殊坐标(GAV)的 Java 多模块项目。下面步骤全部基于官方推荐的forgegradle插件 v5.1+,实测兼容 JDK 17(OpenJDK 17.0.2+)、Gradle 8.4+、IntelliJ IDEA 2023.3+。

2.1 用官方模板生成基础骨架:./gradlew genSources不是可选项,是强制前置

不要手动创建build.gradle。Forge 1.20.1 强制要求使用forgegradle提供的genSources任务生成反编译源码与映射表,否则后续所有 IDE 导入、断点调试、符号跳转都会失效。执行前请确认:

  • 已安装 JDK 17(java -version输出含17.0.)
  • 当前目录为空(避免污染)
  • 网络可访问https://files.minecraftforge.net/(国内用户建议提前配置 Maven 镜像)
# 1. 下载官方模板脚本(仅需一次) curl -o gradle.properties https://raw.githubusercontent.com/MinecraftForge/ForgeGradle/5.1/src/templates/gradle.properties curl -o build.gradle https://raw.githubusercontent.com/MinecraftForge/ForgeGradle/5.1/src/templates/build.gradle curl -o settings.gradle https://raw.githubusercontent.com/MinecraftForge/ForgeGradle/5.1/src/templates/settings.gradle # 2. 初始化 Gradle wrapper(确保版本匹配) ./gradlew wrapper --gradle-version 8.4 # 3. 执行源码生成(耗时约 3~8 分钟,取决于网络) ./gradlew genSources

提示:genSources会下载minecraft-1.20.1-mapped-official_1.20.1的 SRG 映射文件、反编译 client/server jar,并生成src/main/java/net/minecraft/client/等包结构。这是整个开发链路的基石——没有它,你在 IDEA 里 Ctrl+Click 任何Minecraft.getInstance()都会跳转到乱码 class。

2.2 在 IntelliJ IDEA 中正确导入:禁用自动构建,启用「Delegate IDE build/run actions to Gradle」

很多翻车源于 IDEA 自作聪明。Forge 1.20.1 的编译流程必须由 Gradle 全权控制,IDEA 只负责代码提示与调试。按顺序操作:

  1. File → New → Project from Existing Sources,选择你刚执行genSources的目录
  2. 在导入向导中,勾选"Use auto-import",但取消勾选 "Create separate module per source set"(避免生成冗余 module)
  3. 进入Settings → Build → Build Tools → Gradle:
    • Build and run using:Gradle
    • Run tests using:Gradle
    • ✅ 勾选"Delegate IDE build/run actions to Gradle"
  4. Settings → Editor → Inspections → Java → Classpath Problems:将Cannot resolve symbol 'net.minecraft'的严重级别调为Warning(因为 IDEA 无法原生识别 SRG 映射,但 Gradle 可以)

完成导入后,你会看到src/main/java下出现net.minecraft.*包,且所有类名可正常跳转。此时右键src/main/java→"Mark as Sources Root"(若未自动标记)。

2.3 编写第一个可运行模组:@Mod注解不是装饰器,而是 Gradle 构建期注入的启动契约

Forge 1.20.1 的模组入口不再是main方法,而是通过@Mod注解触发 Gradle 在构建时向META-INF/mods.toml注入元数据,并在游戏启动时由ModLoader扫描加载。你的主类必须满足三个硬性条件:

  • 类路径必须在src/main/java下(不能在test或resources)
  • 必须有无参构造函数(public ModClass() {})
  • @Mod的 value 必须与mods.toml中modId完全一致(大小写敏感、无空格、仅限 ASCII)
// src/main/java/com/example/myfirstmod/MyFirstMod.java package com.example.myfirstmod; import net.minecraft.world.item.CreativeModeTab; import net.minecraftforge.api.distmarker.Dist; import net.minecraftforge.common.MinecraftForge; import net.minecraftforge.event.server.ServerStartingEvent; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @Mod("myfirstmod") // ← 必须与 mods.toml 中 modId 严格一致 public class MyFirstMod { private static final Logger LOGGER = LogManager.getLogger(); public MyFirstMod() { // 获取 FML 的事件总线 IEventBus modEventBus = FMLJavaModLoadingContext.get().getModEventBus(); // 注册通用初始化事件 modEventBus.addListener(this::setup); // 注册客户端专用初始化(如渲染器、模型加载) modEventBus.addListener(this::doClientStuff); // 注册服务端事件(如命令、世界生成) MinecraftForge.EVENT_BUS.register(this); } private void setup(final FMLCommonSetupEvent event) { LOGGER.info("HELLO FROM SETUP"); // 此处放通用逻辑:注册物品、方块、事件监听器等 } private void doClientStuff(final FMLClientSetupEvent event) { LOGGER.info("HELLO FROM CLIENT SETUP"); // 此处放客户端逻辑:注册渲染器、覆盖 GUI 等 } @SubscribeEvent public void onServerStarting(ServerStartingEvent event) { LOGGER.info("SERVER STARTING: {}", event.getServer().getServerName()); } }

参数说明:@Mod("myfirstmod")中的字符串是模组唯一 ID,它会:

  • 决定mods.toml中modId=的值
  • 影响资源路径(如assets/myfirstmod/models/item/)
  • 控制日志前缀([myfirstmod])
  • 作为ModList.get().getModContainerById("myfirstmod")的查询键
    若此处写错,模组根本不会被加载,且无任何错误日志——这是新手最常踩的静默失败点。

2.4 构建并运行:runClient任务不是快捷方式,而是嵌入式 Minecraft 实例的完整生命周期管理

不要双击runClient图标。务必在终端中执行,以便捕获 Gradle 日志:

# 清理旧构建(避免缓存污染) ./gradlew clean # 构建并启动客户端(首次会下载大量依赖,约 1.2GB) ./gradlew runClient

该命令实际执行以下动作链:

  1. compileJava→ 编译你的源码(含@Mod类)
  2. processResources→ 将src/main/resources下的mods.toml、pack.mcmeta等复制到build/resources/main
  3. makeObfJar→ 使用SpecialSource将你的代码从named映射转为srg,确保与 Minecraft 字节码兼容
  4. runClient→ 启动一个 JVM,加载net.minecraft.client.main.Main,并注入你的模组 jar 到 classpath

成功启动后,你会看到 Minecraft 启动器界面 → 加载进度条 → 进入主菜单。此时打开logs/latest.log,搜索HELLO FROM SETUP,确认你的模组已加载。

关键验证点:若latest.log中无任何myfirstmod相关日志,但 Minecraft 正常进入主菜单,则说明模组未被扫描到。请立即检查:

  • mods.toml中modId="myfirstmod"是否与@Mod值一致
  • build.gradle中是否包含modImplementation "net.minecraftforge:forge:1.20.1-47.2.0"(版本号必须匹配)
  • src/main/resources/META-INF/mods.toml是否真实存在(不是.toml.template)

3.mods.toml不是配置文件,而是 Forge 的模组契约声明书:字段含义、必填规则与动态注入机制

mods.toml是 Forge 1.20.1 的核心契约文件,它不参与 Java 编译,但在游戏启动时被ModFile解析为ModFileInfo对象,决定模组能否被加载、何时加载、以何种权限加载。它的每个字段都对应底层ModLoader的校验逻辑,填错即拒载。

3.1 最小可用mods.toml结构:6 个字段缺一不可

以下是最简但可运行的src/main/resources/META-INF/mods.toml(注意路径必须精确到META-INF/):

# Mandatory fields modId="myfirstmod" version="1.0.0" displayName="My First Mod" description="A demo mod for Forge 1.20.1" authors=["dev"] credits="Thanks to Forge team" # Required by Forge 1.20.1+ [[dependencies.myfirstmod]] modId="forge" mandatory=true versionRange="[47.2.0,)" ordering="NONE" side="BOTH" [[dependencies.myfirstmod]] modId="minecraft" mandatory=true versionRange="[1.20.1,1.20.2)" ordering="NONE" side="BOTH"

字段解析:

  • modId:必须与@Mod注解值完全一致,且只能是小写字母、数字、下划线([a-z0-9_]),长度 2~64 字符
  • version:语义化版本(SemVer),影响ModList.get().getModContainerById("myfirstmod").getVersion()返回值
  • displayName:显示在模组列表中的名称,支持空格与中文(但建议用英文,避免某些字体渲染异常)
  • description:纯文本描述,长度建议 < 200 字符(过长会被截断)
  • authors:字符串数组,非空即可(["dev"]合法)
  • credits:致谢字段,不影响功能
  • [[dependencies.*]]:必须至少声明forge和minecraft两个依赖。versionRange语法为[min,max),[47.2.0,)表示 ≥47.2.0 的任意版本;[1.20.1,1.20.2)表示仅兼容 1.20.1.x 系列(Forge 1.20.1 对应的推荐版本是47.2.0,可通过./gradlew dependencies --configuration modRuntimeClasspath查看)

3.2 动态注入机制:mods.toml如何影响ModContainer生命周期

当你执行runClient时,Forge 的ModFile类会解析mods.toml并生成ModContainer实例。这个过程不是静态读取,而是动态绑定:

mods.toml字段绑定到ModContainer的哪个属性影响行为
modIdgetModId()返回值决定ModList.get().getModContainerById()查询键;影响资源定位(assets/{modId}/)
versiongetVersion()返回值决定模组更新检查逻辑;影响ModList.get().getModContainerById().getModInfo().getVersion()
side="BOTH"getSide()返回Dist.DEDICATED_SERVER或Dist.CLIENT控制@Mod构造函数中FMLJavaModLoadingContext.get().getModEventBus()的可用性(BOTH时始终可用)
mandatory=trueisLoaded()返回true若依赖项缺失,整个模组加载失败,抛出ModLoadingException

血泪经验:曾遇到某模拟项目 X 因mods.toml中side="CLIENT"导致服务端无法启动。原因在于:side="CLIENT"时,ModContainer在服务端环境被标记为NOT_LOADED,其@Mod构造函数根本不会执行,所有addListener()调用均被跳过。解决方案是统一设为BOTH,再用DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> { ... })区分执行环境。

3.3 版本兼容性陷阱:versionRange不是越宽越好,而是要精确匹配 Forge 推荐版本

Forge 官方文档明确指出:每个 Minecraft 版本只保证与一个 Forge 版本完全兼容。例如 Minecraft 1.20.1 的推荐 Forge 版本是47.2.0(对应forge-1.20.1-47.2.0)。若你在mods.toml中写versionRange="[47.0.0,)",看似兼容性更强,实则埋下隐患:

  • 47.0.0存在已知 Mixin 注入 bug,导致PlayerEvent.PlayerLoggedInEvent无法触发
  • 47.1.0修改了LevelChunk的序列化逻辑,与47.2.0不兼容
  • 47.3.0+已开始实验性支持 Java 21,但1.20.1官方 jar 仍基于 Java 17 编译

因此,versionRange应严格锁定为[47.2.0](精确版本)或[47.2.0,47.3.0)(排除下一个大版本)。验证方法:查看https://files.minecraftforge.net/maven/net/minecraftforge/forge/1.20.1-47.2.0/页面的changelog.txt,确认其标注为"Recommended"。

实操技巧:在build.gradle中强制指定 Forge 版本,避免mods.toml与构建配置不一致:

dependencies { // ✅ 正确:版本号与 mods.toml 中 versionRange 严格对应 modImplementation "net.minecraftforge:forge:1.20.1-47.2.0" // ❌ 错误:使用 latest 或 release,可能拉取非推荐版本 // modImplementation "net.minecraftforge:forge:1.20.1-latest" }

4. 避坑:5 条 Forge 1.20.1 开发中高频静默失败现象与根因定位法

Forge 1.20.1 的错误往往不报红,而是静默跳过、日志缺失、功能不生效。以下是我在某高校某实验室支撑数字孪生仿真项目时,累计记录的 5 条最高频、最隐蔽的坑,每条都附带可立即执行的诊断命令。

4.1 现象:@SubscribeEvent方法从未被调用,logs/latest.log中无任何相关日志

原因:事件总线注册失败。常见于MinecraftForge.EVENT_BUS.register(this)被放在@Mod构造函数之外,或this对象在事件总线注册前已被 GC。
解决:

  • 确保MinecraftForge.EVENT_BUS.register(this)在@Mod构造函数中执行(如 2.3 节代码所示)
  • 在register后添加日志:LOGGER.info("Registered to MinecraftForge.EVENT_BUS");
  • 若仍无效,执行./gradlew runClient --debug | grep "EVENT_BUS",确认日志中出现Registering listener

4.2 现象:自定义物品/方块在创造模式物品栏中不显示,但GameTest能正常获取实例

原因:CreativeModeTab注册时机错误。Forge 1.20.1 要求所有物品必须在FMLCommonSetupEvent之后、DeferredRegister的register()调用之前注册到CreativeModeTab。
解决:

  • 不要在setup()方法中直接item.setCreativeTab(CreativeModeTabs.BUILDING_BLOCKS)
  • 改用DeferredRegister并显式绑定 Tab:
    public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, "myfirstmod"); public static final RegistryObject<Item> MY_ITEM = ITEMS.register("my_item", () -> new Item(new Item.Properties().tab(CreativeModeTabs.BUILDING_BLOCKS)));
  • 确保ITEMS.register(modEventBus)在modEventBus.addListener(this::setup)之前执行

4.3 现象:runClient启动后卡在Loading World,CPU 占用 100%,latest.log无新日志

原因:@Mod构造函数中执行了阻塞 IO(如FileInputStream读取大文件)或无限循环,阻塞了 Forge 的主线程调度。
解决:

  • 在@Mod构造函数中禁止任何 IO、网络、循环、sleep
  • 将耗时操作移至FMLCommonSetupEvent或FMLClientSetupEvent回调中
  • 启动时加 JVM 参数-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=./heap.hprof,用 VisualVM 分析线程栈

4.4 现象:自定义模型/纹理在游戏内显示为紫黑方块(Missing Texture)

原因:资源路径大小写不匹配。Windows 文件系统不区分大小写,但 Forge 的ResourceLocation解析器严格区分。
解决:

  • 检查src/main/resources/assets/myfirstmod/models/item/my_item.json路径是否全小写
  • 确认 JSON 中parent字段指向item/generated(而非Item/generated)
  • 执行./gradlew runClient --info | grep "myfirstmod",查看资源加载日志是否报Failed to load model

4.5 现象:gradlew build成功,但生成的build/libs/*.jar无法被其他模组依赖(Could not resolve)

原因:build.gradle中未配置maven-publish插件,导致 jar 未发布到本地 Maven 仓库。
解决:

  • 在build.gradle中添加:
    plugins { id 'maven-publish' } publishing { publications { mavenJava(MavenPublication) { from components.java } } repositories { mavenLocal() } }
  • 执行./gradlew publishToMavenLocal
  • 其他项目即可用implementation 'com.example:myfirstmod:1.0.0'依赖

5. 进阶技巧:用runData任务生成可复用的数据包(datapack),实现模组逻辑与数据分离

Forge 1.20.1 的runData任务常被误解为“仅用于生成默认数据”,实则是你将模组中可热更、可配置、可版本化管理的业务数据(如合成表、战利品表、维度配置)抽离为标准 Minecraft 数据包的核心工具。它让你的模组具备「无需重新编译 Java 代码即可调整游戏规则」的能力,这对某跨平台系统中需频繁迭代数值平衡的策划团队至关重要。

5.1runData的本质:一个嵌入式数据生成器,输出符合 Minecraft 原生格式的 JSON

执行./gradlew runData会启动一个精简版 Minecraft 服务端,加载你的模组,然后调用所有实现了GatherDataEvent的监听器,最终将生成的 JSON 写入src/main/resources/data/。它不生成 Java 字节码,只生成data/{modid}/下的标准数据包结构。

首先,在build.gradle中启用数据生成:

// 启用 data 任务 loom { runs { data { workingDirectory project.file("run") args "--mod", "myfirstmod", "--all", "--output", file("src/main/resources/") } } }

然后编写数据生成器:

// src/main/java/com/example/myfirstmod/data/ModDataGenerator.java package com.example.myfirstmod.data; import net.minecraft.data.DataGenerator; import net.minecraft.data.recipes.FinishedRecipe; import net.minecraft.data.recipes.RecipeProvider; import net.minecraft.data.recipes.ShapedRecipeBuilder; import net.minecraft.world.item.Items; import net.minecraftforge.data.event.GatherDataEvent; import net.minecraftforge.eventbus.api.SubscribeEvent; import net.minecraftforge.fml.common.Mod; import java.util.function.Consumer; @Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD) public class ModDataGenerator { @SubscribeEvent public static void gatherData(GatherDataEvent event) { DataGenerator generator = event.getGenerator(); if (event.includeServer()) { // 生成合成表 generator.addProvider(event.includeServer(), new ExampleRecipes(generator.getPackOutput())); } } // 示例:生成一个用木棍合成的自定义物品 public static class ExampleRecipes extends RecipeProvider { public ExampleRecipes(PackOutput output) { super(output); } @Override protected void buildRecipes(Consumer<FinishedRecipe> consumer) { ShapedRecipeBuilder.shaped(RecipeCategory.MISC, ModItems.MY_ITEM.get()) .pattern("I") .define('I', Items.STICK) .unlockedBy("has_stick", has(Items.STICK)) .save(consumer); } } }

关键逻辑:GatherDataEvent会在runData启动时触发,RecipeProvider的save()方法会将ShapedRecipeBuilder生成的 JSON 写入src/main/resources/data/myfirstmod/recipes/my_item.json。该文件结构与原生 Minecraft 数据包完全一致,可直接被其他模组或服务器复用。

5.2 数据包与模组代码的协同工作流:如何让 Java 逻辑读取 JSON 配置

数据包的价值在于「可外部修改」。但 Java 代码如何读取这些 JSON?答案是:不直接读取,而是通过RegistryObject和DeferredRegister的延迟绑定机制,在运行时由 Forge 自动注入。

例如,你想让合成表中的my_item数量可配置:

  1. 在data/myfirstmod/loot_tables/blocks/my_block.json中定义战利品表
  2. 在 Java 中通过LootTableSerializer注册自定义解析器
  3. 在CommonSetupEvent中调用LootTableLoadEvent监听器,动态修改掉落率

但更轻量的做法是:将配置项写入data/myfirstmod/tags/items/,然后用TagKey.create(Registries.ITEM, ResourceLocation.fromNamespaceAndPath("myfirstmod", "configurable_items"))在代码中引用。这样,策划只需编辑tags/items/configurable_items.json,无需触碰 Java 代码。

5.3 验证数据包是否生效:三步定位法

当runData执行完毕,务必验证生成结果:

  1. 检查文件结构:

    find src/main/resources/data/myfirstmod -name "*.json" | head -10 # 应输出类似: # src/main/resources/data/myfirstmod/recipes/my_item.json # src/main/resources/data/myfirstmod/loot_tables/blocks/my_block.json
  2. 检查 JSON 格式合法性(避免手误逗号):

    jq -n 'empty' src/main/resources/data/myfirstmod/recipes/my_item.json 2>/dev/null || echo "JSON invalid"
  3. 启动游戏并验证:

    • 启动runClient
    • 按T打开聊天框,输入/reload(强制重载数据包)
    • 输入/recipe give @s myfirstmod:my_item,确认物品可合成

我的习惯:从那以后我每次提交data/目录前,都强制走一遍./gradlew runData && ./gradlew runClient && /reload验证流。因为数据包一旦出错,玩家端只会看到“合成失败”,没有任何日志提示——这比 Java 报错更难 debug。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询