ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

NeoForge 1.21.1 Mod开发:项目结构与主类全面拆解

NeoForge 1.21.1 Mod开发:项目结构与主类全面拆解 1. 开篇从零搭起NeoForge Mod的骨骼做Minecraft模组开发拿到一个1.21.1 NeoForge的项目模板很多人第一反应就是打开主类开写。但实际落地时你会发现真正卡住新手的往往不是代码本身而是面对一整个陌生的项目结构时无从下手——哪个文件管什么、gradle脚本里到底配了啥、主类为什么要写成这个样子、资源文件放哪里才对。这一篇笔记就从最基础的文件结构和Mod主类讲起帮你把项目的骨架彻底看透。这套内容适合谁如果你正准备入坑1.21.1版本的NeoForge模组开发或者你已经写过几个小Mod但一直凭感觉往里塞文件这篇笔记能帮你把底层逻辑理顺。我用的环境是JDK 21、Gradle 8.x、NeoForge 1.21.1稳定版整个项目从构建到运行都是在这套组合下验证过的。2. 整个项目的设计思路为什么NeoForge要把文件拆得这么细2.1 NeoForge和Forge的历史渊源以及1.21.1的定位在拆文件之前先花点篇幅说说NeoForge到底是个什么东西。2023年Forge社区发生了一次比较大的分支事件一部分核心开发者另起炉灶基于Forge的代码基础做了二次分支这就是NeoForge。到1.21.1这个版本NeoForge已经相对成熟API和Forge有不少差异比如命名映射从MCP换成了官方混淆映射Official Mappings包名从net.minecraftforge变成了net.neoforged。1.21.1是目前Mod开发里比较舒适的一个版本NeoForge的API稳定、文档相对齐全、社区积累了不少踩坑经验Mod加载机制也梳理得比较干净。和更高版本比如1.21.4、1.21.5比起来1.21.1的生态更稳很多库和前置Mod都跟得上。理解了这一点你就能明白为什么项目结构里会出现neoforge相关的包名和依赖坐标——这不是拼写错误而是NeoForge刻意和Forge做区分的表现。2.2 为什么需要标准化的目录结构Minecraft的Mod项目本质上是一个Java工程但它比普通Java项目多了一层复杂性需要被打包成符合Minecraft加载规范的Jar文件资源文件要放到Minecraft能识别的路径下代码要经过Gradle的混淆映射处理。如果没有一套标准化的目录约定每个Mod作者各写各的那Mod加载器就没法统一处理了。NeoForge继承了Forge时代沉淀下来的项目布局习惯再叠加Gradle的sourceSets机制形成了一套固定的范式。这套范式的核心逻辑就一句话约定优于配置。你不需要在每个文件里声明我是谁、我在哪只要放对位置构建工具和加载器自动就能认出来。这对于团队协作也有实际意义你从别人仓库拉下来的项目哪怕完全没看过代码只要知道目录约定很快就能定位到入口类、配置文件和资源目录排查问题效率高出一大截。3. 核心细节拆解逐层看懂NeoForge项目的家底3.1 根目录下的一堆文件到底都是干嘛的用IDE推荐IntelliJ IDEA导入NeoForge官方模板或者用MDK生成项目后根目录会有一堆文件。第一次看到这个阵仗别慌我一个个给你过。build.gradle整个项目的构建脚本Gradle的入口所有依赖、插件、发布配置都写在这。重要的东西是plugins块里声明的net.neoforged.gradle.userdev插件版本以及dependencies块里的neoForged依赖坐标。settings.gradle声明项目名称和仓库地址。一般不用动但如果要换Maven仓库镜像改的就是这里。gradle.properties项目级别的属性文件定义Minecraft版本、NeoForge版本、Java版本等变量。这是MDK模板比较贴心的地方升级版本时改一处就能全局生效。gradlew/gradlew.batGradle Wrapper的启动脚本保证团队里所有人用的Gradle版本一致避免我本地能编译、你那边报了怪错的尴尬。gradle/wrapper/gradle-wrapper.properties指定Wrapper要拉取的Gradle版本号。src/源码根目录核心中的核心下面单独拆开讲。不少新手习惯直接用系统里装的Gradle跑构建我建议统一用gradlew。Wrapper会自动下载匹配的Gradle版本绕开版本不一致的问题。实测下来这一条能帮你省掉大量环境没问题但就是编译不过的排查时间。3.2 src目录的纵向拆解main、resources、generatedsrc目录是Mod代码和资源的家它按Gradle标准sourceSet组织NeoForge在此基础上增加了Minecraft特有的分层。先看整体结构src/ ├── main/ │ ├── java/ # Java源码 │ └── resources/ # 资源文件 └── generated/ # 运行时生成的文件部分版本会用到main/java下面跟着包名路径走比如com/example/examplemod/里面放你的Java类。main/resources存放不参与编译但在运行时需要的文件——Mod图标、语言文件、数据包JSON、材质贴图等。generated目录比较特殊它是Gradle运行runData任务时自动生成的数据文件输出目录比如你写了数据生成器DataProvider跑一次gradlew runData就会在这个目录下产出对应的JSON文件。这些文件通常不手动编辑生成后要么直接留在原地、要么拷贝到resources下参与打包。3.3 main/java里的核心Mod主类的前世今生每个NeoForge Mod必须有且仅有一个用Mod注解标记的入口类这个类就是Minecraft加载Mod时最先碰到的类。我见过一些项目把入口类命名为Main、Core、Entrypoint这个不强求但保持和Mod ID相关的命名习惯更利于维护——比如Mod ID是myfirstmod主类叫MyFirstMod就比较清晰。一个最小的NeoForge 1.21.1主类长这样package com.example.examplemod; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; Mod(myfirstmod) public class MyFirstMod { public MyFirstMod(IEventBus modEventBus) { // 构造器里完成模块注册 } }注意几个关键点。第一Mod注解的参数必须和gradle.properties或neoforge.mods.toml里声明的Mod ID完全一致大小写敏感。第二构造器的参数由NeoForge自动注入你声明IEventBus modEventBus框架会传给你Mod总线声明FMLJavaModLoadingContext之类也可以但1.21.1推荐直接用构造器参数拿事件总线。第三主类不一定要继承任何基类——NeoForge不强制继承ModContainer之类的类保持POJO风格反而更灵活。3.4 resources目录里的隐藏机关META-INF和pack.mcmetasrc/main/resources下有几个文件决定了Mod能不能被Minecraft识别属于那种文件结构不对代码写得再好也白搭的存在。最核心的是META-INF/neoforge.mods.toml——这是NeoForge加载器的元数据声明文件Mod ID、版本、依赖、入口类全写在这里。1.21.1版本里这个文件用的是TOML格式字段含义比较直观modLoader javafml loaderVersion [4,) license MIT [[mods]] modId myfirstmod version 1.0.0 displayName My First Mod authors YourName description A demo mod for NeoForge 1.21.1 [[dependencies.myfirstmod]] modId neoforge type required versionRange [21.1.0,)modLoader javafml告诉加载器用JavaFML的方式加载[[mods]]定义了Mod本身的信息[[dependencies.xxx]]声明依赖关系这里声明了对NeoForge的依赖版本范围用区间表达式表达[21.1.0,)表示21.1.0及以上。还有一个文件是pack.mcmeta它是资源包的描述文件。Minecraft需要每个资源目录都有这个文件才能识别。一个最小可用的pack.mcmeta长这样{ pack: { description: My First Mod resources, pack_format: 34 } }pack_format的数字和Minecraft版本一一对应——1.21.1对应的是34。这个数字如果写错游戏会提示资源包版本不兼容但NeoForge对Mod内的pack.mcmeta校验相对宽松可别因为这个报错浪费半小时。4. 实操过程从零创建一个完整可运行的NeoForge 1.21.1 Mod4.1 第一步搭出工程骨架打开浏览器进入NeoForge官网下载对应1.21.1的MDKMod Development Kit压缩包或者直接用Git clone官方模板仓库。我个人更推荐后者——通过Git拿到的是模板最新状态而且方便随时拉取更新。拿到模板后先把gradle.properties改掉这是整个项目最关键的配置文件之一minecraft_version1.21.1 neo_version21.1.0 # 注意Minecraft版本和NeoForge版本要配套 # 21.1.x对应Minecraft 1.21.1再看build.gradle里的依赖声明dependencies { implementation net.neoforged:neoforge:${neo_version} }这里的neo_version不是随便写的它和Minecraft版本号强关联。NeoForge的版本号规则是mc版本.补丁号比如21.1.0就是为1.21.1服务的。选版本时去NeoForge的Maven仓库看一眼挑最新稳定的补丁号就好。改完这两处在项目根目录执行./gradlew idea或者直接用IDEA打开build.gradle让IDEA同步Gradle项目。第一次同步会下载大量依赖耐心等。这里有个经验国内网络环境下如果拉取缓慢可以在settings.gradle里配置阿里云Maven镜像实测能把依赖下载时间从半小时压到几分钟。4.2 第二步写主类和基础资源文件在src/main/java下按你的包名创建目录写一个带Mod注解的主类代码可以先用最小可运行版本开头别急着堆功能。配套创建src/main/resources/META-INF/neoforge.mods.toml和src/main/resources/pack.mcmeta把Mod ID、作者、描述、版本号都填好。这里有个细节特别容易踩坑Mod ID只能用小写字母、数字、下划线、连字符不能以数字开头而且一旦发布就不要改了——因为存档里的方块、物品、附魔数据都会以Mod ID作为命名空间前缀改了ID等于让旧存档全部失效。还需要准备一个Mod图标。放在src/main/resources/下任意位置都行但惯例是放在根目录或者icon.png尺寸推荐64x64或128x128PNG格式。图标不是必填项但没有的话游戏里会显示一个默认的草方块图标观感比较凑合。顺手做一张简单的就行不必太较真。4.3 第三步跑起来验证一切正常在IDE里找到Gradle面板展开Tasks下的neoforge分组里面有个runClient任务。直接双击运行或者命令行执行./gradlew runClient这个任务会启动一个开发环境的Minecraft客户端。首次运行时Gradle会下载Minecraft的资源和依赖然后进入游戏主菜单。确认Mod加载成功的方法是游戏启动日志里搜索你的Mod ID——出现类似Completed mod loading的字样并且在Mod列表里能看到你的Mod信息就算基本成功。我能给你的实操建议是第一次跑客户端不要带任何其他Mod。NeoForge开发环境的依赖里已经包含了必要的库不需要额外装Forge或OptiFine之类的东西。带多了反而容易引发版本冲突排查起来特别痛苦。4.4 第四步注册第一个物品确认代码路径真的通了只写一个空主类虽然能跑但没法验证你的代码是否真正被加载进了游戏。我建议第二步就注册一个最简单的物品写个无脑的测试流程。package com.example.examplemod; import net.minecraft.world.item.Item; import net.neoforged.bus.api.IEventBus; import net.neoforged.fml.common.Mod; import net.neoforged.neoforge.registries.DeferredRegister; Mod(myfirstmod) public class MyFirstMod { public static final DeferredRegister.Items ITEMS DeferredRegister.createItems(myfirstmod); public MyFirstMod(IEventBus modEventBus) { ITEMS.register(modEventBus); } }这里用到了DeferredRegister——NeoForge提供的一个延迟注册机制。它不直接向游戏注册物品而是先把注册动作记录下来等游戏进入注册阶段时再统一执行。这套机制的好处是避免加载顺序问题、支持自动处理注册表的冻结状态是1.21.1版本下注册内容的推荐姿势。注册完物品后你就能在游戏里用/give p myfirstmod:item_name拿到这个物品。如果这个命令生效了说明从主类到资源文件再到注册链路全部打通了——这才是真正意义上的第一个Mod跑通了。4.5 打包发布产出可安装的Jar文件开发环境能跑还不够最终要给玩家用的话需要打出可安装的Jar包。执行./gradlew build构建产物在build/libs/目录下通常是一个xxx-1.0.0.jar文件。直接把这份Jar丢进Minecraft的mods文件夹就能用前提是玩家已经装好了对应版本的NeoForge加载器。打包时有个细节值得注意NeoForge构建出的Jar里一定包含META-INF/neoforge.mods.toml这是它被加载器识别的凭证。可以用压缩软件打开Jar检查一下如果这个文件不在或者路径错了那加载器会直接跳过你的Mod。5. 遇到的问题与排查心得从日志到依赖的避坑全记录5.1 启动直接崩溃日志报LoadingException这是我见过最多的情况新手尤其容易遇到。核心特征游戏启动到一半就崩溃日志末尾出现LoadingException或者ModLoadingException。排查思路固定三步第一步打开日志搜ERROR和Caused by第二步看是不是缺依赖——neoforge.mods.toml里写了依赖另一个Mod但没装就需要去把前置Mod下载安装第三步看版本冲突——NeoForge版本和Minecraft版本不匹配比如21.1.0的NeoForge硬塞给1.21.2的Minecraft那几乎必崩。我遇到过最离谱的一次崩溃是因为在neoforge.mods.toml的versionRange里写了个不存在的版本号区间加载器解析时直接放弃治疗。修复方式很简单把版本区间改回[21.1.0,)就一切正常。所以排查时千万别忽略这个看似人畜无害的字段。5.2 构建能通过但游戏里找不到Mod这个问题的典型特征是日志里根本没有你的Mod ID游戏Mod列表里也没有你的Mod。排查从两个方向入手。一是检查META-INF/neoforge.mods.toml里的modId是不是和Mod注解一致。我一度把Mod ID写成了my_first_mod而Mod里写的是myfirstmod加载器比对不上就直接跳过了。二是在IDEA里检查输出目录——有时候IDE没把resources目录里的文件拷贝到编译输出目录需要在Gradle面板里重新刷新、执行processResources任务或者干脆./gradlew clean build全量重建。5.3 注册物品时不小心把ID写重了这个错误不常发生但一旦发生就是注册阶段直接崩溃。核心报错一般是Registry Object Duplicate。原因很简单同一个注册表里同一个命名空间下不能有两条相同ID的注册项。解决方案也简单——改一个不一样的物品名或者检查是不是在代码里对同一个DeferredRegister重复调用了register方法。这里要给一个建议统一管理注册表的注册逻辑不要到处散着写register调用。我在自己的项目里习惯一个内容类型对应一个静态DeferredRegister注册方法全部收敛到模块内部看起来清爽排查时也一目了然。5.4 开发环境资源加载滞后改了没反应运行时改了JSON或者贴图但游戏里不生效。这不一定是你改错了往往是因为IDEA没有触发资源拷贝。Minecraft在开发环境下读取的是build/resources/main目录不直接读src/main/resources。所以Gradle的processResources没被触发的话你改的文件就不会被同步进去。解法很粗暴但有效看到改动没生效就先执行一次./gradlew processResources或者干脆重新跑runClient。开发机上反复重启确实慢但我建议至少养成改资源后重新跑任务的习惯不要被改了没反应这种假象拖住节奏。5.5 mods.toml字段填错但不报错neoforge.mods.toml里的字段值有一些隐性约束。比如displayName不能太长、description不能有多行特殊字符加载器在解析时可能不会直接报错但配置文件在游戏里展示时会异常。这类问题最讨厌因为不报错只能靠肉眼对比文档排查。我的经验是写完mods.toml后对照NeoForge官方的字段说明逐项核对一遍别偷懒。6. 主类之外事件总线和FML生命周期6.1 为什么主类构造器能和事件总线扯上关系很多第一次接触NeoForge的开发者会对主类构造器里的IEventBus参数感到困惑——为什么一个Mod入口类的构造函数还能拿参数这里要解释一下Mod注解的类NeoForge通过依赖注入的方式实例化构造器参数会被自动填入。IEventBus就是Mod总线它负责分发FML生命周期事件和Mod自身的事件。NeoForge有两条事件总线注意别混淆。一条是ModEventBus——处理Mod加载生命周期比如注册物品、方块、配方序列化器通常在主类构造器里通过modEventBus.register(...)或ITEMS.register(modEventBus)这种方式挂接另一条是NeoForge.EVENT_BUS——处理游戏运行期的事件比如玩家登录、实体受伤、方块交互。这两者的区别是许多新手容易绕晕的地方记住一个口诀ModEventBus管注册NeoForge.EVENT_BUS管运行。6.2 Mod主类什么时候真正被加载从NeoForge启动流程来看顺序大致是Minecraft客户端启动NeoForge加载器读取neoforge.mods.toml根据modId找到主类反射调用构造器注入事件总线然后依次触发FML的各个生命周期阶段。这些阶段包括FMLCommonSetupEvent通用初始化、FMLClientSetupEvent客户端专用初始化、RegisterEvent注册各种内容等。在实际开发里区分不同阶段做初始化很重要。比如依赖服务端持有数据的初始化逻辑放在FMLCommonSetupEvent里修改渲染配置的初始化放在FMLClientSetupEvent里。如果你不管三七二十一全堆在构造器里轻则逻辑混乱重则在某些加载场景下报状态异常。合理做法是构造器里只做注册动作真正需要数据准备的工作放到对应的事件阶段里去。6.3 模块化开发对主类的影响随着功能增加把一切代码写进主类会越来越臃肿。NeoForge社区比较推荐的做法是按功能域拆出独立的处理类比如ItemRegistry、BlockRegistry、EventHandler等主类只做装配和注册转发。这样做的好处是功能边界清晰换人或后续扩展时不会在几千行代码里大海捞针。这是我个人很推崇的风格主类保持短小精悍内部只创建各个模块的DeferredRegister并注册到事件总线其余逻辑全部下沉到各自的模块类里。7. 从内容本身再往深看命名的价值与维护的意义7.1 命名规范Mod ID决定你的生态位Mod ID不只是一个识别符号它会渗透进Minecraft的整个数据体系。物品注册表、方块注册表、配方、战利品表、数据包路径全都以Mod ID作为命名空间前缀。举个例子你的Mod ID是myfirstmod注册一个名为ruby的物品它在游戏里的完整注册名就是myfirstmod:ruby存档里也是这么存储的。这意味着Mod ID一旦发布出去就基本不能改一改就相当于抛弃了之前所有存档中的数据。所以取Mod ID时建议短、好记、全小写、不含特殊字符。比如myfirstmod就比My_First-Mod_2024稳妥得多。7.2 文件结构与多人协同多人协作开发时文件结构的规范性直接决定团队的开发效率。如果大家都在主类里塞东西git合并时冲突会极其频繁。我见过的做法是每个人负责独立的功能包比如player包、worldgen包、combat包包之间尽量避免互相依赖的静态引用用事件总线做解耦。这样合并冲突的概率会大幅下降代码评审也能聚焦到具体模块。另外.gitignore一定要在项目搭建初期就配好把build/、.gradle/、run/、IDE的工程文件目录都排除在外。不然哪天不小心把整个构建目录提交上去仓库体积爆炸不说每次构建都会有一堆无关文件变动非常难受。7.3 版本跨越时的文件结构变化NeoForge每个版本之间在文件组织方式上不是一成不变的。比如早期Forge时代neoforge.mods.toml还不存在用的是mcmod.info比如数据生成目录在不同版本下也可能有变化。所以当你从网上找教程时一定要确认教程对应的Minecraft版本和NeoForge版本很多报错其实是版本的代沟而不是你写错了。我自己的经验是官方文档对每个版本的变更都有记录遇到大版本切换时先看release note而不是直接套老经验。8. 由这份笔记延伸出去的开发习惯8.1 版本对照要养成查文档的习惯开发Mod和做普通Java项目的感受很不一样——生态活跃度高API迭代频繁且不同版本之间兼容性差异巨大。拿1.21.1来说NeoForge的API和1.20.1的Forge之间就有大量区别。所以遇到不懂的类或方法第一反应应该是查对应版本的官方JavaDoc而不是直接用老经验猜。这是我在多个版本间切换后最深刻的体会。8.2 善用runData生成重复数据一旦你要做的Mod内容量上来手动维护一堆JSON数据文件会变得非常痛苦。NeoForge提供了数据生成器框架允许你用代码来生成物品模型、语言文件、配方、战利品表等。在文件结构层面这意味着你的src/main/java下会多出一个datagen目录通常叫datagen或data包具体取决于你的组织方式用GatherDataEvent挂接生成器。跑一次./gradlew runData所有JSON会在src/generated目录下生成然后你再根据需要拷贝或引用。这个流程能让你少写大量重复内容值得在了解基础文件结构后就开始使用。8.3 社区资源是最大的知识库围着一个问题卡几个小时不如花五分钟搜一下社区。Minecraft Mod开发有非常活跃的社区生态遇到问题英文社区和中文社区都有大量讨论。搜索时带上你的版本号比如搜NeoForge 1.21.1 DeferredRegister example通常比搜NeoForge DeferredRegister更能命中有效内容。8.4 本篇的根基价值现在回头再看这篇笔记的价值文件结构和主类就像一栋楼的地基和承重墙你做任何功能都绕不开它们。地基歪了墙面装修得再好看也没用主类臃肿了后续扩展就是灾难。把这两个基础打牢后面的数据生成、事件监听、自定义渲染、网络同步才能有干净的落脚点。9. 实操的碎碎念我在实际开发中踩过的坑最后聊几个偏个人经验的话题可能正文前面已经零散提到了但值得集中再说一次。第一开发环境建立好之后不要频繁切换NeoForge版本。我在1.21.1之前顺手试过1.20.4很多API命名和包路径完全不同频繁切换会让大脑缓存一片混乱。认准一个版本把手里的功能做完再考虑迁移。第二Gradle构建慢不是你的问题是网络和缓存的问题。如果条件允许配置好镜像、增加内存参数在gradle.properties里设置org.gradle.jvmargs-Xmx4G能切实改善构建体验。但不建议把org.gradle.daemon关掉那个反而会让构建更慢。第三调试Mod时善用日志。NeoForge的日志系统是基于SLF4J的直接在类里声明private static final Logger LOGGER LogUtils.getLogger();在关键节点打日志通常比断点调试更直观。游戏渲染进程本身就是个重型应用断点有时候会卡得人没脾气。第四对于修改注册表数据的Mod养成经常备份测试存档的习惯。有些错误不会立刻崩溃但会一步步腐蚀存档数据。一个干净的测试存档能帮你快速判断问题是出在代码还是存档脏数据上。第五也是我认为最实用的一条不要让完美主义卡住你的第一个版本。先让代码跑起来哪怕注册一个毫无意义的物品、打个无用的日志都比对着一个完美的空壳反复犹豫强。跑通了你才有继续往前的底气。这篇笔记写到这文件结构和主类的核心内容基本都覆盖了。下一篇我打算写事件监听和数据生成的实际应用把Mod能做点什么这件事展开到时候见。
返回列表