Minecraft模组开发 Java AIGC

Minecraft 1.21 NeoForge 模组开发笔记(macOS)

本文将为一名有 Java 基础但从未涉足 Minecraft Java 模组开发的读者,提供一个 Minecraft Java 版 1.21、基于 NeoForge 框架、适配 macOS (Apple Silicon / arm64) 的完整教程。

希茉
107分钟
21226 字
Minecraft 1.21 NeoForge 模组开发笔记(macOS)

本文将为一名有 Java 基础但从未涉足 Minecraft Java 模组开发的读者,提供一个面向 Minecraft Java 版 1.21、基于 NeoForge 框架、适配 macOS (Apple Silicon / arm64) 的完整教程。教程涵盖环境搭建、创建第一个模组、注册游戏内容、事件处理、自定义 GUI 与容器、世界生成、数据驱动内容、网络通信,以及一些高级技巧(如 Mixins、反射和条件注册),最后介绍模组的打包发布与版本管理。希望通过本指南,帮助你从零开始制作出一个完整的 Minecraft 模组。


1. 环境搭建

要开始 NeoForge 模组开发,需要先搭建好开发环境,包括安装正确的 JDK、IDE 和构建工具,并确保这些工具在 Apple Silicon 架构下正常运行。

  • 安装 Java 21 JDK: Minecraft 1.21 要求使用 Java 21 运行环境。请在 Oracle 官方网站或 OpenJDK 发布站点下载适用于 macOS Arm64 的 JDK 21,并完成安装(建议将 JAVA_HOME 指向该 JDK)。使用 Apple Silicon 原生的 JDK 可以显著提升构建和运行性能。安装完成后,在终端运行 java -version 确认版本,确保输出为 Java 21 且架构为 ARM 64 位。

  • 配置开发 IDE: 推荐使用 IntelliJ IDEA(社区版即可),NeoForge 官方支持 IntelliJ IDEA 并提供 Gradle 集成。前往 JetBrains 官网下载 macOS AArch64 (Apple Silicon) 版本的 IntelliJ IDEA,并安装。启动 IDEA 后,安装 Gradle 插件(通常已经内置)并确保使用的 JDK 是刚安装的 Java 21。你也可以使用 Eclipse 等其他 IDE,但本教程假定使用 IDEA 进行示例。

  • NeoForge 开发模板: NeoForge 提供模组开发套件 (MDK) 模板,方便我们快速创建项目环境。你有两种方式获取 MDK:

    1. 使用 NeoForge 官方 Mod Generator: 打开 NeoForge 官网的 “Mod Generator”页面,根据提示输入模组信息(如 Mod ID、名称、包名等),生成项目模板 ZIP 并下载。解压后即得到一个包含 Gradle 配置、示例代码和资源文件的模组工程。

    2. 从 GitHub 获取 MDK: 前往 NeoForgeMDKs 仓库下载对应 Minecraft 1.21 版本的 MDK ZIP 文件。例如,可以在 GitHub 上找到 MDK-1.21-ModDevGradle 项目,点击 “Code -> Download ZIP” 下载。下载后解压 ZIP,得到模组工程模板。或者也可以使用 GitHub 的 “Use this template” 功能,将 MDK 模板直接复制为你自己的新仓库,然后克隆到本地。

  • 导入项目并配置 Gradle: 将上一步得到的模组工程用 IntelliJ IDEA 打开。首次导入时,IDEA 会自动检测 Gradle 项目并下载所需依赖,包括 Minecraft 和 NeoForge 的开发库。由于需要从网络获取 Minecraft 源码并反编译,这一步可能耗时较长(取决于网络和硬件,可能需要数十分钟)。请耐心等待 Gradle 同步完成。完成后,IDEA 的 Gradle 工具窗口中应列出项目的各项构建任务。

  • 确保 Apple Silicon 兼容: 得益于 NeoForge 和现代工具链的改进,在 Apple M1/M2 芯片上搭建 Forge/NeoForge 开发环境已非常顺畅。只要使用 ARM64 原生 JDK,Forge/NeoForge 的最新版本将可以直接运行,无需借助 Rosetta 模拟。如果在运行客户端时遇到任何 Apple Silicon 特有的问题(如与 Netty DNS 解析器的已知冲突),请确保所用 NeoForge 版本为最新,因为官方已针对 Apple Silicon 做出优化。在极少数情况下,可以尝试使用 Intel 架构的 Java 运行(通过终端使用 arch -x86_64 启动 IDEA)或关注社区提供的针对 Apple Silicon 的补丁。

完成以上步骤后,开发环境就准备就绪了。接下来可以开始创建和运行你的第一个模组。

2. 创建第一个模组

有了模板工程,我们将创建一个简单的模组,了解模组项目的结构、基本配置和运行方法。

  • 项目结构说明: 模板工程采用标准的 Gradle 多源集结构。主要目录包括:

    • src/main/java:Java 源代码目录。在此包下按照你的包名组织代码。

    • src/main/resources:资源文件目录。包含游戏所需的素材和数据文件。

      • META-INF/neoforge.mods.toml:模组元数据文件(TOML 格式),用于声明模组 ID、名称、版本、描述、依赖等信息。使用 NeoForge 模板时,该文件的大部分内容会由 gradle.properties中的配置自动填充,无需手动修改。

      • assets/<你的modid>/...:资源包内容,包括材质、模型、语言文件、声音等(即只在客户端加载的资源)。

      • data/<你的modid>/...:数据包内容,包括合成配方、战利品表、世界生成配置、标签等(服务器和客户端都会读取)。NeoForge 会为每个模组自动生成符合当前版本要求的 pack.mcmeta,因此无需自己创建这些文件。

    • build.gradle 和 gradle.properties:Gradle 构建脚本和属性文件。gradle.properties 包含模组的大部分通用属性配置,例如 mod_idmod_namemod_version 等。通常只需修改这里的配置即可,Gradle 会将这些值替换到 neoforge.mods.toml 等位置。注意: 模板中的主 Mod 类上通常使用 @Mod注解指定 Mod ID,这里的字符串需要与你在 gradle.properties 中设定的 mod_id 一致,否则加载时会出现找不到模组的错误。

  • 设置模组基本信息: 打开项目的 gradle.properties,根据需要修改其中模组信息:

    • mod_id:模组唯一标识符,建议使用小写字母、数字和下划线组合,长度2-64字符。必须与主 Mod 类的 @Mod 注解参数一致。

    • mod_name:模组展示名称(可有空格和大小写,在 “模组列表” 中显示)。

    • mod_version:模组版本号,建议遵循语义化版本 (Semantic Versioning) 格式(如 1.0.0)。

    • mod_description:模组描述,可写多行简介。

    • mod_authors:作者名单。

    • mod_license:授权协议(用 SPDX 标识符或简短说明,如 MIT、GPL-3.0、All Rights Reserved 等)。

    • 以上属性修改后,Gradle 会自动同步到 neoforge.mods.toml 中对应的占位符。完成后别忘了打开主 Mod 类,更新其中的 @Mod("...") 参数为新的 mod ID。

  • 主 Mod 类结构: 模板项目会提供一个示例的主 Mod 类(通常在 …/ExampleMod.java,类名和包名可能需要你重命名)。典型的主类结构如下:

    @Mod("你的modid")
    public class YourMod {
        // 建议添加一个公共常量保存 modid,如:
        public static final String MOD_ID = "你的modid";
        
        // 构造器:NeoForge 在初始化模组时调用
        public YourMod(IEventBus modEventBus) {
            // 注册内容到事件总线,稍后介绍
            BlocksInit.BLOCKS.register(modEventBus);
            ItemsInit.ITEMS.register(modEventBus);
            ...
            // 注册模组生命周期事件处理
            modEventBus.addListener(this::commonSetup);
            modEventBus.addListener(this::clientSetup);
            LOGGER.info("模组初始化完成");
        }
        
        private void commonSetup(final FMLCommonSetupEvent event) {
            // 一般的初始化内容
        }
        private void clientSetup(final FMLClientSetupEvent event) {
            // 客户端专用的初始化(仅在物理客户端运行)
        }
    }

    上述是一个示例,实际代码可能不同。关键点是:

    • 使用 @Mod("modid") 注解标记主类,Forge/NeoForge 会在启动时实例化该类。

    • 构造函数可以接收一个 IEventBus 参数(即此模组的事件总线)。我们应利用它将模组的注册内容和事件监听器绑定到该总线,以参与相应的初始化阶段。

    • LOGGER 通常是一个日志记录器,用于输出调试信息到控制台。

    • 常见的生命周期事件包括 FMLCommonSetupEvent(服务端和客户端均执行的初始化阶段)和 FMLClientSetupEvent(仅客户端执行的初始化阶段),稍后“事件与生命周期”章节会详细说明。

  • 运行模组: 配置完成后,就可以尝试运行游戏来测试模组。Gradle MDK 已包含所需的运行配置:

    • 使用 Gradle 任务运行: 在 IntelliJ 的 Gradle 工具窗口,展开任务列表找到 runClient,双击即可启动客户端。首次运行会自动下载 Minecraft 1.21 游戏所需资源文件并启动带 NeoForge 的测试客户端。提示: IDEA 可能已经为你生成了预配置的运行配置(Run Configuration),可以直接点击运行。如果没有,运行上述 Gradle 任务会在 run 目录生成启动配置文件,下次即可通过运行该配置启动。

    • 第一次启动过程: 游戏窗口弹出后,可能会先要求选择语言(Language)并同意用户协议等。进入主菜单,点击“模组”按钮,可以看到你的模组已列在列表中,名称、版本和描述与 gradle.properties 设置相符。若未列出,检查控制台日志中是否有错误或 Mod ID 不匹配等问题。

    • 调试与热重载: 你可以在 IDEA 中设置断点调试运行的客户端。在开发过程中,资源文件(assets/data)可以通过按下游戏中的 F3+T 组合键来触发资源重载,无需每次重启游戏。但对代码的改动通常仍需重启客户端才能生效。

    • 运行服务端: 模板同样提供了 runServer 任务用于启动一个本地服务端测试你的模组。第一次运行需要先接受 EULA:服务端会立即退出,在项目根目录的 run 子文件夹下生成 eula.txt,打开将其中 false 改为 true 保存,然后再次运行 runServer。启动后,你可以用客户端加入本地服务器来测试联机情况。注意默认服务器开启 online-mode(正版验证),由于开发环境下客户端通常未登录正版账号,会被踢出,可在 run/server/server.properties 中将 online-mode 设为 false 以便本地测试。建议: 开发完成后 一定 在真实服务器环境下测试,哪怕是纯客户端模组也应确保在服务端加载时不报错。

