跳到主要内容

绪论


前言

作者水平有限,望各位读者不吝赐教。

这篇教程:

  • 其实是我写给我自己看的;
  • 当然也可以帮助想学习mod开发的你入坑。

所以,但与其他教程可能略微不同的是,我包含了在开发《我的六号(Rainbow6:Minesiege)》(以下简称R6MS)、《灭 Extinguish》、《亮光 Brighter》、T88、《疯狂粒子 MadParticle》等模组时的经验总结。对应地,不像通常的教程会教你写一个foo物品、bar方块,我更倾向于告诉你如何从无至有地写出破片手榴弹、消防水炮等等具体的物品、方块、实体或者其他的什么东西。

开始之前:需要什么?

提示

本小节内容主要面向没有足够编程经验的人群。

这个经典问题的完整表述是:需要什么才能进行Minecraft模组开发?

有无数的前人在无数的评论区/帖子/文章/教程/频道中讨论过此事,我将给你我的回答。

  1. 你当然得玩Minecraft;你需要了解方块、实体、物品、世界、游戏刻等基础概念。

  2. 想要与程序打交道,英语是一切的基础。作为最低的标准,你应该以较高的英语成绩通过了中考,或者以适当的英语成绩通过了高考;本教程也假设你有对应的英语基础;

  3. 编程语言方面则稍微复杂一些:

    1. 如果你没有任何编程基础,那你应该学习此Java文字教程日期与时间及其之前的部分,并大致了解了其余内容;或是学习了此Java视频教程的基础部分。在学习过程中还建议你了解相关的计算机原理、基础的数据结构等知识。
    2. 如果你有其他编程语言的基础,例如C或C++等与java语法较相近的语言,我仍然推荐你先去学习java语法而不是直接开始。
信息

在你掌握Java基础之后,我非常推荐你去看看这个补充课程

  1. 接下来的事情则可能没有那么好量化。你需要知道自己想要什么。 你首先需要一个明确的目的,然后在你了解MC各个基础概念的情况下,把你的想法具象为某种实现方式:是想要写一个物品,通过物品右键来实现某种功能?还是想要写一个方块,右键方块打开GUI来合成什么?或是提供一种新的游戏机制?改变世界的生成样貌?修改现有的某个机制?

    这是一些简单的例子:

    1. 通常来说,世界、服务端/客户端、刻、事件等概念是所有mod都需要的基础知识;物品、方块、方块实体和实体是几乎所有mod共通的4个具体知识;
    2. 编写Java自然需要使用IntelliJ IDEA
    3. 如果涉及到模型和材质,你需要学习使用Blockbench
    4. 如果你想要修改原版机制,或者需要对原版内容动手动脚,你需要学习Mixin和AT的知识;

    到后面,每个开发者的情况都会依需求而有所不同:

    1. 如果你厌烦了某些重复编码,你可能需要学习注解和注解处理器的知识;
    2. 如果一个单一平铺的项目不能满足你的需求,你可能需要学习Gradle的有关知识,而不是只会简单地改两个参数就完事;
    3. 如果你想要写一个跨Forge/Fabric的mod,你可能需要学习Architectury的使用;
    4. 如果你想要单个文件内跨版本而不是疯狂切分支,你可能需要学习Manifold编译器插件的使用;
    5. 如果你想要更加酷炫的视觉展示,你可能需要学习图形学知识,OpenGL的知识,以及使用GLSL编写shader;
    6. ......

    随着开发的逐渐深入,你可能会需要各式各样的黑白魔法来推进你的工作。

    你可以在开发某个功能之前寻找有没有类似的mod可供借鉴,查看并反思别人的实现方式优劣,这可能会使你少走一些弯路;

  2. 另一个很少被提及的锦上添花的条件是:数学。只需要一些初等数学,你的某些实现逻辑就可能得到不小的效果提升或是性能优化;用到大学数学的时候相对很少,但仍然可能对你的算法有帮助。

题外话:直到什么时候,你就知道自己已经入门了,成为了合格的MC开发者?

