在一档名为《石头世界》的系列内容第 4 季第 14 集里,“给鸡挤奶?还要制作成混沌碎片?”听起来像一句整活台词。但把它当成玩法需求交给 Minecraft 模组开发者时,这句话包含的信息量并不小:玩家要和鸡交互、交互结果要产出一种特殊物品、这个物品还要进入合成链变成“混沌碎片”。这里不讨论剧情,只把标题当作需求输入,实现一个可运行的 Forge 1.20.1 模组原型,并通过这次开发把实体交互、物品注册、事件监听、NBT 冷却、配方 JSON 和配置外置的完整链路串起来。
这类需求最大的特征是:原始描述很短,可执行细节几乎为零。开发者的任务不是照着标题写代码,而是先确定一套合理假设,再让假设变成可验证的流程。本文使用的假设是:玩家手持玻璃瓶右键鸡,消耗一个玻璃瓶后获得“鸡奶瓶”;鸡奶瓶和一块普通石头在工作台里合成“混沌碎片”。
1. 先把“给鸡挤奶做成混沌碎片”拆解成可执行需求
任何一个看似荒诞的需求,落到代码里都要先拆成可执行动作。标题里的“给鸡挤奶”是行为层,“制作成混沌碎片”是结果层。中间还有一个隐含状态:挤出来的奶要先变成一种中间物品,否则“制作”没有材料来源。
1.1 标题里其实有两套玩法
如果只看“给鸡挤奶”,首先要回答四个问题:
- 玩家用什么物品触发这个交互。
- 交互是否消耗物品。
- 交互是否有次数限制或冷却。
- 这个交互是否只对鸡生效,还是对一类动物生效。
再看“还要制作成混沌碎片”,又要回答另外几个问题:
- 混沌碎片是一个物品、一个方块,还是一种特殊货币。
- 碎片是挤奶时直接掉落,还是需要二次合成。
- 如果二次合成,使用的配方是什么。
- 合成之后,鸡奶瓶是否消耗。
原版 Minecraft 里,鸡只负责下蛋,不存在“挤奶”这个行为。右键鸡时,原版逻辑不会给出任何奶类结果。所以这个需求必须通过事件系统拦截实体交互,在玩家手持指定物品右键鸡时,取消原版逻辑并执行自定义逻辑。
这里采用一种最小闭环:把鸡奶瓶设计为普通物品,而不是真正的流体桶。普通 Item 足够验证行为链路;真正的流体桶需要额外实现 Fluid、桶物品和流体渲染,会让原型复杂很多。
1.2 需求拆成四个模块
为了让开发过程不混乱,可以把整个需求拆成四个模块。
实体交互模块负责判断“玩家手持玻璃瓶右键鸡”这个动作是否成立,以及是否处于冷却状态。物品模块负责注册鸡奶瓶和混沌碎片,并设置堆叠数量、物品 ID、显示名称等属性。合成模块负责定义“鸡奶瓶 + 石头 = 混沌碎片”的配方,让碎片能通过工作台制作。状态与配置模块负责记录玩家上次挤奶的时间,并把冷却时间、产出数量等参数放到配置文件里,避免每次调整都要重新编译。
这四个模块不是并列关系,而是依赖关系:事件模块产生鸡奶瓶,物品模块提供鸡奶瓶,合成模块消费鸡奶瓶,配置模块约束事件模块的触发频率。
1.3 先定技术路线:数据包、Forge 还是 Fabric
| 方案 | 能实现什么 | 不能实现什么 | 适合场景 |
| --- | --- | --- | --- | | 纯数据包 | 物品标签、配方、进度、部分战利品 | 新增自定义物品、拦截实体交互、写入玩家 NBT | 已经存在基础物品,只是调整配方 | | Fabric | 自定义物品、事件监听、服务端逻辑 | 需要掌握 Fabric Loader 和对应 API | 团队已经使用 Fabric 生态 | | Forge | 自定义物品、事件监听、NBT 操作、配置系统 | 需要处理较重的 MDK 结构 | 社区资料多,适合教学和快速验证 |
这里选择 Forge 1.20.1,原因是它的PlayerInteractEvent、DeferredRegister、ForgeConfigSpec都属于比较稳定的 API,网上资料也多。对于“给鸡挤奶”这种需要新增交互逻辑的需求,纯数据包做不了,因为数据包没有办法在玩家右键鸡时执行自定义逻辑。
1.4 先想清楚物品是普通 Item 还是流体容器
标题里提到“奶”,容易让人下意识去实现流体。但一个最小原型不应该一上来就做流体。合理做法是先把“鸡奶”建模成一个普通物品,名叫“鸡奶瓶”,堆叠数量设为 16。它只是一个中间材料,不参与真正桶装液体的倾倒、放置和填充逻辑。
这样做的取舍是:视觉上不够真实,鸡奶瓶不能倒出来;换来的是开发链路短,能快速验证“交互、产出、合成”这个主干。后续需要真实流体时,再基于这个结构扩展 Fluid 和 BucketItem。
2. 环境准备与项目骨架:先锁版本,再写事件
2.1 需要准备的环境
模组开发中大多数问题都出在版本不匹配。写代码之前,先确认 JDK、Minecraft、Forge 和 Gradle 的版本关系。
| 环境项 | 推荐值 | 说明 |
|---|---|---|
| JDK | 17 | Minecraft 1.20.1 使用 Java 17 编译和运行 |
| 开发 IDE | IntelliJ IDEA | 方便导入 Gradle 项目 |
| Minecraft | 1.20.1 | 社区模组兼容性较高 |
| Forge MDK | 47.x | 以 Forge 官网或 Maven 列表为准 |
| Gradle | MDK 自带 wrapper | 不需要单独安装 |
下载 Forge MDK 后,先不要急着写代码。打开gradle.properties,把版本号固定下来,再启动gradlew导入项目。
如果原始材料没有给出明确版本,落地前要先确认依赖版本。不同 Minecraft 主版本对应不同 Forge API,代码风格也会不同,尤其是事件注册和配置系统的写法。
2.2 Gradle 配置要点
在gradle.properties中写入以下内容:
minecraft_version=1.20.1 forge_version=47.1.0 mappings_channel=official mappings_version=1.20.1其中mappings_channel=official表示使用 Mojang 官方映射。对新手来说,官方映射名称和代码里看到的Chicken、Item、Player一致,阅读和调试都比较直观。
build.gradle关键段落如下:
plugins { id 'net.minecraftforge.gradle' version '[6.0,6.2)' } dependencies { implementation fg.deobf("net.minecraftforge:forge:${minecraft_version}-${forge_version}") }fg.deobf会自动处理 Minecraft 和 Forge 的反混淆依赖,开发者不需要手动管理运行时库。
2.3 项目目录结构
一个最简单的 Forge 模组项目结构如下:
stoneworld-mod/ ├── build.gradle ├── gradle.properties └── src/main/ ├── java/com/stoneworld/ │ ├── StoneWorldMod.java │ ├── item/ModItems.java │ ├── event/ChickenMilkHandler.java │ └── config/StoneWorldConfig.java └── resources/ ├── META-INF/mods.toml ├── assets/stoneworld/ │ ├── lang/zh_cn.json │ └── textures/item/ └── data/stoneworld/ └── recipes/资源文件的路径非常关键。配方文件必须放在data/stoneworld/recipes/下,语言文件必须放在assets/stoneworld/lang/zh_cn.json下。路径一旦写错,不会直接报编译错误,而是运行时配方便静默失效,这是模组开发中最容易浪费时间的坑之一。
3. 实现核心交互:给鸡挤奶并产出一瓶鸡奶
3.1 先把两个物品注册进去
使用 Forge 的DeferredRegister注册物品,这是当前推荐写法。它会在模组加载阶段统一注册物品,同时保证物品 ID 不会冲突。
public class ModItems { public static final DeferredRegister<Item> ITEMS = DeferredRegister.create(ForgeRegistries.ITEMS, StoneWorldMod.MOD_ID); public static final RegistryObject<Item> CHICKEN_MILK_BOTTLE = ITEMS.register("chicken_milk_bottle", () -> new Item(new Item.Properties().stacksTo(16))); public static final RegistryObject<Item> CHAOS_FRAGMENT = ITEMS.register("chaos_fragment", () -> new Item(new Item.Properties().stacksTo(64))); public static void register(IEventBus bus) { ITEMS.register(bus); } }stacksTo(16)是因为这瓶奶被定位为中间材料,数量太多会显得不合理;混沌碎片作为最终合成材料,堆叠到 64 比较顺手。如果后续想让物品拥有特殊提示文字或稀有度,再单独继承Item类并重写方法。
StoneWorldMod里的注册代码如下:
@Mod(StoneWorldMod.MOD_ID) public class StoneWorldMod { public static final String MOD_ID = "stoneworld"; public StoneWorldMod() { IEventBus bus = FMLJavaModLoadingContext.get().getModEventBus(); ModItems.register(bus); ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, StoneWorldConfig.SPEC); } }3.2 监听玩家与鸡的交互
Forge 提供 `Player