完成这些步骤,你已经成功创建并运行了一个基础模组工程。接下来,我们将深入讲解如何向游戏注册新的物品、方块等内容。

3. 注册物品与方块(含 BlockEntity 与 JSON 资源)

注册自定义的游戏内容(物品、方块等)是模组开发的核心环节。本节将介绍如何定义和注册新的物品、方块,以及它们相关的资源文件配置。此外,还将介绍如何创建方块实体(BlockEntity)以支持具有复杂状态的方块。

3.1 注册物品和方块的基础

注册机制简介: Minecraft 使用注册表(Registry)来管理游戏中的各种元素(例如方块注册表、物品注册表等)。每个注册表项都有一个唯一名称(ResourceLocation),包括命名空间(通常为模组 ID)和路径。例如,泥土方块的注册名是 minecraft:dirt。模组添加的新元素会使用模组自己的命名空间,如 yourmod:your_block。Forge/NeoForge 提供了便捷的 DeferredRegister 类帮助我们注册内容。DeferredRegister 本质上封装了对注册事件的监听,可避免手动调用低层次的注册方法,降低出错风险。

初始化注册表: 通常我们会为每种内容创建一个独立的注册类。例如可以建立 ModBlocks 和 ModItems 两个类,分别持有方块和物品的 DeferredRegister:

public class ModBlocks {
    public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(YourMod.MOD_ID);
    // 在此类中声明具体的方块(稍后介绍)
}
public class ModItems {
    public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(YourMod.MOD_ID);
    // 在此类中声明具体的物品
}

这样在主 Mod 类构造中,将这两个 DeferredRegister 注册到 modEventBus,即:

ModBlocks.BLOCKS.register(modEventBus);
ModItems.ITEMS.register(modEventBus);

表示让 NeoForge 在适当的时机调用我们定义的注册内容。在 NeoForge 1.21 中,注册事件属于模组事件总线 (modEventBus),并会在模组加载过程中并行执行。确保所有注册调用都在模组初始化时完成,注册表会在稍后阶段冻结,若错过时机再注册会导致错误。

注意: 不要在静态初始化块或构造器之外随意创建方块/物品实例。所有方块/物品对象应当只在注册时创建一次。因为游戏对注册表项要求单例存在,重复实例化会导致注册不一致甚至游戏崩溃。同时,注册必须在注册表未冻结期间进行,NeoForge 会在我们调用 DeferredRegister.register() 时自动处理这些细节。

3.2 注册物品 (Item)

定义物品: 通过 DeferredRegister.Items 提供的便捷方法可以快速注册物品。例如,在 ModItems 类中添加:

public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(YourMod.MOD_ID);

// 注册一个普通物品
public static final DeferredItem<Item> MY_ITEM = ITEMS.registerSimpleItem(
    "my_item", 
    new Item.Properties()  // 物品属性,可以链式设置属性
);

上述代码将注册一个新的物品,命名为 “my_item”(完整资源名为 yourmod:my_item)。这里使用了 registerSimpleItem 方法,它会隐式使用 Item::new 作为工厂,并应用传入的 Item.Properties 属性。物品属性可以设定最大堆叠数、耐久、是否耐火 (fire-resistant) 等。例如:

new Item.Properties().stacksTo(16).fireResistant()

将令物品最多只能堆叠16个且耐火不烧毁。

物品模型与材质: 注册物品后,需要提供对应的模型文件和材质,使其在游戏中有正确的显示。按约定,在资源包路径放置:

  • assets/yourmod/textures/item/my_item.png:物品的纹理图片。

  • assets/yourmod/models/item/my_item.json:物品模型定义。对于普通物品,可使用生成模型:

    {
      "parent": "item/generated",
      "textures": { "layer0": "yourmod:item/my_item" }
    }

    这会将 my_item.png 纹理贴到默认的平面物品模型上。

  • 在 assets/yourmod/lang/zh_cn.json(以及 en_us.json 等)语言文件中添加物品显示名,如:

    { "item.yourmod.my_item": "我的物品" }

    这样游戏界面会显示友好的中文名称。

添加完资源后,重新编译并运行客户端,在创造模式物品栏(或通过给予指令)即可看到新物品。但默认情况下,你的物品不会自动出现在创造模式栏。下一节将介绍如何将物品加入创造物品栏(Creative Tab)。

3.3 注册方块 (Block) 和方块物品

定义方块: 注册方块与物品类似,但有一些特殊之处:

public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(YourMod.MOD_ID);

// 注册一个方块
public static final DeferredBlock<Block> MY_BLOCK = BLOCKS.register(
    "my_block",
    // 使用 lambda 获取注册名 ResourceKey,以便设置方块属性 ID
    registryName -> new Block(BlockBehaviour.Properties
                                .of(Material.STONE)
                                .strength(3.0f, 9.0f)
                                .lightLevel(state -> 7)
                                .sound(SoundType.STONE)
                                .requiresCorrectToolForDrops()
                                .setId(registryName))
);

上面创建了一个名为“my_block”的方块,属性基于石头材质,硬度3.0(铁镐可快速开采),爆炸抗性9.0,发光亮度7,且必须使用正确工具掉落。需要注意的是,必须调用 setId(registryName) 设置方块的注册名,否则 NeoForge 会抛出异常。这里 registryName 由 DeferredRegister 自动提供,即 “yourmod:my_block”。

注册方块对应物品: 大部分方块在破坏后会以物品形式掉落,因此我们需要为每个方块注册一个 BlockItem

public static final DeferredItem<BlockItem> MY_BLOCK_ITEM = ModItems.ITEMS.registerSimpleBlockItem(
    "my_block",      // 物品名称通常与方块同名
    MY_BLOCK,        // 对应的方块对象
    new Item.Properties()
);

以上使用 registerSimpleBlockItem 方法,将自动创建一个 BlockItem 实例,链接到我们定义的 MY_BLOCK,并使用给定的物品属性。注意: 这个方法也会自动设置 BlockItem 的翻译键前缀与方块保持一致(即显示名称可以共用方块的翻译)。

方块模型与状态文件: 与物品类似,我们要提供资源来定义方块的外观:

  • 方块纹理:assets/yourmod/textures/block/my_block.png(可与物品纹理相同或不同)。

  • 方块模型:assets/yourmod/models/block/my_block.json。如果是简单立方体方块,可以使用内置模型父类:

    {
      "parent": "block/cube_all",
      "textures": { "all": "yourmod:block/my_block" }
    }

    这表示一个各面使用相同纹理的标准立方体。

  • 方块状态:assets/yourmod/blockstates/my_block.json

    {
      "variants": {
        "": { "model": "yourmod:block/my_block" }
      }
    }

    这里定义了方块状态(无附加属性时)的模型映射,即所有状态均使用上面定义的 my_block 方块模型。

将资源文件放置正确后,重启游戏,你就可以看到方块渲染正常。使用给予指令获得 yourmod:my_block 并放置,能看到自定义方块出现在世界中。如果方块破坏后未掉落,确保你已经将方块的掉落战利品表配置正确(下一节将提到)。

3.4 方块实体 (BlockEntity) 注册与使用

什么是方块实体: 普通方块只有基本的 ID 和状态(朝向、属性等),而方块实体是一种附加到方块的特殊对象,用于在方块中保存复杂的数据或逻辑(类似于早期 Forge 中的 TileEntity)。例如箱子需要存储物品清单、熔炉需要存储燃烧时间,这些都通过方块实体实现。

注册方块实体类型: 首先需要在模组中注册一个 BlockEntityType。可以在类似 ModBlockEntities 类中:

public static final DeferredRegister<BlockEntityType<?>> BLOCK_ENTITIES =
    DeferredRegister.create(Registries.BLOCK_ENTITY_TYPE, YourMod.MOD_ID);

// 注册方块实体类型
public static final Supplier<BlockEntityType<MyBlockEntity>> MY_BLOCK_ENTITY = BLOCK_ENTITIES.register(
    "my_block_entity",
    () -> BlockEntityType.Builder.of(MyBlockEntity::new, ModBlocks.MY_BLOCK.get()).build(null)
);

这里 MyBlockEntity 是我们自定义的方块实体类,继承自 BlockEntity。我们通过 BlockEntityType.Builder 指定该实体由 MyBlockEntity::new 提供实例,并附加在哪些方块上(本例中仅 ModBlocks.MY_BLOCK)。将 BLOCK_ENTITIES.register(modEventBus) 在主类构造函数注册。

编写方块实体类: 示例:

public class MyBlockEntity extends BlockEntity implements MenuProvider {
    private int counter = 0;  // 示例数据

    public MyBlockEntity(BlockPos pos, BlockState state) {
        super(ModBlockEntities.MY_BLOCK_ENTITY.get(), pos, state);
    }