我的答案很简单:当你知道自己不知道什么的时候,恭喜你。

游戏版本

这篇教程混杂有多个版本的内容,总体以Minecraft 1.18.2及其对应的Forge 40.x为起点。

2023年3月20日更新:参照Teacon 2023的规定,即日起教程内容将以Minecraft 1.19.4及其对应的Forge 45.x为主。

在此之后,已经编写的部分若需变化将会以这样的选项卡补丁形式呈现。适用于新版本的原有内容不会被修改,新编写的部分也不会有提示,你可以默认它们适用于新版本。

显然我不一定能全部找到需要打补丁的部分——所以如果教程中提到的某个方法找不到或者不对劲,那就有可能是漏掉了。

至于你应该选择什么游戏版本?一个比较普遍的建议是:选择落后最新版本一个大版本的版本。比如麻将正在发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被标红了,这是因为这个包暂时不存在。在项目目录中新建此包即可。然后在在这个项目栏的右上角打开设置,如图所示取消勾选。

0-1

此时,你的文件结构应该是这样的:

0-2

现在,你可以运行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插件,负责帮助设置开发环境相关的东西。

NeoForge

在2023年7月13号——这个在Minecraft未来的历史中将被铭记的伟大的日子——原Forge团队中的绝大部分人出走,在Forge的技术基础上建立了NeoForge项目。

截至2024年1月20日,甩去包袱后的NeoForge仍然在持续地进行大量更新,推出了许多对开发者来讲十分便利的新功能。虽然目前还不太稳定,但我仍然建议你在编写1.20.4的项目时使用NeoForge。

Parchment

正如上文所讲,MC源码是经过混淆而成的天书,我们需要想办法把无意义的名称重新反混淆为有意义的名称。而负责储存这个映射关系的,就是混淆表。

你可能已经注意到了,我们一建立项目,就把mappings channeloffical改成了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名getCollisionBoxmcp名中的类名和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 channeloffical,简称moj表或官表。由此mcp表便失去了大部分存在的意义,被交由社区维护,这也进一步促使一部分开发者便逐渐向moj表转移。

但是moj表只包含类名、方法名等的映射,不包含形参/变量名,对开发者并不足够友好。于是为了补充形参/变量名的反混淆,Parchment出现了,它对应的便是parchment。你可能会在源码中见到有些参数前加有一个“p”,这是Parchment为了与moj表区别所加。

Moj in everywhere

1.20.4的NeoForge彻底取消了SRG作为中间名的存在,实行moj in everywhere的政策。

这意味着你需要在看到本教程后续章节中提及SRG名时自动地在脑子里转换为Moj名称。

Level

Level类,mcp表称world,顾名思义是一个世界。一个服务器上一般至少有三个世界:主世界、下界和末地。当你在其中一个世界玩耍时,服务器会将你所处的世界的必要信息,例如其他玩家、生物、区块数据等,发送给你的客户端,这样你才能看到游戏画面。由此,Level显然会有两个子类:ServerLevelClientLevel,分别主要负责逻辑运算和画面渲染。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的包来开放接口,诸如此类。

0-3.png

GameRule——传火注意!

了解一些历史的读者大概很容易就能看出这一条从何而来。

请牢记游戏中GameRule涉及到的相应选项。当mod的行为可能会涉及到有关内容时,按需添加相应的判定。以下是一些比较常见的条目:

GameRule名称含义或涉及到的情况
doFireTick火的蔓延、自然熄灭
doEntityDrops非生物实体掉落物品
drowningDamage玩家窒息伤害
fallDamage玩家跌落伤害
fireDamage玩家火焰伤害
freezeDamage玩家冰冻伤害
pvp玩家之间的伤害

一个简单的例子:如果你想要让篝火能够意外引燃周围的方块,你应该在引燃前如此查询:level.getGameRules().getBoolean(GameRules.RULE_DOFIRETICK),就像原版的火和岩浆一样,而不是直接引燃方块。当然,如果是陨石砸地引起或炸弹爆炸的火焰,你或许就不需要这样判定。请以实际情况为准。