绪论
前言
作者水平有限,望各位读者不吝赐教。
这篇教程:
- 其实是我写给我自己看的;
- 当然也可以帮助想学习mod开发的你入坑。
所以,但与其他教程可能略微不同的是,我包含了在开发《我的六号(Rainbow6:Minesiege)》(以下简称R6MS)、《灭 Extinguish》、《亮光 Brighter》、T88、《疯狂粒子 MadParticle》等模组时的经验总结。对应地,不像通常的教程会教你写一个foo
物品、bar
方块,我更倾向于告诉你如何从无至有地写出破片手榴弹、消防水炮等等具体的物品、方块、实体或者其他的什么东西。
开始之前:需要什么?
本小节内容主要面向没有足够编程经验的人群。
这个经典问题的完整表述是:需要什么才能进行Minecraft模组开发?
有无数的前人在无数的评论区/帖子/文章/教程/频道中讨论过此事,我将给你我的回答。
-
你当然得玩Minecraft;你需要了解方块、实体、物品、世界、游戏刻等基础概念。
-
想要与程序打交道,英语是一切的基础。作为最低的标准,你应该以较高的英语成绩通过了中考,或者以适当的英语成绩通过了高考;本教程也假设你有对应的英语基础;
-
编程语言方面则稍微复杂一些:
在你掌握Java基础之后,我非常推荐你去看看这个补充课程。
-
接下来的事情则可能没有那么好量化。你需要知道自己想要什么。 你首先需要一个明确的目的,然后在你了解MC各个基础概念的情况下,把你的想法具象为某种实现方式:是想要写一个物品,通过物品右键来实现某种功能?还是想要写一个方块,右键方块打开GUI来合成什么?或是提供一种新的游戏机制?改变世界的生成样貌?修改现有的某个机制?
这是一些简单的例子:
- 通常来说,世界、服务端/客户端、刻、事件等概念是所有mod都需要的基础知识;物品、方块、方块实体和实体是几乎所有mod共通的4个具体知识;
- 编写Java自然需要使用IntelliJ IDEA;
- 如果涉及到模型和材质,你需要学习使用Blockbench;
- 如果你想要修改原版机制,或者需要对原版内容动手动脚,你需要学习Mixin和AT的知识;
到后面,每个开发者的情况都会依需求而有所不同:
- 如果你厌烦了某些重复编码,你可能需要学习注解和注解处理器的知识;
- 如果一个单一平铺的项目不能满足你的需求,你可能需要学习Gradle的有关知识,而不是只会简单地改两个参数就完事;
- 如果你想要写一个跨Forge/Fabric的mod,你可能需要学习Architectury的使用;
- 如果你想要单个文件内跨版本而不是疯狂切分支,你可能需要学习Manifold编译器插件的使用;
- 如果你想要更加酷炫的视觉展示,你可能需要学习图形学知识,OpenGL的知识,以及使用GLSL编写shader;
- ......
随着开发的逐渐深入,你可能会需要各式各样的黑白魔法来推进你的工作。
你可以在开发某个功能之前寻找有没有类似的mod可供借鉴,查看并反思别人的实现方式优劣,这可能会使你少走一些弯路;
-
另一个很少被提及的锦上添花的条件是:数学。只需要一些初等数学,你的某些实现逻辑就可能得到不小的效果提升或是性能优化;用到大学数学的时候相对很少,但仍然可能对你的算法有帮助。
题外话:直到什么时候,你就知道自己已经入门了,成为了合格的MC开发者?
我的答案很简单:当你知道自己不知道什么的时候,恭喜你。
游戏版本
这篇教程混杂有多个版本的内容,总体以Minecraft 1.18.2及其对应的Forge 40.x为起点。
- 1.19.4
- 1.20
- 1.20.4
- 1.21
2023年3月20日更新:参照TeaCon 2023的规定,即日起教程内容将以Minecraft 1.19.4及其对应的Forge 45.x为主。
在此之后,已经编写的部分若需变化将会以这样的选项卡补丁形式呈现。适用于新版本的原有内容不会被修改,新编写的部分也不会有提示,你可以默认它们适用于新版本。
显然我不一定能全部找到需要打补丁的部分——所以如果教程中提到的某个方法找不到或者不对劲,那就有可能是漏掉了。
2023年6月13日更新:参照TeaCon 2023的变化,即日起教程内容将以Minecraft 1.20及其对应的Forge 46.x为主。
2024年1月20日更新:随着卡慕村民生存项目的结束,即日起教程内容将逐步更新至Minecraft 1.20.4及其对应的NeoForge。
关于NeoForge,请查看下面的内容。
2024年7月27日更新:参照TeaCon甲辰的规定,即日起教程内容将以Minecraft 1.21为主。
至于你应该选择什么游戏版本?一个比较普遍的建议是:选择落后最新版本一个大版本的版本。比如麻将正在发1.19.x的快照,那就建议选择1.18.2。
- 我强烈反对你选择落后于1.13的版本——扁平化更新 前后有相当大的变化;
- 我不推荐你选择落后于1.16.5的版本——你可能更难在网络上得到支持;
- 我不反对你选择最新版本——记得及时升级Forge就行。
对于Forge的小版本号,选择稳定版或者最新版都行。一般情况下这些版本间的差异对你不会有太大影响。
教程结构
在页面最下方有更多关于作者的链接。欢迎反馈。
页面右侧通常会有该章内容索引,给你的鼠标滚轮省点寿命。
每一大章的后面可能会有奇奇怪怪的注意事项
,这算是一个踩坑大合集。你至少应该了解其中的内容小标题,在涉及到或者你愿意的时候回来看具体细节。
在开始之前,我建议你先看看-1.1 折跃门。其中有许多值得一看的内容,可能比我这篇教程对你更有帮助。
本教程中的:
- “我”指本教程或作者;
- “你”指正在阅读这段话的人;
- “原版”或类似表述并不区分提供方是Forge还是Minecraft;
- “可以”“建议”指一种建议,请自由选择采纳其与否;
- “应该”“应当”指一种建议,但除非你十分熟悉有关内容,请照做;
- “需要”指一件必须做的事,请 照做。如您觉得不妥,请与作者联系。
那么,让我们开始吧!
你需要为你的IDEA安装一款叫Minecraft Development
的插件,它将是你以后开发过程中的得力助手。同时我建议你安装一款叫Alibaba Java Coding Guidelines
的插件,它由阿里巴巴开发,提供代码规范性检查,适合大多数人。在绝大多数时候你应该听它的。插件的官方版本已经很久没有更新了,安装时你可以选择第三方维护的版本。我也安装了中文语言包
,所以在多数情况下我会使用对应的中文来描述操作。
安装插件重启IDE后新建项目,你应该就能看到名为Minecraft
的选项了。
随着MCDev插件的更新,以下提到的项目名称或顺序可能发生改变。自行甄别即可。
名称
写你的完整项目名,平台选Mod > Forge
,MC版本选1.18.2,Forge版本选最新的。Mod Name
也填完整名称,Main Class
不用管,由其自动生成即可。- 勾选
Use Mixins
,选择你想要的许可证。
如许可证,法律与协作一章所述,我建议你谨慎选择一个许可证。
- Optional Settings不用管。
GroupID
指的是你或者这个项目所在组织的域名(倒着写),对我而言就应该填入cn.ussshenzhou
。如果你并不拥有自己的域名,你可以填入pers.[你的名字]
。pers
代表personal,即这是你的个人项目。ArtifactId
指你的mod的modID(不含任何符号),对我而言,这个mod将用于教程演示,所以填入tutorialmod
。你会发现它自动的根据项目名称为你填上了,但你需要注意应该全部小写,不含有任何特殊符号。
modID是你的mod在游戏中的通用识别凭证,每个mod都(应该)有独一无二的modID。它通常可以是你的mod英文名称的小写,也可以进行适当地简化。对《我的六号》而言是r6ms
,对《灭》而言是extinguish
。modID允许含有下划线。
Version
自然就是你的版本号,我个人习惯是填入0.1.0
。- JDK自然是选择17。创建项目。
IDE左下侧的运行
和构建
图标会带上正在运行的绿点,点开,停止所有在运行的工作。在左侧找到build.gradle
,打开,作如下修改。
buildscript {
repositories {
// These repositories are only for Gradle plugins, put any other repositories in the repository block further below
maven { url = 'https://repo.spongepowered.org/repository/maven-public/' }
mavenCentral()
maven { url = 'https://maven.parchmentmc.org' }
}
dependencies {
classpath 'org.spongepowered:mixingradle:0.7-SNAPSHOT'
classpath 'org.parchmentmc:librarian:1.+'
}
}
plugins {
id 'net.minecraftforge.gradle' version '5.1.+'
}
apply plugin: 'org.spongepowered.mixin'
apply plugin: 'org.parchmentmc.librarian.forgegradle'
然后向下找到mappings channel: 'official', version: '1.18.2'
,将其改为mappings channel: 'parchment', version: '[发布日期]-1.18.2'
。在这里查看对应版本的最新发布,将其填入。对我来讲,应该是mappings channel: 'parchment', version: '2022.07.17-1.18.2'
。
当然我建议你选择本教程支持的最新版本。
Gradle默认缓存位置是C:\Users\[用户名]\.gradle
,如果你想要改为其他位置,你可以在此时到设置 > 构建、执行、部署 > 构建工具 > Gradle
里改成你想要的位置。
如果窗口中悬浮有gradle刷新标志。点击它。如果没有,右击build.gradle
,选择“链接Gradle项目”。Gradle将会为你下载和生成一大堆东西,这个过程可能需要花费十五分钟、半小时或更久。在一大堆狂暴输出之后,如果你看到BUILD SUCCESSFUL
,这代表你已经成功构建了开发环境。不过别着急,在BUILD SUCCESSFUL
之后应该还有一些工作要等待电脑完成。
如果出现各种各样奇奇怪怪的报错,提示找不到这不认识那,以至于构建失败的话,直接删除Gradle缓存文件夹再重新构建不失为一种简单而有效的办法。
这是一个可选项:在右侧点开小象图标的Gradle栏,找到Tasks > other > hideOfficialWarningUntilChanged
(以下省略Tasks
)并双击运行,这将隐藏官方映射表的警告,也就是这一段:
(c) 2020 Microsoft Corporation. These mappings are provided "as-is" and you bear the risk of using them. ...
如果你已经看到BUILD SUCCESSFUL
却又看到一堆错误或警告,且文件位置在build/tmp
文件夹下,你可以直接把那些文件删掉。
运行forgegradle runs > genIntellijRuns
,它将进行一系列的准备工作,其中包括下载MC的各种资源文件。当你修改了Forge版本等,更简单地说,改动了build.gradle
之后,不仅要刷新Gradle,也要运行这个任务。
在左侧找到resources
文件夹,展开,检查 是否有[modID].mixins.json
这个文件。这是mixin配置文件,你暂时不需要理会具体内容。正常情况下应该是有的。如果没有即新建并填入:
{
"required": true,
"minVersion": "0.8",
"package": "[你的mod包地址].mixin",
"compatibilityLevel": "JAVA_17",
"refmap": "mixins.[modID].refmap.json",
"mixins": [
],
"client": [
]
}
[你的mod包地址]
应该是类似这样的格式:cn.ussshenzhou.tutorialmod
。你可能注意到了mixin
被标红了,这是因为这个包暂时不存在。在项目目录中新建此包即可。然后在在这个项目栏的右上角打开设置,如图所示取消勾选。
此时,你的文件结构应该是这样的:
现在,你可以运行runClient
了。一切顺利的话,在风扇的轰鸣之后你就能看见游戏主菜单了。
如果runClient
不成功,显示如下信息:
Unrecognized option: -p
Error: Could not create the Java Virtual Machine.
Error: A fatal exception has occurred. Program will exit.
你需要到项目结构
里将SDK改为17或更高,而不是1.8。
在编写实际内容之前
请记住,原版实现是你最好的伙伴 (当然如果你的mod目的就是对原版进行优化等,当我没说,不过有能力优化原版内容的读者大概是不需要看本教程的)。在实现某个物品/方块的某个功能之前,请仔细回顾原版内容中是否有可以借鉴的实现;在遇到教程不曾涉足的未知领域时,也请先想想看看原版是怎样实现的。
不过需要注意的是,Forge通常接管了大部分的注册系统。因此在注册什么东西时,照抄原版实现可能不太行得通。
一些概念
Forge
这可能是最基础也是最重要的大概念了。
显然,Minecraft是个商业软件,其在发布时经过混淆(把原来正常的的变量名改为无意义名称)与编译,而原则上你也不可以将其反编译、修改、链接的。
拜JVM语言的易于反编译(看看隔壁基岩版吧,惨)和Mojang也知道Minecraft社区驱动的特殊性质所赐,这里有一个小小的灰色地带供开发者们折腾。
而Forge,就是这个小小的灰色地带至今最巨型的产物。
我们通常所说的Forge其实包含两个子项目:FML与MinecraftForge。
FML全称是Forge Mod Loader,它是一个mod加载器,能够让你一次把jar拖进/mods
里就能装一大堆mod,而不是像上古时期一样去用覆盖jar文件的方式来安装mod。
MinecraftForge的职责则更广泛,它负责修改MC底层源码(为了给其他mod用,或者修麻将没修的bug)并暴露相关接口(例如注册系统),并半独立地提供一个mod兼容层(Capability系统和事件总线等)。
此外还有一个ForgeGradle,由Forge团队推出的Gradle插件,负责帮助设置开发环境相关的东西。
- 1.20.4
NeoForge
在2023年7月13号——这个在Minecraft未来的历史中将被铭记的伟大的日子——原Forge团队中的绝大部分人出走,在Forge的技术基础上建立了NeoForge项目。
截至2024年1月20日,甩去包袱后的NeoForge仍然在持续地进行大量更新,推出了许多对开发者来讲十分便利的新功能。虽然目前还不太稳定,但我仍然建议你在编写1.20.4的项目时使用NeoForge。
Parchment
正如上文所讲,MC源码是经过混淆而成的天书,我们需要想办法把无意义的名称重新反混淆为有意义的名称。而负责储存这个映射关系的,就是混淆表。
你可能已经注意到了,我们一建立项目,就把mappings channel
从offical
改成了parchment
。
以下内容引自《Boson 1.16 Modding Tutorial》,FledgeShiu,CC BY-NC-ND 4.0:
我们得从Minecraft本身说起,首先我们得明确Minecraft是一个用Java写成的商业软件。这意味着两件事:第一,Minecraft相对容易修改;第二,代码本身是不开源 而且是被混淆过的。在Minecraft历史的早期,因为在Mojang一直都没有给Minecraft提供官方API,所以「Mod Coder Pack」项目诞生了(以下简称为MCP)。
还记得我之前说过的,Minecraft的两个特性吗?MCP就利用这两个特性,实现了一套工具,可以让开发者可以直接修改Minecraft jar包里的内容。
于是
srg名
,notch名
和mcp名
诞生了。那么这三个是什么呢?
首先是
notch名
,他是Minecraft直接反编译、反混淆之后的名称,通常是无意义的字母数字组合。你从名称Notch就可以看出,这个名字是直接来自Minecraft(以及对Notch的怨念),举例来说j
就是一个典型的notch名
。接下来是
srg名
,这个名字是和notch名
是一一对应的,srg名
在一个版本里是不会变动的,之所以叫做srg名
,是为了纪念MCP项目开发的领导者Searge。在srg名
中,Minecraft中的类名已经是可读了,变量方法等名称虽然还是不可读,但是有相对应的前缀和尾缀来区分了。以上面的j
为例,它的srg名
是func_70114_g
。最后是
mcp名
,这个名称也是我们mod开发中接触最多的名称,在mcp名
中,代码已经是可读的了。和我们正常写java程序中的名称没什么两样。但是mcp名
是会变动的。举例来说上面的func_70114_g
它的mcp名
是getCollisionBox
。mcp名
中的类名和srg名
中的类名是相同的。
以下内容引自《TeaCon 茶后谈第 257 期》,3TUSK,TeaCon执行委员会,CC-BY 4.0:
每当我们迎来 Minecraft 大版本更新的时候,我们总能在各种地方看到翘首以盼等模组更新的玩家们,但屏幕前的你是否曾思考过一个问题:模组开发者如果想要更新他们的作品,需要满足哪些必要条件?这个「模组开发者也在等」的清单里的内容会因为切入问题的角度的不同而不同,但在这个列表中肯定有一个东西排在前面:「我们需要一个模组开发者能用的开发环境」,或者说「我们需要反混淆 Minecraft,不然我们就得硬啃混淆后的 Minecraft,另外,我们有时候还需要反编译 Minecraft」。反混淆的问题很好解决:把大家破译出来的信息(即所谓的「反混淆名」)收集成一个表就是了。然而,开发者们很快意识到 Minecraft 每次更新后,混淆的对应关系都会有所变化,反混淆名和混淆后的名字需要重新建立对应关系,另外破译出来的结果也可能会在未来被发现并不准确,需要修订。为了应对这些问题,「中间名」的概念应运而生:这边大家把反混淆名跟中间名对应上,那边 Minecraft 更新后再把新的混淆名和中间名对应上。这样一来,两边的进度就都不会被 Minecraft 本身的更新所耽误,模组开发者还可以通过使用这个「中间名」来抵消「反混淆数据变来变去」的影响。至于反编译?表面上我们只需要一个反编译器就够了,但实际上,反编译器并不一定能百分之百复原程序编译前的样貌,为此我们一方面可以通过改进反编译器来提供更好的反编译结果,另一方面我们也可以考虑直接上手,人工修复反编译的结果。在 Minecraft 模组社区的十余年历史中,这个说难不难,说轻松也不轻松的工作,到今天却只有寥寥数个团队在做,比较知名的有直属 Forge 的 MCPConfig和直属 FabricMC 的 Intermediary。其中,MCPConfig 提供通称 SRG 的中间名数据和修复反编译结果的补丁(patch);而追求轻量化的 Fabric 工具链则只提供了通称 Intermediary 的中间名数据。
很久很久以来,mcp表是Forge mod开发中最常用的混淆表,对应的mappings channel
通常是snapshot
或者stable
。
在2019年9月4日,MC 1.15的第三个快照19w36a发布,与此同时,Mojang第一次发布了官方的混淆表,对应的mappings channel
是offical
,简称moj表或官表。由此mcp表便失去了大部分存在的意义,被交由社区维护,这也进一步促使一部分开发者便逐渐向moj表转移。
但是moj表只包含类名、方法名等的映射,不包含形参/变量名,对开发者并不足够友好。于是为了补充形参/变量名的反混淆,Parchment出现了,它对应的便是parchment
。你可能会在源码中见到有些参数前加有一个“p”,这是Parchment为了与moj表区别所加。
- 1.20.4
Moj in everywhere
1.20.4的NeoForge彻底取消了SRG作为中间名的存在,实行moj in everywhere
的政策。
这意味着你需要在看到本教程后续章节中提及SRG名时自动地在脑子里转换为Moj名称。
Level
Level
类,mcp表称world
,顾名思义是一个世界。一个服务器上一般至少有三个世界:主世界、下界和末地。当你在其中一个世界玩耍时,服务器会将你所处的世界的必要信息,例如其他玩家、生物、区块数据等,发送给你的客户端,这样你才能看到游戏画面。由此,Level
显然会有两个子类:ServerLevel
和ClientLevel
,分别主要负责逻辑运算和画面渲染。Level
涵盖几乎一切,几乎一切游戏对象都包含在Level
里,从其获取、操作。
mods.toml
你可能注意到这个自带的文件,它会在游戏启动、模组被加载时读取,告诉模组加载程序你需要哪些依赖,也含有你的mod要展示在主菜单mods
选项内的内容。你可以阅读其中的注释并尝试自定义一些内容。更详细的说明可以在Forge社区Wiki或者《正山小种》找到。其中《正山小种》的这个页面还含有其他许多内容,你要是愿意可以一并阅读。
值得注意的是,对于logoFile
一项,你应该将对应的图片放在resources
文件夹下,与上面提到的mixin配 置文件并列。
当你在开发环境启动游戏,进去mods
选项之后,你很有可能会发现你的mod版本号为0.0NONE
,与你在build.gradle
中填写的并不相符。多数时候这是意料之内的,你可以尝试将mod导出至一个生产环境下的MC(即通过HMCL/PCL什么的启动的MC),就会发现版本号变为你在build.gradle
中填写的样子。
如果生产环境下的mc版本号仍然不正确,你可以考虑直接将"${file.jarVersion}"
替换为相应的版本号即可。不过要记得以后在更改版本号时,除了build.gradle
,也要修改此处。
奇奇怪怪的注意事项
判断开发环境
有些时候出于调试目的,你可能想在开发环境和生产环境执行不同的逻辑。
在多数情况下,用ForgeGameTestHooks.isGametestEnabled()
就可以得知了。如果你想要更精细一点的信息,可以参考这个方法的实现。
文件结构
mod源文件可以有多种分包形式,你可以按你自己的喜好来选择一个合适的形式。下面是一个我个人比较习惯的文件结构,对于《灭》这样小规模的mod来说足够了。更大型的项目有着不同的组织形式,你可能需要把客户端专有的类集中到client包下面,或者需要一个名为api的包来开放接口,诸如此类。
GameRule——传火注意!
了解一些历史的读者大概很容易就能看出这一条从何而来。
请牢记游戏中GameRule
涉及到的相应选项。当mod的行为可能会涉及到有关内容时,按需添加相应的判定。以下是一些比较常见的条目:
GameRule名称 | 含义或涉及到的情况 |
---|---|
doFireTick | 火的蔓延、自然熄灭 |
doEntityDrops | 非生物实体掉落物品 |
drowningDamage | 玩家窒息伤害 |
fallDamage | 玩家跌落伤害 |
fireDamage | 玩家火焰伤害 |
freezeDamage | 玩家冰冻伤害 |
pvp | 玩家之间的伤害 |
一个简单的例子:如果你想要让篝火能够意外引燃周围的方块,你应该在引燃前如此查询:level.getGameRules().getBoolean(GameRules.RULE_DOFIRETICK)
,就像原版的火和岩浆一样,而不是直接引燃方块。当然,如果是陨石砸地引起或炸弹爆炸的火焰,你或许就不需要这样判定。请以实际情况为准。