    // 每刻 (tick) 调用,用于更新逻辑
    public static void tick(Level level, BlockPos pos, BlockState state, MyBlockEntity be) {
        if (!level.isClientSide) {
            be.counter++;
            // ... 服务端逻辑,每刻执行
        }
    }

    // 存取数据以持久化
    @Override
    public void load(CompoundTag tag) {
        super.load(tag);
        counter = tag.getInt("Counter");
    }
    @Override
    protected void saveAdditional(CompoundTag tag) {
        tag.putInt("Counter", counter);
    }

    // 如果需要 GUI,MenuProvider 接口方法:
    @Override
    public Component getDisplayName() {
        return Component.literal("My Block");  // GUI 显示名称
    }
    @Override
    public AbstractContainerMenu createMenu(int id, Inventory playerInv, Player player) {
        // 返回对应的 Container(Menu) 实例,此处留空,GUI章节详细介绍
        return new MyMenu(id, playerInv, this);
    }
}

上述 MyBlockEntity 保存了一个计数器示例数据,并每刻递增。它还实现了 MenuProvider 接口以提供打开 GUI 所需的信息(见下一节)。要使 tick 方法生效,需要在方块类中覆盖 Block#tick 相关方法设置 Ticker。例如在注册方块时可以指定:

// 在方块类中(假设继承自 BaseEntityBlock)
@Override
public <T extends BlockEntity> BlockEntityTicker<T> getTicker(Level level, BlockState state, BlockEntityType<T> type) {
    return type == ModBlockEntities.MY_BLOCK_ENTITY.get() && !level.isClientSide
           ? (lvl, pos, st, be) -> MyBlockEntity.tick(lvl, pos, st, (MyBlockEntity) be)
           : null;
}

这样游戏每 tick 就会调用我们定义的 MyBlockEntity.tick 方法(仅在服务端执行)。

BlockEntity 的数据同步: 若方块实体包含需要同步到客户端的数据(比如 GUI 显示),有几种方式:

  • 随区块加载同步: 实现 BlockEntity#getUpdateTag() 和 IForgeBlockEntity#handleUpdateTag,Minecraft 会在客户端加载区块时自动用 NBT 同步数据。

  • 随方块更新同步: 实现 BlockEntity#getUpdatePacket() 返回 ClientboundBlockEntityDataPacket,并在服务器改变数据后调用 Level#sendBlockUpdated(...) 通知客户端更新。

  • 使用自定义网络包同步: 通过我们自己定义的网络消息发送数据(参见后续“网络通信”章节)。这种方式最灵活高效,但也最复杂,一般在需要频繁、选择性同步时使用。

资源文件: 若 BlockEntity 需要渲染特殊模型(如带动态渲染的方块),还需编写对应的 BlockEntityRenderer 类并在客户端注册。这部分属于高级内容,这里暂不展开。

至此,我们完成了新物品、方块及方块实体的注册流程。你可以尝试在游戏中放置你的自定义方块、查看其掉落物品和属性。在创建好游戏内容后,下一步就是处理游戏事件和模组生命周期,以定制行为。

4. 事件与生命周期

Minecraft 提供了丰富的事件机制来让模组拦截或注入游戏逻辑。例如玩家互动、方块破坏、实体生成等各种行为都会触发事件。理解并正确使用事件系统,可以让你的模组在适当时机执行代码。本节还将介绍模组加载的生命周期事件,以及如何在合适的阶段进行初始化工作。

4.1 事件系统概述

事件总线 (Event Bus): NeoForge 中,事件通过事件总线发布,不同类别的事件位于不同的总线。例如,游戏事件总线 (NeoForge.EVENT_BUS) 上发布游戏运行时的各种事件(玩家、世界、渲染等),而模组事件总线(每个 Mod 一个)则发布模组加载阶段的事件。大多数游戏事件都在主线程顺序执行,而模组事件常在初始化时并行执行以加快加载速度。开发者可以将自己的事件处理方法订阅到相应总线上,当事件发生时,Forge 会调用这些方法。

订阅事件的方法: 有两种常见方式:

  • 使用 IEventBus.addListener: 直接将方法引用注册到事件总线。例如在主 Mod 类构造中:

    NeoForge.EVENT_BUS.addListener(YourMod::onPlayerJump);

    这会将 onPlayerJump 方法注册为监听所有 LivingJumpEvent(假设方法签名匹配)。方法必须是 static 或者函数引用形式,参数为事件类型。

  • 使用 @SubscribeEvent 注解: 先定义一个专门的事件处理类(或主类),在方法上标注 @SubscribeEvent,并确保该类注册到事件总线。例如:

    public class CommonEvents {
        @SubscribeEvent
        public static void onPlayerLoggedIn(PlayerEvent.PlayerLoggedInEvent event) {
            Player player = event.getEntity();
            player.sendSystemMessage(Component.literal("欢迎, " + player.getName().getString()));
        }
    }

    然后在主类构造中调用:

    NeoForge.EVENT_BUS.register(CommonEvents.class);

    或使用 @Mod.EventBusSubscriber 注解让类自动注册。注意: 如果用 @EventBusSubscriber,不注册参数则默认订阅 游戏总线 (Bus.GAME)。可通过注解参数指定 bus = Bus.MOD 来订阅模组总线事件,以及用 Dist参数限定只在客户端或服务端加载。例如:

    @Mod.EventBusSubscriber(modid="yourmodid", bus=Bus.MOD)
    public class ModLifecycleEvents { ... }

线程与并发: 需要注意,不同事件可能在不同线程触发。如世界生成类事件可能在IO线程。多数游戏事件在主线程,同步执行。模组的生命周期事件很多被设计为并行执行(ParallelDispatch)。如果你需要在这些并行事件里执行主线程任务,可以使用 event.enqueueWork(Runnable) 将任务切换到主线程排队执行。

4.2 模组生命周期事件

模组在加载过程中会按顺序触发一系列生命周期事件(Lifecycle Events)。了解它们可以帮助我们在正确的时间做对应初始化:

  • 模组构造函数调用: 这是最早阶段,@Mod 类实例化时执行。在这里通常绑定注册内容和注册事件监听器。如前述,我们在构造中注册 DeferredRegister 和添加监听 commonSetup/clientSetup 等。

  • 注册阶段 (RegisterEvent): 紧接着,NeoForge 会依次发布各种注册事件,用于注册新的游戏内容。由于我们使用 DeferredRegister,这部分框架已代劳,我们无需手动处理。但需要注意,此阶段 registries 处于未冻结状态,可安全注册物品、方块等。DeferredRegister 会在内部捕获这些事件并完成注册。

  • FMLCommonSetupEvent: 所有内容注册完毕后,触发通用初始化事件。适合在这里执行与游戏内容初始化相关的操作,例如网络消息通道的注册、其它 mod 互操作初始化等。

  • FMLClientSetupEvent / FMLDedicatedServerSetupEvent: 根据物理运行环境(客户端或服务器)触发客户端或服务端专属的初始化事件。例如我们通常在 FMLClientSetupEvent 中注册实体渲染器、屏幕 (GUI) 工厂等客户端特有内容。通过在主类构造中使用 modEventBus.addListener(...) 注册对应方法即可捕获这些事件。

  • InterModEnqueueEvent & InterModProcessEvent: 这两个事件用于模组间通讯 (Inter Mod Communication, IMC)。你可以在前者发送 IMC 消息(例如 InterModComms.sendTo("othermod",key,data)),在后者接收处理其他 mod 发来的 IMC。IMC 常用于可选的跨 Mod 协作。

  • FMLLoadCompleteEvent: 所有模组加载完毕事件。一般很少用到,仅在需要所有初始化结束后执行动作时使用。

以上事件大多在 模组事件总线 上发布,因此你的监听器需要注册到 modEventBus。例如:

modEventBus.addListener(this::onCommonSetup);
modEventBus.addListener(ModLifecycleEvents::onInterMod);

或用 @EventBusSubscriber(bus=Bus.MOD) 的静态订阅方式。

4.3 游戏运行时事件

除了加载阶段,Minecraft 在游戏运行过程中有许多游戏事件可以利用,让模组参与游戏行为:

  • 世界事件: 如方块掉落物生成 (BlockEvent.BreakEvent)、生物生成 (LivingSpawnEvent)、天气变化、世界保存等。你可以拦截这些事件改变默认行为。例如拦截 BlockEvent.BreakEvent,可以修改掉落或取消破坏。

  • 玩家事件: 如玩家登录登出 (PlayerEvent.PlayerLoggedInEvent / PlayerLoggedOutEvent)、拾取物品、死亡、克隆(从维度传送)等。利用登录事件可以给予新玩家奖励,利用死亡事件可以取消经验掉落等等。

  • 输入和交互事件: Forge 提供 InputEvent 监听键鼠输入,PlayerInteractEvent 系列监听玩家对方块/空气/实体的点击。比如可以监听玩家右键某个你添加的物品时,执行特殊动作。

  • 渲染事件(客户端): 如 RenderGameOverlayEventModelRegistryEvent 等,可用于添加HUD元素或注册自定义模型。

  • Tick事件: TickEvent 分为世界 tick、玩家 tick、服务器 tick 等,可以用来执行周期性任务。例如每隔一定 tick 检查某状态等。

示例: 给玩家每次跳跃时回血半颗心:

@SubscribeEvent
public static void onLivingJump(LivingEvent.LivingJumpEvent event) {
    LivingEntity entity = event.getEntity();
    if (!entity.level().isClientSide() && entity instanceof Player) {
        entity.heal(1.0F);
    }
}

这个事件会在生物跳跃后触发,我们检查确保在服务端执行并且对象是玩家,然后调用 heal(1.0F) 恢复生命。

线程安全与 Side 注意: 很多游戏事件在客户端和服务器都会触发(Logical Side)。要区分执行,可以检查 level.isClientSide() 或使用 FMLEnvironment.dist 判断当前环境。另外,不要在服务器端调用客户端专有的方法(如播放粒子、打开 GUI 等),反之亦然。如果需要跨端互动,应通过网络消息通知另一端执行(参见后续网络部分)。

创造模式物品栏 (Creative Tab) 更新: 前面注册物品时提到,新物品默认不会自动加入创造模式物品栏。在 1.19+ 的新系统中,创造标签本身也是可注册的数据,物品的归类通过事件来追加。NeoForge 提供了 BuildCreativeModeTabContentsEvent 事件,可用于将物品加入某个已有分类。示例:

@SubscribeEvent
public static void onBuildCreativeTab(BuildCreativeModeTabContentsEvent event) {
    if (event.getTabKey() == CreativeModeTabs.BUILDING_BLOCKS) {
        event.accept(ModBlocks.MY_BLOCK.get().asItem()); // 将自定义方块物品加入建筑方块标签
    }
}

将此事件处理方法注册到游戏总线(客户端执行),即可在加载创造物品栏时把我们的物品添加进去。你也可以选择注册自定义的 CreativeModeTab(和注册物品类似)来创建自己的标签分类。

通过事件系统,你可以高度定制模组的行为,让游戏在满足特定条件时执行你的代码逻辑。在使用事件时,务必参考官方文档或社区 Wiki 获取事件的具体触发时机和功能,合理使用可以避免冲突和性能问题。

5. 自定义 GUI、菜单和容器

许多模组需要为玩家提供交互界面(GUI),例如容器式方块(箱子、工作台)或自定义机械的界面。本节将讲解如何创建自定义 GUI,包括服务端的 菜单容器 (Menu/Container) 和客户端的 屏幕 (Screen),以及二者如何通信。

5.1 GUI 基本原理

Minecraft GUI 通常由两部分组成:

  • 服务端容器 (Container / Menu): 继承自 AbstractContainerMenu,包含槽位 (Slot) 信息和与玩家背包交互的逻辑,在服务器端维护真实物品状态。

  • 客户端界面 (Screen): 继承自 AbstractContainerScreen<T>,渲染界面元素(物品槽、按钮、文本)并处理玩家输入,在客户端显示。

当玩家打开一个带容器的 GUI 时,客户端向服务器请求开启,服务器创建 Menu 对象并通过网络将必要信息发送给客户端,客户端据此创建对应的 Screen。玩家在 Screen 上的操作(点击槽位、按钮)通过网络发送到服务器,由 Menu 处理后反馈结果,保证客户端显示与服务器状态同步。

5.2 注册菜单类型 (MenuType)

Forge/NeoForge 要求我们为每一种容器 GUI 注册一个 MenuType 对象,它类似于容器的工厂。可在 ModMenus 类中:

public static final DeferredRegister<MenuType<?>> MENUS =
    DeferredRegister.create(Registries.MENU, YourMod.MOD_ID);

// 注册菜单类型
public static final Supplier<MenuType<MyMenu>> MY_MENU_TYPE = MENUS.register("my_menu", () ->
    new MenuType<>(MyMenu::new, FeatureFlags.DEFAULT_FLAGS)
);

这里 MyMenu 是自定义的容器类(继承 AbstractContainerMenu)。MenuType 构造需要提供一个工厂(MenuSupplier)和 FeatureFlag(一般用 FeatureFlags.DEFAULT_FLAGS 表示默认特性)。上述注册将生成 yourmod:my_menu 类型。完成注册后,别忘了在主类初始化时 ModMenus.MENUS.register(modEventBus)

Menu 类编写: 例如:

public class MyMenu extends AbstractContainerMenu {
    private final ContainerLevelAccess access;
    private final MyBlockEntity blockEntity;

    // 服务端调用的构造器
    public MyMenu(int id, Inventory playerInv, MyBlockEntity be) {
        super(ModMenus.MY_MENU_TYPE.get(), id);
        this.blockEntity = be;
        this.access = ContainerLevelAccess.create(be.getLevel(), be.getBlockPos());
        // 添加槽位(例如方块有 9 个内部槽)
        for (int i = 0; i < 9; ++i) {
            this.addSlot(new Slot(be.getInventory(), i, 8 + i*18, 18));
        }
        // 添加玩家背包槽位
        for (int r = 0; r < 3; ++r) {
            for (int c = 0; c < 9; ++c) {
                this.addSlot(new Slot(playerInv, c + r*9 + 9, 8 + c*18, 50 + r*18));
            }
        }
        // 玩家快捷栏槽位
        for (int c = 0; c < 9; ++c) {
            this.addSlot(new Slot(playerInv, c, 8 + c*18, 108));
        }
    }

    // 客户端调用的构造器,用于通过网络数据创建临时容器
    public MyMenu(int id, Inventory playerInv, FriendlyByteBuf buf) {
        this(id, playerInv, (MyBlockEntity) playerInv.player.level().getBlockEntity(buf.readBlockPos()));
    }

    @Override
    public boolean stillValid(Player player) {
        // 控制 GUI 打开时玩家是否能保持使用,一般检查方块距离等
        return stillValid(access, player, ModBlocks.MY_BLOCK.get());
    }
}

这里我们定义了两个构造器:服务端实际调用带 MyBlockEntity 的构造,将真实数据注入;以及客户端通过 FriendlyByteBuf 调用的构造,从网络数据中解析出方块位置并获取方块实体(或者构造临时的数据容器)。我们在构造中使用 addSlot 添加槽位:

  • 假设 MyBlockEntity 持有一个实现了 Container 接口的物品库存(可以继承 ItemStackHandler 用于简化管理),将其槽位加入 GUI。

  • 添加玩家自身的背包和快捷栏槽位,使玩家物品栏也显示在界面中(以上计算 X, Y 坐标只是布局示例,可根据美术需要调整)。

stillValid 用于控制当玩家距离方块过远时自动关闭界面,这里调用了 Forge 提供的 stillValid(ContainerLevelAccess, Player, Block) 简化方法。

5.3 打开 GUI 界面

触发打开: 常见地,当玩家与特定方块交互时打开 GUI。实现上,可以在方块类中覆盖 use 方法:

@Override
public InteractionResult use(BlockState state, Level level, BlockPos pos, Player player, InteractionHand hand, BlockHitResult hit) {
    if (!level.isClientSide) {
        BlockEntity be = level.getBlockEntity(pos);
        if (be instanceof MyBlockEntity) {
            // 使用 ServerPlayer 的 openMenu 方法打开界面
            ServerPlayer serverPlayer = (ServerPlayer) player;
            serverPlayer.openMenu((MyBlockEntity) be, pos -> {
                // 将方块位置写入数据包,以便客户端知道哪个方块实体
                pos.writeBlockPos(pos);
            });
        }
    }
    return InteractionResult.sidedSuccess(level.isClientSide);
}

在 NeoForge 20.4.70+ 之后,原先的 NetworkHooks.openScreen(...) 方法已被弃用,取而代之的是 ServerPlayer.openMenu 接口,它接收 MenuProvider 和一个数据写入器。如上,我们将 BlockEntity 自身作为 MenuProvider(因为之前实现了 MenuProvider 接口),NeoForge 会调用其中的 createMenu 来实例化 MyMenu。同时通过第二个参数提供一个函数,将方块位置信息写入 FriendlyByteBuf,这个缓冲会发送给客户端,用于调用我们定义的客户端构造器。

调用 openMenu 后,NeoForge 将自动完成以下过程:创建 MyMenu 实例、发送 ClientboundOpenScreenPacket 给客户端,客户端收到后创建对应的 Screen 实例并显示。因此,我们需要在客户端注册 Screen 工厂

注册 Screen: 可以在客户端初始化时调用:

MenuScreens.register(ModMenus.MY_MENU_TYPE.get(), MyScreen::new);

此方法将 MyScreen(我们自定义的 Screen 类)的构造函数作为工厂注册关联到 MY_MENU_TYPE。通常把这行放在 FMLClientSetupEvent 中执行,以确保只在客户端注册。

5.4 编写 Screen 类

Screen 类需要扩展 AbstractContainerScreen<MyMenu> 并实现绘制逻辑。简单示例:

public class MyScreen extends AbstractContainerScreen<MyMenu> {
    private static final ResourceLocation BACKGROUND_TEXTURE = new ResourceLocation("yourmod", "textures/gui/my_screen.png");

    public MyScreen(MyMenu menu, Inventory playerInv, Component title) {
        super(menu, playerInv, title);
        this.imageWidth = 176;
        this.imageHeight = 166;
    }

    @Override
    protected void renderBg(GuiGraphics gfx, float partialTicks, int mouseX, int mouseY) {
        // 绘制背景
        RenderSystem.setShaderTexture(0, BACKGROUND_TEXTURE);
        gfx.blit(BACKGROUND_TEXTURE, this.leftPos, this.topPos, 0, 0, this.imageWidth, this.imageHeight);
    }

    @Override
    protected void renderLabels(GuiGraphics gfx, int mouseX, int mouseY) {
        // 绘制标题和玩家物品栏标签
        gfx.drawString(this.font, this.title, 8, 6, 0x404040, false);
        gfx.drawString(this.font, playerInventoryTitle, 8, this.imageHeight - 94, 0x404040, false);
    }
}

在 renderBg 中,我们绘制一个自定义背景纹理(需要在 textures/gui/my_screen.png 提供一个GUI底图),并绘制物品槽等。在 renderLabels 绘制GUI上的文字。GuiGraphics 是 Forge 提供的绘图工具类,封装了大部分绘制方法。

实际应用中,你可以添加按钮控件(通过 AbstractContainerScreen.addRenderableWidget),并在 Screen 类中处理其点击事件。若需要与服务器交互(比如点击按钮让服务器执行某操作),应通过发送自定义网络包通知服务端,这将在下一章节讨论。

完成以上步骤后,编译并启动游戏,右键点击自定义方块即可弹出我们设计的 GUI 界面,玩家可以在其中与物品槽交互。确保服务器逻辑(如物品合成、烧炼等)都在 MyMenu/MyBlockEntity 上处理,而客户端 Screen 仅用于渲染显示。

6. 自定义世界生成:结构、植被等

模组往往会往世界中加入新的地形和结构元素,例如新的矿脉、植物、建筑结构等。Minecraft 1.18+ 引入了基于数据驱动的世界生成系统,大部分世界生成内容可以通过 数据包 (Datapack) 形式添加,而无需在代码中硬编码。本节将介绍如何添加自定义的世界生成要素,包括矿石、植物和结构等。

6.1 世界生成的基本概念

Minecraft 的世界生成由一系列 ConfiguredFeature 和 PlacedFeature 组成:前者定义了要生成的基本元素(如一棵树由树干和树叶组成的结构),后者将其放置到世界某处并可附加规则(如高度范围、频次)。在 1.19.3+ 中,还引入了 Biome Modifier (生物群系修改器) 系统,通过数据驱动方式批量地向指定的群系添加或移除特定特征。

优先采用数据包方式: Forge/NeoForge 提供了数据驱动的途径,让你无需写代码即可注入世界生成。将相应的 JSON 文件放入模组 Jar 的 data/<modid>/worldgen 和 data/<modid>/neoforge/biome_modifier 目录下即可。这样做的好处是兼容性高、直观可调,且允许配置在服务器/Modpack 层面覆盖。

6.2 添加矿石和植被

以添加一种新矿石为例,说明所需的几个数据文件:

  1. Configured Feature: 定义矿脉形状。例如创建文件:
    data/yourmod/worldgen/configured_feature/my_ore.json

    {
      "type": "minecraft:ore",
      "config": {
        "targets": [
          {
            "target": { "predicate_type": "minecraft:block_match", "block": "minecraft:stone" },
            "state": { "Name": "yourmod:my_ore_block" }
          }
        ],
        "size": 9,
        "discard_chance_on_air_exposure": 0.1
      }
    }

    上述 JSON 配置一个矿脉:在石头中替换为 yourmod:my_ore_block,每团最多9个方块,暴露在空气时10%几率被丢弃(模拟矿脉露天减少)。

  2. Placed Feature: 决定矿脉在世界生成的频次和高度等:
    新建 data/yourmod/worldgen/placed_feature/my_ore_placed.json

    {
      "feature": "yourmod:my_ore", 
      "placement": [
        { "type": "minecraft:count", "count": 20 }, 
        { "type": "minecraft:in_square" },
        { "type": "minecraft:height_range", "height": { "type": "minecraft:uniform", "min_inclusive": 0, "max_inclusive": 64 } },
        { "type": "minecraft:biome" }
      ]
    }

    这表示在一个区块内尝试生成20次矿脉,均匀分布,高度在0-64之间。

  3. Biome Modifier: 将上述 Placed Feature 添加到特定生物群系。新建:
    data/yourmod/neoforge/biome_modifier/add_my_ore.json

    {
      "type": "forge:add_features",
      "biomes": "#minecraft:is_overworld",
      "features": "yourmod:my_ore_placed",
      "step": "underground_ores"
    }

    这利用 Forge 内建的 AddFeatures 修改器,指定将 yourmod:my_ore_placed 矿脉特征添加到所有主世界群系(标签 #minecraft:is_overworld)的 “地下矿石 (underground_ores)” 步骤中。step 对应 Minecraft 内部的生成阶段,underground_ores 是用于生成矿石的阶段。

将上述 JSON 文件打包进模组后,启动游戏,新矿脉就会在世界生成!可以创建一个新的超平坦世界或使用命令 /placefeature yourmod:my_ore_placed 来测试生成效果。

类似地,添加新的花朵或植物也可以按照:

  • 定义一个 ConfiguredFeature,类型为 minecraft:random_patch 或其他植物生成类型,指定目标方块(如草方块上生成)、数量和分布。

  • 定义 PlacedFeature 来设置群落密度、高度等。

  • 用 Biome Modifier 将其添加到相应的群系(比如只添加到平原或森林,可用生物群系标签如 #minecraft:is_forest)。

提示: 可以参考 Minecraft 原版的数据文件。NeoForge 开发环境会包含游戏内置的资源和数据,使用 IDEA 打开 External Libraries,可在 ng_dummy...:client-extra 查看原版 worldgen JSON 配置。照葫芦画瓢修改适合自己模组需求的配置,是快速上手的好方法。

6.3 添加自定义结构

结构 (Structure): 相比矿脉和植物,结构往往规模更大(如房屋、遗迹),生成逻辑也更复杂。Minecraft 提供了 “结构要素 (structure feature)” 系统,可通过数据或者代码定义。考虑到复杂性,这里简要介绍两种途径:

  • 数据驱动结构 (Jigsaw 结构): 通过在数据包定义 worldgen/structure JSON 和配套的结构模板(使用结构方块导出的 .nbt 文件)来添加。例如可以设计一个村庄结构,由数个模板拼接而成,然后在 JSON 中定义拼接规则。该方法无需写代码,但对素材制作和理解原版结构生成要求较高。

  • 代码注册结构: Forge 允许直接注册自定义 Structure 类。你需要扩展 Structure 类实现生成算法,然后像注册方块一样注册一个 StructureType,并结合 AddFeaturesBiomeModifier 将其加入世界。不过1.19+对这方面进行了精简,很多时候完全可以用预置 feature 来实现,不需要自己写结构算法。

对于初学者而言,建议从数据驱动入手,尝试制作小型结构。例如,用结构方块在游戏内保存一个小屋为 yourmod:house.nbt,然后:

  • 编写 configured_feature/house.json,类型为 minecraft:random_structure(或参考原版村庄结构的写法)。

  • 编写 placed_feature/house_placed.json(或直接在 Biome Modifier 用 add_structure 类型)。

  • 编写 biome_modifier/add_house.json,将结构添加到某些群系的 surface_structures 阶段。

由于结构添加较为繁琐,建议参考 Minecraft Wiki 或 Forge 社区教程,它们有详细示例。这里提供框架认识即可。

6.4 世界生成高级话题

数据生成 (Datagen): 手工编写大量 JSON 文件既枯燥又易出错。Gradle MDK 通常包含数据生成器的支持。你可以通过编写 datagen 脚本,用 Java 代码生成上述 JSON,大幅减少人工错误。Forge 提供 GatherDataEvent 事件,可注册自定义 DataProvider。例如使用 BiomeModifierProvider 或 ConfiguredFeatureProvider 自动输出配置。当项目规模变大时,datagen 是非常值得投入学习的方向。

条件生成和其他控制: 你可以利用 Biome Modifier 的其他类型进行更多控制:

  • 添加/移除生物群系的特征:除了 add_features 还有 remove_features,可用于禁用某些群系的原版生成(比如阻止某生物群系下雨或生成水坑)。

  • 添加/移除生物刷怪:通过 forge:add_spawnsremove_spawns 可以为特定群系添加或移除生物刷怪。比如你的模组新增一个生物,可通过 JSON 添加到怪物或动物列表中,而无需编码。

  • 自定义 Biome Modifier: Forge 也允许你注册新的 biome modifier 类型,用代码实现更复杂的筛选或操作,然后数据驱动调用。一般只有在内建功能不足时才需要这样做。

世界生成对玩法影响重大,因此在修改时注意平衡和性能。用数据定义可以方便地调整出现频率和条件,鼓励多用数据、少用硬编码。如果必须在代码中调整,可以监听 BiomeLoadingEvent(1.16-1.18 常用)或使用 StructurePlacement API。总之,充分利用 Forge 提供的工具,可以让你的模组世界元素更丰富。

7. 数据驱动内容:数据包、战利品表、合成配方

现代模组开发大量内容是通过 数据包 (Data Pack) 形式注入的。前一节介绍的世界生成属于数据包内容,除此之外还有合成配方、熔炉配方、战利品表、功能标签(Tag)、进度(Advancement) 等。合理地编写这些数据可以大幅减少代码量,也使模组更易于维护。本节将介绍一些常用的数据驱动内容及其添加方式。

7.1 合成配方 (Recipes)

新增配方: 在 data/<modid>/recipes/ 下添加 JSON 文件即可引入合成配方。Minecraft 有工作台合成 (shaped/shapeless)熔炉/高炉/烟熏炉 (smelting/blasting/smoking)石切台 (stonecutting)酿造锅 (brewing) 等多种配方类型,格式略有区别。这里以一个工作台配方为例:

创建 data/yourmod/recipes/my_item_from_diamonds.json

{
  "type": "minecraft:crafting_shaped",
  "pattern": [
    " D ",
    "D D",
    " D "
  ],
  "key": {
    "D": { "item": "minecraft:diamond" }
  },
  "result": { "item": "yourmod:my_item", "count": 1 }
}

这个配方表示:以钻石 “D” 摆出一个菱形(四角和中心放钻石),即可合成1个 yourmod:my_itempattern 数组表示 3x3 工作台网格的布局,“ ”表示空格。key 定义每个字母对应的材料。

将此 JSON 放入模组资源后,游戏加载时会自动识别配方。你可以在游戏中打开合成书,或者使用 /recipe give @p yourmod:my_item_from_diamonds 解锁来测试。

无序合成 (shapeless): 若配方对摆放形状无要求,可使用 "type": "minecraft:crafting_shapeless",并将原料放入 "ingredients": [] 列表。例如:

{
  "type": "minecraft:crafting_shapeless",
  "ingredients": [
    { "item": "minecraft:stick" },
    { "item": "minecraft:feather" }
  ],
  "result": { "item": "yourmod:magic_wand" }
}

表示将木棍+羽毛放入任何两个格子即可合成魔法棒。

其他配方: 熔炉/高炉/营火等配方文件放在同一路径下,类型分别为 "minecraft:smelting""minecraft:blasting""minecraft:campfire_cooking" 等,需要指定 "cookingtime""experience" 等属性。可以参考现有原版配方示例修改。

高级用法:

  • 条件配方:可以用 "conditions" 字段使配方有条件地生效(例如某模组存在时,或某游戏规则开启时)。

  • 标签 (Tag) 作为材料"ingredient": { "tag": "forge:ingots/copper" } 表示任意属于 forge:ingots/copper 标签的物品均可作为材料。这让不同模组的材料可以通用配方。

  • 返回容器"crafting_remainder"(在物品属性中)和配方 JSON 的特殊设置可以使物品在合成后不消失而返回(如桶在合成蛋糕后返还空桶)。

7.2 战利品表 (Loot Tables)

方块掉落: 我们添加的新方块通常需要自定义掉落物,否则默认会掉落自身(无丝触时)或什么也不掉。通过 战利品表 可以精确控制掉落。创建路径 data/yourmod/loot_tables/blocks/my_block.json

{
  "type": "minecraft:block",
  "pools": [
    {
      "rolls": 1,
      "entries": [
        { "type": "minecraft:item", "name": "yourmod:my_block" }
      ],
      "conditions": [
        { "condition": "minecraft:survives_explosion" }
      ]
    }
  ]
}

这份战利品表定义:每当 yourmod:my_block 被破坏时,掉落1次yourmod:my_block物品条目,并受survives_explosion条件约束(意味着若因爆炸破坏则有几率掉落为空,模拟原版 TNT 破坏掉落的机制)。如果希望方块掉落其他物品,可以把 "name" 换成想掉落的物品 ID。如果想实现概率掉落多个,不同条件,则可以增加 pools 或 entries 并设置 "rolls" 和条件组合。

生物掉落: 类似地,将 JSON 放在 loot_tables/entities/<entity>.json 可以修改生物的掉落。同理还有 loot_tables/chests/... 可定义宝箱战利品内容。原版的战利品表可在 Minecraft Wiki 查询作为模板。

钓鱼/垃圾掉落等 也可通过战利品表扩展,例如放在 loot_tables/gameplay/fishing.json 的数据可以添加自定义钓鱼战利品。

验证: 战利品表调试可以使用 /loot spawn 命令。比如:/loot spawn ~ ~ ~ loot yourmod:blocks/my_block 可以在当前位置直接生成按照战利品表模拟的掉落物,以检查是否如预期。

7.3 标签 (Tags)

标签的作用: Tag 是一种为方块、物品等分配“组”的机制,许多原版机制和模组兼容都会利用标签判断。例如 #minecraft:planks 包含了所有木板方块,某些合成配方或燃料列表用的是标签而不是具体物品。这保证了只要模组正确地把自己物品加入相关标签,其他模组或游戏机制就能自动识别互动。

创建标签: 文件位于 data/<namespace>/tags/<registry>/<tagname>.json。如我们希望将自己的铜锭加入 Forge 通用的铜锭标签,让其它模组识别它也是铜锭,可以:

  • 创建文件:data/forge/tags/items/ingots/copper.json

    {
      "replace": false,
      "values": [ "yourmod:copper_ingot" ]
    }

    这会在加载时将 yourmod:copper_ingot 添加到 forge:ingots/copper 标签中(replace=false 表示不替换已有定义,增量加入)。如果没有其它模组提供这个标签文件,你也可以在自己命名空间 yourmod/items/ingots/copper.json 定义,再在 mods.toml 声明依赖顺序,但通常直接放入 forge 命名空间即可,Forge 会合并所有模组对同一标签的定义。

常用标签实例:

  • forge:ores/* 和 forge:ingots/* 等,用于矿石和锭类物品的统一处理(兼容矿辞)。

  • minecraft:stone_crafting_materialsminecraft:logs 等,用于原版合成或燃料判定。

  • forge:tools/*(例如斧头、镐),模组可以标记某物品属于某工具类别。

  • forge:armorforge:armor/helmets 等,统一不同来源装备的标识。

添加标签不会直接改变游戏行为,但能让模组之间更好地协作。例如前述铜锭加入标签后,任何以 #forge:ingots/copper 为材料的合成都能使用你的铜锭,这对于玩家来说提升了模组兼容性体验。

7.4 进度 (Advancements) 与功能文件

进度系统: 如果你希望当玩家完成某些条件时解锁一条提示(类似原版成就),可以在 data/<modid>/advancements/ 下添加 JSON 定义自定义进度。例如 data/yourmod/advancements/root.json 定义模组的进度树根节点,data/yourmod/advancements/kill_mob.json 定义击败自定义生物解锁的进度等。进度 JSON 包含触发条件(target)、展示信息(display)等。编写进度既可以给玩家目标感,也可以用来指引玩家模组内容。

功能 (Functions) 文件: 放在 data/<modid>/functions/ 下的 .mcfunction 脚本可以由命令方块或事件触发执行多条游戏内指令。这通常在冒险模组或预设地图中有用。对于一般模组,较少需要自定义函数文件,但如果有特定需求,这也是数据驱动的一部分。

总而言之,模组的很多玩法内容都可以通过数据文件实现,减少繁琐的编程。把逻辑交给 Minecraft 原生系统处理,也往往更稳定和兼容。例如,不同模组都添加了新矿石,如果都正确使用了 forge:ores 标签,那么无论是世界矿字典兼容、粉碎机加工还是高炉提炼,都可以通过标签自动兼容,而无需写专门的适配代码。

务必注意 JSON 文件格式是否严格正确,IDEA 等编辑器会提供高亮和格式校验。出现格式问题游戏会在启动时报告相应数据包错误。良好地组织 data 和 assets 目录,可以让你的模组更易维护,也方便他人查阅和基岩版迁移等。

8. 网络通信 (Packet) 与客户端交互

在客户端-服务端分离的架构下,模组经常需要发送自定义数据包,以在两端同步信息或触发行为。例如:当玩家在客户端点击自定义按钮,需要通知服务端执行某操作;或者方块实体的数据更新后,需要推送给附近客户端更新界面显示。本节讲解 Forge/NeoForge 的网络通信机制,以及如何定义和使用自定义数据包。

8.1 Forge 的网络系统概览

过去 Forge 使用 SimpleChannel 简化网络通信,但在 NeoForge 20.4.70 之后网络系统进行了重构。重构后的系统基于 Custom Payload 原理,每个模组可以注册自己的“网络负载”类型及处理器,然后通过发送封包达成通信。简单来说:

  • 你需要注册自己的数据包类型及其处理函数。

  • 客户端服务端在需要时发送该数据包到对端。

  • 对端收到后,调用你注册的处理函数执行相应逻辑。

8.2 定义和注册自定义数据包

1. 定义消息类: 你可以用一个普通的 Java 类或记录 (record) 来表示要发送的数据。例如:

public record ToggleAbilityMessage(boolean enable) {
    // 编码:将数据写入 FriendlyByteBuf
    public void encode(FriendlyByteBuf buf) {
        buf.writeBoolean(enable);
    }
    // 解码:从 FriendlyByteBuf 读取数据
    public static ToggleAbilityMessage decode(FriendlyByteBuf buf) {
        return new ToggleAbilityMessage(buf.readBoolean());
    }
    // 处理:收到消息时的逻辑(区分客户端/服务端环境)
    public void handle(Supplier<NetworkEvent.Context> ctx) {
        NetworkEvent.Context context = ctx.get();
        if (context.getDirection().getReceptionSide().isServer()) {
            ServerPlayer sender = context.getSender();
            if (sender != null) {
                sender.getCapability(ModCapabilities.ABILITY).ifPresent(cap -> {
                    cap.setEnabled(enable);
                });
            }
        } else {
            // 客户端接收逻辑(例如更新本地数据)
            ClientAbilityData.setEnabled(enable);
        }
        context.setPacketHandled(true);
    }
}

这段代码展示了传统 Forge SimpleChannel 模式下定义消息的惯用写法:包含 encode、decode 和 handle 静态方法。在 NeoForge 新系统下,概念类似,但注册方式不同。

2. 注册消息信道 (Channel) 和处理器: 在 NeoForge 1.21 中,你不再使用 NetworkRegistry.newSimpleChannel,而是在模组初始化阶段监听一个 RegisterPayloadHandlerEvent。例如,在你的主 Mod 类构造或 FMLCommonSetupEvent 中:

modEventBus.addListener((RegisterPayloadHandlerEvent event) -> {
    IPayloadRegistrar registrar = event.registrar(YourMod.MOD_ID).versioned("1.0.0");
    // 注册双向消息处理
    registrar.registerPlayChannel("toggle_ability", ToggleAbilityMessage::decode, ToggleAbilityMessage::handle);
});

上面逻辑:

  • 通过 event.registrar("yourmodid") 获取一个 Payload 注册器,并设置版本号。“toggle_ability” 是你为该消息取的唯一名称。

  • 调用 registerPlayChannel 注册一个游戏阶段 (Play) 消息,提供了解码函数和处理函数。NeoForge 将自动管理该消息的发送与接收流程。

  • 这样,在客户端和服务端各自都会注册对应的处理器。ToggleAbilityMessage.handle 中会根据当前是在服务端还是客户端执行不同逻辑。

⚠️ 注意: 必须在 RegisterPayloadHandlerEvent 发生时注册消息。错过时机将导致消息类型未注册而无法发送。同时,registerPlayChannel 会为你同时注册客户端->服务端和服务端->客户端的处理器。如果某消息只在一侧接收,也可以使用 registerClientToServer 或 registerServerToClient 等方法分别注册。

8.3 发送网络消息

注册完成后,就可以在需要时发送消息。发送通过 IPacket 接口或者简化的封装实现。例如:

  • 服务端发送给特定玩家:

    ServerPlayer player = ...;
    player.connection.send(new ClientboundCustomPayloadPacket(
        new ResourceLocation("yourmod", "toggle_ability"),
        new FriendlyByteBuf().writeBoolean(true)
    ));

    这种方式直接构造了原版的自定义包进行发送。但因为我们已注册了 toggle_ability 通道和解码器,其底层原理会处理解析为我们的消息对象。这种直接使用 Packet 对象的方法需要自行构造 FriendlyByteBuf内容。

    更简单的,可以使用 Forge 提供的 PacketDistributor 封装:

    ToggleAbilityMessage msg = new ToggleAbilityMessage(true);
    SimpleChannel channel = ...; // 若NeoForge保留SimpleChannel对象的话
    channel.send(PacketDistributor.PLAYER.with(() -> player), msg);

    然而在 NeoForge 移除了 SimpleChannel 后,你可能需要使用 IPayloadRegistrar 提供的发送方法或自行获取 Networking 实例。

  • 客户端发送给服务端:
    客户端可以通过 Minecraft.getInstance().getConnection().send(new ServerboundCustomPayloadPacket(…)) 发送服务端数据包。或者 Forge 可能提供了 Player#openMenu 类似的辅助。

由于 NeoForge 网络改动较新,具体 API 可能持续优化。**建议查看 NeoForge 官方文档的 Networking 部分和 Porting 指南,获取最新范例。**总体来说,理解消息的注册和收发原理后,API 使用仅是细节问题。

8.4 使用网络通信的场景举例

  • GUI 按钮:玩家点击 Screen 上的按钮 ——> Screen 调用发送 new DoSomethingMessage(...) 到服务端 ——> 服务端 handler 收到后修改方块实体状态或触发某事件 ——> 服务端可能再发送 UpdateScreenMessage 返回客户端更新界面显示。

  • 键位绑定:玩家按下键盘快捷键 ——> 客户端监听到后发送 KeyPressMessage 到服务端 ——> 服务端执行对应行为(如玩家冲刺技能)。

  • 数据同步:方块实体某数据变化 ——> 服务端发送 SyncBEDataMessage 带新数据给客户端 ——> 客户端 handler 更新本地缓存并渲染到界面。

  • 跨模组通信:甚至你的模组可以定义协议,与另一模组通过交换网络消息实现互操作(较少见,一般用 IMC 或共享 API 解决)。

当使用网络包时,请牢记安全和性能

  • 不要信任客户端发送的数据,一定在服务端验证。例如不要直接使用客户端发来的实体 ID 执行攻击,需检查该实体是否在合法范围内、玩家是否有权限等。

  • 控制发送频率和数据大小,过多的消息会占用网络带宽,引起延迟或卡顿。尽量合并数据、降低同步频率,例如每秒只同步4次状态而不是20次。

NeoForge 的网络系统让消息的注册和处理更加集中统一,也借鉴了 Fabric 的思想。初学阶段,按照本文提供的思路,可以先使用简单可靠的方式(如 ServerPlayer.openMenu 已经封装了 GUI 打开的网络通信)。当需要自定义消息时,再投入时间研究最新的 NeoForge 网络 API。

9. 高级技巧:Mixins、反射与条件注册

在模组开发逐渐深入时,你可能会遇到一些特殊需求或挑战:想修改原版游戏的底层逻辑、访问私有字段、或者根据环境有选择地启用某些功能。本节介绍几种高级技巧:Mixin反射 和 条件注册,帮助你应对这些情况。但也请谨记,高级技巧有一定风险,应谨慎使用。

9.1 Mixin 注入修改

Mixin 是什么: Mixin 是一种在运行时修改已有类字节码的技术,可以在不直接修改源代码的情况下插入、替换或删除方法逻辑。Fabric Mod Loader 广泛使用 Mixin 实现其补丁功能。在 Forge/NeoForge 环境下,Mixin 也可以使用,尤其当你需要修改原版或第三方 mod 行为而没有官方接口时。

使用 Mixin 的场景: 例如,你想改变玩家在特定情况下的最大生命值,或者修改生物 AI,但 Forge 没有提供对应的事件或 hook,这时 Mixin 可以让你直插相关函数。又或者优化 Mod 性能,需要修改其私有逻辑,也可能用 Mixin。

NeoForge 对 Mixin 的支持: NeoForge 已原生支持加载 Mixin 配置,无需额外的 Gradle 插件配置。你只需在 neoforge.mods.toml 中声明 Mixin 配置文件:

[[mixins]]
config="yourmod.mixins.json"

然后在 src/main/resources/ 新建 yourmod.mixins.json

{
  "required": true,
  "package": "yourmod.mixin",
  "compatibilityLevel": "JAVA_17",
  "mixins": [
    "PlayerMixin"
  ],
  "client": [],
  "injectors": { "defaultRequire": 1 }
}

这个 JSON 指定需要的 Mixin 列表和设置。之后在 yourmod.mixin 包下创建一个 Mixin 类,例如:

@Mixin(Player.class)
public abstract class PlayerMixin {
    @Inject(method = "jumpFromGround", at = @At("HEAD"))
    private void onJump(CallbackInfo ci) {
        if (!this.level().isClientSide) {
            this.heal(1.0F); // 每次起跳服务器给玩家回血,类似之前用事件实现的功能
        }
    }
}

这个 Mixin 会在 Player 类 jumpFromGround 方法执行(HEAD位置)插入我们的代码,实现玩家起跳就回血的效果(与前面事件例子相同效果)。

Mixin 风险与注意:

  • 不正确的 Mixin 可能导致游戏启动崩溃或行为异常,因为它直接修改底层代码。

  • Mixin 名单中 required: true 表示 Mixin 加载失败就中止游戏,可以根据需求设置。

  • 要确保 Mixin 代码仅在需要的平台执行(可通过 @Environment 或拆分客户端/服务端 Mixin 列表)。

  • Mixin 修改其他模组的代码可能违反那些模组的协议,也可能引发冲突,请谨慎处理并取得授权(如果是发布模组)。

9.2 反射和访问私有字段/方法

反射的用途: Java 反射机制允许在运行时检查和调用类的私有成员。在模组开发中,如果需要获取或修改 Minecraft 私有字段(如某管理器列表)而没有公共 API,就可以使用反射。

示例场景: 例如获取 Minecraft 主窗口句柄 (Minecraft.getInstance().window) 这个字段可能是私有的,可以:

Field windowField = Minecraft.class.getDeclaredField("window");
windowField.setAccessible(true);
Window window = (Window) windowField.get(Minecraft.getInstance());

这样获取到了 Window 对象。当然,这示例实际并不需要反射,因为 Minecraft 提供了方法。但类似的技巧可以用于没有公开方法的情况。

反射 vs AccessTransformer: Forge 还有一种 Access Transformer 机制,可以在编译时修改类的访问权限。例如你可以编写一个 .cfg 文件将某字段由 private 改为 public,然后 Forge 构建过程会修改字节码。这需要在 build.gradle 和 neoforge.mods.toml 中登记 AT 文件。相对而言,AT 比反射性能更好、使用更安全(编译期静态修改),但配置起来稍复杂。如果只是少量几个字段,反射也未尝不可。NeoForge MDK 默认包含 AT 支持,可以在需要时考虑使用。

反射性能与兼容: 反射调用相对普通调用要慢一些,但在可控范围(比如初始化时调用一次)并无大碍。更大的问题是名称兼容:MCP/Mojang 映射下,私有字段名称会变化。如果你硬编码了 "field_12345" 这样的 SRG 名称,在将模组换另一版本映射时可能无效。因此推荐使用 MCP/Mojang mappings 中的映射名称。在开发环境,使用字段名字符串即可(如 "fieldName"),构建时 ForgeGradle 会处理映射。但务必测试模组在生产环境下字段是否正确映射。

9.3 条件注册与模组互操作

根据环境注册内容: 有时你的模组内容只在客户端或服务端需要存在,例如只在客户端注册渲染器或键位,这可以通过 Dist 判断或环境注解做到:

  • 使用 @EventBusSubscriber(value=Dist.CLIENT) 标记专门处理客户端事件的类。

  • 在主类中判断 FMLEnvironment.dist 来有选择地注册内容。

  • 或使用 DistExecutor.safeRunWhenOn 在运行时执行某些初始化。

例如:

if (FMLEnvironment.dist == Dist.CLIENT) {
    ItemProperties.register(ModItems.SPECIAL_GUN.get(), new ResourceLocation("aiming"),
        (stack, level, entity, seed) -> entity != null && entity.isUsingItem() ? 1.0F : 0.0F);
}

这段代码确保只有在客户端环境下才注册物品的模型属性Override(服务端拿不到 ItemProperties 类)。

模组依赖和软适配: 在 mods.toml 里可以声明对其他模组的依赖关系:

[[dependencies.yourmod]]
modId="othermod"
mandatory=false
versionRange="[2.0,)" 
ordering="NONE"
side="BOTH"

其中 mandatory=false 标识 “othermod” 是可选依赖。这样,若玩家未安装该 mod,你的模组仍可加载。如果想在检测到该 mod 存在时启用某些功能,可以:

if (ModList.get().isLoaded("othermod")) {
    // 执行与 othermod 相关的注册或集成代码
    OtherModAPI.registerFancyBlock(ModBlocks.MY_BLOCK.get());
}

最好将依赖 mod 的调用用 if 隔离,防止类加载错误。例如上面代码应置于某个事件处理而非类初始化静态区,这样只有真的存在 othermod 时才触发调用,否则 JVM 连 OtherModAPI 类都不会尝试加载,避免 ClassNotFound 异常。

条件特性配置: 另外,有些模组会提供配置文件 (比如通过 ForgeConfigSpec) 让玩家启用/禁用某些模块。如果某模块禁用,可以不注册相关内容。这也算一种条件注册。可通过在注册前读取配置决定是否执行 DeferredRegister.register

9.4 其他高级技巧一瞥

  • 核心修改 CoreMod: Forge 允许编写 CoreMod,通过 ASM Transformer 修改字节码。这比 Mixin 更底层更危险,一般除非万不得已不建议。NeoForge 1.21 对 CoreMod 也有支持,但在大多数情况下 Mixin 已足够且更简单。

  • 安全检查与错误处理: 在高级操作中,加入充分的日志和错误捕获很重要。例如用反射获取字段失败时,打印警告提示玩家版本不兼容等。

  • 性能调优: 若你的模组出现性能瓶颈,可能需要用到工具(如 WarmRoast、Spark)定位,然后通过上述方法(如 Mixin 优化某算法、用事件取代逐 tick 轮询)等方式改进。这已超出本文范围,但值得有意识地编写高效代码。

总之,Mixins、反射这些工具应当被视为最后的手段:当 Forge 提供的常规机制无法满足需求时再考虑使用。它们用不好可能导致和其他模组冲突、难以调试的问题。所以初期开发能不用就尽量不用,等积累经验后再尝试。

10. 模组打包、版本控制与发布

走到这一步,你的模组功能已经开发完成。最后需要将其打包发布,并在后续维护中管理好版本和兼容性。本节将介绍如何构建模组 Jar、管理源码版本,以及发布到常用平台(如 CurseForge)的注意事项。

10.1 模组打包构建

使用 Gradle 打包: 在开发环境中,你始终可以通过运行 gradlew build 来生成发布用的 Jar 文件。构建成功后,Jar 位于项目目录下的 build/libs/ 中。默认情况下,Jar 文件名格式类似 <modid>-<version>.jar,例如 examplemod-1.0.jar,其中版本号和 modid 来自 gradle.properties 中设置的 mod_version 和 mod_id

输出 Jar 内容: 打开的模组 Jar 可以看到包含:

  • META-INF/ 目录:其中有生成的 neoforge.mods.toml(包括你的 mod 元信息)和签名等(如有)。

  • 你的包名对应的 .class 文件,即编译后的模组代码。

  • assets 和 data 资源内容。
    确保这些内容齐全,特别是 mods.toml 很关键——如果缺失,模组将无法被加载。Gradle MDK 模板已经为你配置好了处理步骤,所以一般不需要手动修改构建脚本。

调试 Jar: 可以将生成的 Jar 放入本地 NeoForge 安装的 mods/ 文件夹,启动 Minecraft 验证是否正常加载。注意: 开发环境下由于启用了 debug 特性和加载顺序,某些问题可能隐藏,最好在正式环境(即正常启动的 NeoForge 客户端)测试一次,以避免发布后玩家遇到问题。

10.2 版本控制与多版本兼容

代码版本管理 (Git): 强烈建议将模组源代码托管到 Git 仓库(比如 GitHub、GitLab)。这方便你跟踪每次修改、回滚问题、协同开发和提供源代码给他人参考。GitHub 上还有许多 Actions 可用于自动构建和发布模组。NeoForge 官方文档也建议熟悉 Git 的使用。

多 MC 版本维护: 随着 Minecraft 升级,你可能需要将模组移植到新版本(如未来的 1.22),甚至维护多个分支。如果不同版本改动较大,常见做法是在 Git 使用不同分支管理,如 1.21.x 分支,1.22.x 分支各自开发,必要的 bugfix 互相 cherry-pick。NeoForge 尽量保持 API 稳定,但跨大版本有时仍需调整代码(查看 NeoForge 发布日志获取 Porting 攻略)。尽量避免用一套 Jar 兼容多个 MC 版本,绝大多数情况下,一个 Jar 只对应一个 MC 版本和一个 NeoForge 版本范围。

MOD 版本号和依赖: 采用语义化版本可以帮助玩家和整合包作者了解更新是否兼容。通常:主版本号升级表示可能不兼容变更,次版本号表示新功能保持兼容,补丁号表示修复。你可以在 mods.toml 的 versionRange 字段指定模组对 NeoForge 或其他依赖 mod 的兼容范围。例如:

loader_version_range="[1,)"  # 表示兼容 NeoForge Loader 1.x 及以上
neo_version_range="[21.0.0,21.5.0]"  # 兼容 NeoForge 21.0.0-beta 至 21.5.0

合理设置这些范围有助于在模组加载时检查出不匹配的环境并给出提示,而不是直接崩溃。

兼容性管理: 如果你的模组依赖其他模组或库,也要确保在 mods.toml 声明。例如依赖 JEI 则加入 dependencies 配置,并注明是可选还是必需。当其他模组更新可能破坏兼容时,考虑在更新日志中提示或通过版本范围加以限制。另一个兼容性方面是 客户端-服务端兼容:通常纯客户端模组(如小地图)在服务端不存在也能进入,Forge 默认允许这种情况。而含有游戏逻辑的模组要求服务端和客户端均安装;如果想让模组在服务端缺失时客户端依然能进入(即模组完全客户端可选),需要在 mods.toml 将 side 设置为 CLIENT 对应依赖或采用 @OnlyIn(Dist.CLIENT) 控制行为。

10.3 发布模组及渠道

社区发布平台: 当前主要模组发布平台有 CurseForgeModrinth 等。大多数玩家通过启动器(CurseForge App、FTB App、Prism Launcher 等)下载模组包,所以将作品发布到这些平台能获得最大曝光。

发布前准备:

  • 模组名称和描述: 在平台上清晰介绍模组功能特色,必要时提供多语言(至少英文)。可以将 mods.toml 中的描述扩展形成发布描述。

  • 截图和演示: 上传几张游戏内截图,直观展示模组内容。如果可能,录制GIF或视频效果更佳。

  • 版本号: 确保发布文件名或描述中标明对应的 MC 和 NeoForge 版本。例如 MyMod 1.0.0 (MC 1.21.4, NeoForge 21.5.x)

  • 变更日志: 对于后续更新,提供简明的更新内容列表,让用户了解新版本改了什么(Bug 修复/新特性/平衡调整)。

CurseForge 发布:

  1. 在 CurseForge 网站创建一个新项目,选择分类(Minecraft)和模组类型(一般选 Fabric/Forge,NeoForge 基于 Forge 可以选择 Forge 分类)。

  2. 填写基本信息,通过审核后即可上传文件。上传时需要选择支持的游戏版本(如 “Minecraft 1.21.4”)和 Mod 加载器版本(选 “Forge” 并在描述中注明 NeoForge)。

  3. 上传 Jar 文件,并填写变更日志,标记发布类型(alpha/beta/release)。提交后等待 CF 系统扫描(会检查病毒、是否包含非法内容等),通过后用户即可下载。

  4. 可以为不同 MC 版本的模组文件建立同一个项目的多个文件条目,也可以分开项目(但一般同一模组不同版本放一起方便用户)。

Modrinth 发布: Modrinth 相对简洁:

  1. 创建项目,填写简介和图片。

  2. 上传文件,标注游戏版本和加载器(Modrinth 支持 NeoForge 分类)。提供版本名和说明后发布即可。Modrinth 审核宽松且提供API和Gradle插件方便开发者自动上传。

版权和授权: 确定模组许可协议,并在发布页面和 mods.toml 中标明。常见开源协议有 MIT、LGPL 等,非开源则可写 “All Rights Reserved”。注意不要违反 Minecraft EULA 和上层库的许可。例如不要直接包含 Minecraft 的资源文件用于发布(除了玩家需要的改动部分),不要包含其他闭源 Mod 的代码。模组发布包中允许包含你用到的库 Jar,但 CurseForge 对某些库有特殊规定,通常Gradle会在运行时下载库,不需要你手动包含。

与玩家互动: 发布后,可能会收到玩家的评论反馈、问题报告。及时回应有助于提升模组口碑。可以考虑建立 GitHub Issues 或 Discord 频道收集意见。版本更新时,在说明里致谢发现问题的玩家等也可以增加社区黏性。

版本控制提醒: 当你发布一个版本后,尽量避免对相同版本号的文件再次修改并替换上传(除非是刚发布发现重大问题及时撤回)。版本号递增可以帮助用户和启动器正确识别更新。如果发布后发现漏洞需要紧急修复,就尽快增加补丁号发布新文件而不是替换旧文件。

最后,经过一系列漫长但充实的步骤,你的模组终于可以与全球的 Minecraft 玩家见面了!从环境搭建到代码实现,再到资源制作和最终发布,我们覆盖了 NeoForge 模组开发的主要方面。希望这个教程为你打下坚实基础。在未来开发中,你还会学到更多技巧和知识,不断完善你的模组。祝你的模组开发之旅顺利,创造出令人惊叹的作品!