Forge 导论
0.1 Forge 历史与定义
-
Minecraft
其中发行版的一个大类是由Java
写的商业软件,这意味着:Minecraft
容易反编译和修改:由于Java
半解释型语言的特性(但凡换成 C++ 就很可能不会有今天 Mod 丰富的生态了,毕竟 Mojang 当年一直不给官方 API);- 代码本身是闭源的、被混淆的:这毕竟是一款商业软件。
-
为了给
Minecraft
增添更多的游戏特性,大家千方百计地寻找添加代码的办法。最终MCP
(Mod Coder Pack)项目诞生了,它规避了没有官方 API 的问题,通过反编译、反混淆直接修改Minecraft jar
包中的内容。其发展过程中,人们研究各类的名称的产生如下:-
notch
名:Minecraft
各种类直接反编译、反混淆之后的名称,通常是无意义的字母数字组合。例如
j
就是一个典型的notch
名; -
srg
名:与notch
名一一对应。极大的好处是在一个版本里是不会变动的,这意味着类名渐渐可读起来,有相应的前缀后缀来区分。之所以叫做srg名
,是为了纪念MCP项目开发的领导者Searge;例如
notch
名j
对应srg
名的func_70114_g
。 -
mcp
名:这是当年MCP
项目的 mod 开发者使用的最多的名称。在mcp
名中,代码已经是可读的了。和我们正常写java程序中的名称没什么两样,但是也会变动。例如,
notch
名为j
的函数,其srg
名为func_70114_g
,其mcp
名是getCollisionBox
;
-
-
随着时间推移和生态的扩展,大家发现这么做很不行,因为直接修改 Jar 文件写 mod 的方式太过于粗暴了,而且 Mod 和 Mod 之间的兼容性可以说基本没有。于是,
Forge
项目就诞生了。 -
Forge
是通过修改Minecraft
方式实现的第三方的 API,给广大 mod 开发者提供了标准化的接口的指引。而Forge本身也在Minecraft
1.13 版本到来之后经历了一次重写,引入了大量函数式编程的API。随着时间的发展,
MCP
项目现在已经死亡了,除了Forge
这套API,Fabric
也风头正盛。 -
Forge
的工作原理也采用了MCP
的思路。 在为指定版本的Minecraft
安装完Forge
之后,游戏的运行过程中,所有的内容都会反编译成srg
运行,你编译好的 mod 同样也会被混淆成srg
,保证它可以正常运行。用
srg
名就是因为它每个版本不变。
0.2 Minecraft 的架构
除了了解 Forge
的历史和定义,Minecraft
的架构和运作方式在 Mod 开发中也绝对是必要的。
-
Minecraft
是一种 C/S 架构,整体逻辑如下:- 服务端:负责游戏的逻辑,数据的读写。
- 客户端:接受用户的输入输出,根据来自服务端的数据来渲染游戏画面。
-
Tips 1. 这里客户端和服务端的区分仅是逻辑上的区分。
- 实际上如果你处于单人模式,那么你的电脑上会同时存在服务端和客户端,而且他们处于不同的线程(
Server thread
&Render thread
); - 但是当你连接某个服务器时,你的电脑上只存在客户端,服务端被转移到了远程的一台服务器上。
- 实际上如果你处于单人模式,那么你的电脑上会同时存在服务端和客户端,而且他们处于不同的线程(
-
Tips 2. 客户端、服务端各存在一份数据模型。不过「客户端数据模型」只是「服务端数据模型」一个副本,虽然它们都有独立的游戏
Tick
,也共享很多相同的代码,但是最终逻辑还是以服务端为准。 -
Tips3. 客户端和服务端是存在于不同线程的,所以它们不可避免地需要同步数 据。而数据同步都是通过网络数据包实现的。
在大部分时候原版已经实现好了数据同步的方法,我们只需要调用已经实现好的方法就行。
但是在某些情况下,原版没有实现对应的功能,或者不适合使用原版提供的功能,我们就得自己创建和发送网络数据包来完成数据的同步。【可能需要计算机网络基础、
Java
网络编程基础】 -
在代码中,区分服务器端和客户端的方式:
World
中有一个isRemote
字段,开发时判断它就行。
0.3 Minecraft 的运行模式 & 总线
- 离散事件驱动模式,详见数据结构书籍。这个模式包含了 3 个概念:
- 事件:“当方块被破坏” 这个就是一个事件,“当玩家死亡” 这个也是一个事件,甚至 “当渲染模型时” 这个也是一个事件;
- 事件处理器:用来处理 “事件” 的函数。例如可以注册一个事件处理器来处理 “玩家死亡事件”,里面的内容是 “放置一个墓碑”;
- 总线:总线是连接 “事件” 和 “事件处理器” 的工具,当 “事件” 发生的时候,“事件” 的信息将会被发送到总线上,然后总线会选择监听了这个 “事件” 的 “事件处理器”,执行这个事件处理器。
- 在Minecraft中,所写的逻辑基本上都是事件处理。
- 在Forge开发里有两条总线,
Mod
总线和Forge
总线,所有和初始化相关的事件都是在Mod
总线内,其他所有事件都在Forge
总线内。
0.4 重要概念准备
-
注册:如果想往
Minecraft
里添加一些内容,那么你必须做的一件事就是注册。注册是一种机制,告诉游戏本身,有哪东西可以使用。你注册时需要的东西基本上可以分成两个部分:一个注册名和一个实例;更准确地说,是一个对象注册名(唯一标识这个对象的字符串),以及一个创建这个类实例的 Java
-
资源地址(
ResourceLocation
):Minecraft
管理、定位资源(音频 / 图片)的方式是采用特殊格式的字符串。格式为:<domain>:<UNIX-Style relative path>
。- 域可以是
minecraft
(原版资源),也可以是 mod 的名称。相对路径是相对于 mod 根目录下的assets
目录而言(如果是原版资源,即域名为minecraft
,那么相对于.minecraft/assets
); - 例如:
minecraft:textures/block/stone.png
,mod1:textures/Alex.png
;
- 域可以是
-
模型和材质:在游戏中
3D
的对象基本上都有它的模型,模型和材质组合在一起规定了一个对象具体的样子。模型相当于是骨头,材质相当于是皮肤。在大部分时候,你的材质都是 png 图片【可能需要平面设计 和 PS 的相关功底】。注意保证材质背景是不透明的,也不要在材质中使用半透明像素,会有不可预知的问题。
0.5 开发环境
Minecraft Forge
是由Gradle
管理的项目,而Forge
官方写了一个叫做ForgeGradle
(以后简称 FG)的插件来负责整个 mod 开发环境的配置,本节主要介绍这个环境的配置和使用;
0.5.1 前提
JDK 8 or 11
(1.16.x 兼容版本)和64 bit JVM
;请确保您的操作系统已经安装并配置好环境变量JAVA_HOME
、CLASS_PATH
;- 官网获得
MDK(Mod Development Kit)
:我们这里 1.16.5 选择官网推荐的forge-36.2.42
Downloads for Minecraft Forge for Minecraft 1.16.5; - Java 开发 IDE,可以选 VSCode / Eclipse / IDEA,本文以 IDEA 为例进行。
0.5.2 开发准备工作
-
这里我们将下载的 MDK 解压到一个空目录下。以后这里是 mod 项目的根目录了;
FG 项目结构:
├─ .gitattributes # Git 用来记录某些文件属性
├─ .gitignore # Git 用来忽略版本控制的记录文件
├─ build.gradle # Gradle 构建脚本
├─ changelog.txt # Forge 项目版本迭代情况
├─ CREDITS.txt # Forge 项目致谢和 credits
├─ settings.gradle # Gradle 插件配置文件
├─ gradle.properties # Gradle 属性文件,用来定义其他变量和设置
├─ gradlew # Unix 类系统执行 Gradle wrapper 的 shell
├─ gradlew.bat # Windows 系统 ~
├─ LICENSE.txt # Forge 项目证书
├─ README.txt # 基本安装指导说明书
│
├─ gradle/
│ └─wrapper/ # 这里包含了 Gradle Wrapper,这里使用的是 7.4.2
│ ├─ gradle-wrapper.jar
│ └─ gradle-wrapper.properties
│
├─ run/ # 在构建项目后会出现,相当于 .minecraft
|
└─src/ # 源文件目录
└─main/ # main 分组的源文件目录
├─java/ # main 分组的 java 源文件
│ # 这里是 java 的 package,将来在这里写 mod
│
└─resources/ # main 分组的资源目录
├─ pack.mcmeta # 被 minecraft 用来识别数据和资源包的文件
│
└─META-INF/ # Forge 资源 metadata 信息文件存放目录
└─ mods.toml # mod 声明的文件其中最为重要的几个分别为:
build.gradle
、settings.gradle
、gradle.properties
、gradlew.bat
、gradlew
、gradle/
目录(Java Gradle 项目构建系统,类似 C++ 的 CMake);
关于资源目录,我们在后文会详细介绍,这里先熟悉一下。
-
用 IDEA 打开这个新目录,即可自动下载依赖配件、设置项目;如果中途出现错误,99% 是因为网络错误。请竖起梯子,或者上网找国内源解决;
-
打开后,在 IDEA Terminal 下执行
gradlew genIntellijRuns
(如果是 eclipse/vscode,那么后面一个词分别是genEclipseRuns
和genVSCodeRuns
),它会进一步下载游戏需要的资源,并且设置Run Configure
;
0.5.3 项目构建脚本 build.gradle
[!Tip]
如果你对 Java 语言和 Gradle 了解不多,你可以按需跳过这节。
但是如果你想要诸如 “其他 mod 联动”、“多 mod 兼容”、“附属 mod”、“Jar 依赖” 的特性,还是需要看一下这节。
先看第一段:
plugins {
id 'eclipse'
id 'idea'
id 'maven-publish'
id 'net.minecraftforge.gradle' version '[6.0,6.2)'
}
version = mod_version
group = mod_group_id
base {
archivesName = mod_id
}
// Mojang ships Java 17 to end users in 1.18+, so your mod should target Java 17.
java.toolchain.languageVersion = JavaLanguageVersion.of(8)
println "Java: ${System.getProperty 'java.version'}, JVM: ${System.getProperty 'java.vm.version'} (${System.getProperty 'java.vendor'}), Arch: ${System.getProperty 'os.arch'}"
上面的内容是准备 Gradle 构建环境、引入 Forge 和 Minecraft 开发的辅助插件(参见 settings.gradle
),包括预制的构建任务等等。除了某些教程 明确要求(例如 parchment mapping),否则你不应该修改这块内容。
配置文件的下面是 minecraft
闭包,配置中的注释介绍的非常清楚,这里不再赘述。本文只是介绍一下,我们可以在这个闭包中**修改开发时 Gradle Task 的启动参数**(例如 client/server/data
构建任务执行时的命令行参数)。
接下来:
// 添加配置工程的资源存放目录(这里通常是 data generators 生成的),最终打包时会汇总到 JAR 包的对应位置被游戏/你的 mod 识别
sourceSets.main.resources { srcDir 'src/generated/resources' }
然后,在 repositories {}
闭包中,你可以:
-
添加 Maven Repositories(依赖位于的仓库。一般情况下其实不需要添加,因为比较常见的库都在 Maven 主仓 / Forge 主仓中,而主仓已经在 ForgeGradle 插件配置时引入了);
-
添加自己的依赖仓库的位置。这通常是本地目录,如果你有些不需要引用源码的 JAR 包依赖,则可以把它们统一放到一个目录中,然后在这个闭包中将这个目录告诉 Gradle,这样在
dependecies
闭包中引用自己的 JAR 包就能找到了(有点像 CMake 的target_include_directories
);举个例子:repositories {
flatDir {
dir 'libs'
}
}
在 dependencies {}
闭包中,我们可以引用在 Maven 主仓 / Forge 主仓 / repositories
引入的依赖库。默认一定需要引入 minecraft
。
一般我们有几种情况需要添加依赖:例如开发时的工具库 (e.g. moonlight library)、mod 联动,等等。
举个例子,如果我需要和 JEI 联动,那么我们就要引入 JEI 的开发 API 和运行时依赖,这时就需要添加下面的配置:
compileOnly fg.deobf("mezz.jei:jei-${mc_version}:${jei_version}:api") // Adds JEI API as a compile dependency
runtimeOnly fg.deobf("mezz.jei:jei-${mc_version}:${jei_version}") // Adds the full JEI mod as a runtime dependency
另外,如果我需要引入之前在 repositories
中指定的本地依赖,则需要使用 blank
作为 group ID:
// 假设我们在 ./libs/ 中有一个 coolmod-${mc_version}:${coolmod_version}.jar
// 然后我们前面在 repositories 中引入了 libs 目录(./libs),这里就能找到
implementation fg.deobf("blank:coolmod-${mc_version}:${coolmod_version}")
其中 deobf
是 ForgeGradle 帮我们准备的工具函数,可以对指定的 JAR 文件反混淆,帮助我们进行 Mod 开发和 代码 Hint;
关于 dependencies
的依赖配置,更多的使用场景和使用方法,可以参见下面的 Gradle 官方文档:
综上,如果你是有 Java 开发经历的开发者,并且使用过 Gradle,那么这里的阅读难度并不大。
再下面的内容就是在构建时将 gradle.properties
中的环境值替换到 META-INF/mods.toml
、pack.mcmeta
这样的文件中,实现配置的统一管理。具体设置请参见下节。
0.5.4 项目环境配置文件 gradle.properties
Forge 36.2.42 已经将几乎所有需要配置的信息都集中在这个文件中,我们按照注释和变量名称填写设计就好了!
- 将 “examplemod” 都替换为自己 mod id(自己定,唯一,只能有小写字母,不能有大写/空格/其他字符,切记!!!);
0.6 项目结构
-
和普通 Java 项目一样,设定好 top level package,然后为 mod 起一个唯一的名字;比如,我的 top level package 名叫做
com.test
,然后我起个包名helloMC
,于是叫com.test.helloMC
;还是补充一下,如果以后做 Java Web,top level package 的名字需要是自己有的域名前缀,例如我有
xxx.org
,那么为这个域名开发 Java Web 服务的规范就是 top level packageorg.xxx
; -
mods.toml
文件设计:上面说过,这个文件定义了 mod 的元信息(metadata),可以被 mod 使用者在游戏添加 mod 的界面看到;-
一个信息文件能够描述多个 mod;
-
mods.toml
的语言是TOML
(可以理解为和YAML
差不多的东西,语法不一样),必须要被存放于src/main/resources/META-INF/
下; -
在 Forge 36.2.42 中,配置信息都已经由环境变量的形式管理在
gradle.properties
中,这里无需配置。不过还有些额外的配置(例如displayURL
/credits
)你可以在这个 TOML 文件中修改;
-
-
@Mod
annotation:在编写 mod 中,这个标识是用来提示Forge Mod Loader
,这个类是一个 Mod entry point;另外,这个标识的值需要是modid
(在gradle.properties
中配置过); -
一个建议(不强制):比起将源文件散落在文件夹中,使用
sub-packages
的结构可读性更强;例如像物体(items)、方块(blocks)、数据实体(tile entities,或者其他 sub-packages)应该放在common
包,而屏幕(Screens)、渲染器(Renderers)应该放在client
下;这里补充一下什么是
tile entities
,后面用到详细说。Tile Entities就像简化的实体一样,绑定到Block上。 它们用于存储动态数据, 执行基于tick的任务以及动态渲染。
原本 Minecraft的一些例子是:处理库存(箱子),熔炉上的冶炼逻辑或信标的区域效应。 mod中存在更高级的示例,例如采石场,分拣机,管道和显示器。
注意:不要滥用!如果使用不当会导致卡顿;
-
类名命名规范:在您创建的类名之后加上它们的父类名(即是什么),可以让读者更能理解这个类在干什么;
这其实是所有语言、所有场景开发的共同的规范。
例如,一个自定义物品
PowerRing
,它的类继承于Item
,因此最好定为PowerRingItem
;再如一个自定义方块
NotDirt
,继承于Block
,因此命名为NotDirtBlock
; -
forge 项目的构建和测试;
-
当 mod 开发结束后,根目录运行
gradlew build
,会向build/libs
中构建[archivesBaseName]-[version].jar
,您可以直接将这个包放在安装了 forge 的 minecraft 游戏的mod
目录中,即可加载; -
当然,每写一次就要加入游戏目录的操作不现实。所以测试中,可以使用
Run Configure
来运行测试的 Minecraft 服务器 + 客户端,这时会加入开发目录(之前说过,src/main/java
)的所有 mod jar 包;具体命令:启动服务器
gradlew runServer
,自动绑定在localhost
的指定端口;启动客户端gradlew runClient
;
-
0.7 Mod 更新系统
Forge 提供了一个可选的、轻量级的更新检查的框架,在作者提交更新后,使用 mod 的用户会在游戏中 mod 管理的按钮上看到更新,并且会写入 changelogs.txt
,但不会自动下载升级;
为了集成这个功能,只需设置上面 mods.toml
的可选参数 updateJSONURL
,这个 URL 可以指向您提供 “update json” 的网站服务器或者 github 上(只要别人能访问到);
而这个 “update json” 的格式为:
{
"homepage": "<homepage/download page for your mod>",
"<mcversion>": {
"<modversion>": "<changelog for this version>",
// List all versions of your mod for the given Minecraft version, along with their changelogs
...
},
"promos": {
"<mcversion>-latest": "<modversion>",
// Declare the latest "bleeding-edge" version of your mod for the given Minecraft version
"<mcversion>-recommended": "<modversion>",
// Declare the latest "stable" version of your mod for the given Minecraft version
...
}
}
值得注意的是:
homepage
的地址在使用者的 mod 需要更新时会显示出来,注意隐私;- Forge 使用一套内置算法来判断当前版本和 update JSON 的版本哪个更 新,大多数情况下应该没问题,如果有疑惑可以查阅
ComparableVersion
类或 Semantic Versioning; - 上面的
changelog
字符串可以使用\n
,也可以给用户提供一个网站让他们在网站上详细看;
0.8 Mod 进阶调试
Minecraft 自身提供了一个 Debug Profiler,能够分析出耗时的代码块,这对于 mod 开发者和服务器管理员非常有用;
开始分析命令:/debug start
,结束分析命令:/debug end
;
-
建议最少给 Debug Profiler 的分析留出 1 min 时间,时间越多,分析约准确;
-
要分析的实体(Entities)需要在当前世界中存在,不然分析不到它;
在结束分析后,会自动生成 profile-results-yyyy-mm-dd_hh.mi.ss.txt
,
文件格式:<the depth of the section> | <the name of the section> - <the percentage of time it took in relation to it’s parent>/<how much time it took from the entire tick>
0.9 第一个 Mod:跑通流程
上面说了很多内容,现在让我们以一个没有内容的测试 mod 来实际跑一遍:
step 1. 在 src/main/java
下创建一个包,例如 com.test
,创建 Java 类 HelloMC
;
step 2. 根据 0.6 节中介绍的 modid,需要用 @Mod(<modid>)
修饰 mod 的 entry point 类,保持:@Mod 修饰值、mods.toml
的 modid
、build.gradle
的 examplemod
三者一致。因此,我们需要给 HelloMC
加 @Mod()
annotation。为了方便,我们另外创建一个类,专门存储全局变量,就定为 Utils
(你也可以不用,看自己的编码习惯):
// File: HelloMC.java
package com.test.HelloMC;
import net.minecraftforge.fml.common.Mod;
@Mod(Utils.MOD_ID)
public class HelloMC {
// 这里先空着
}
// File: Utils.java
package com.test.Utils;
public class Utils {
public static final String MOD_ID = "mymod"; // 自己写 modid
}
step 3. 现在去写 gradle.properties
,注意填写 mod_id
:如果需要的话可以补充一下 mod.toml
的可选配置。修改个性化内容,然后构建!
step 4. 启动 IDEA 上的任务设置 “RunClient”,进入游戏查看自己的 mod 是否已经显示!
0.10 事件系统
早在 0.3 节说明运行模式的时候,我们提到了 Mod
总线和 Forge
总线,这里在开发前必须要说清楚。Forge 自己的事件系统内是独立于 Minecraft 的事件系统的。
使用 Forge 事件系统的方法有 2 种,先看个例子:
public class TestEventHandler {
@SubscribeEvent
public void pickupEvent(EntityItemPickupEvent event) {
System.out.println("Item picked up!");
}
}
这里定义了一个类 TestEventHandler
,里面有个实例方法 pickupEvent()
。注意 @SubscribeEvent
标记,它的作用就是指示下方的方法为事件处理器,而它监听的事件类型由它的参数决定(EntityItemPickupEvent
,实体捡起物品这个事件)。
但是只是让他声明为事件处理器还不够,还需要在合适的位置将含有事件处理器的类实例化,并注入事件总线中。是 Forge
总线还是 Mod
总线?
-
Mod
总线:负责游戏的生命周期事件,也就是初始化过程的事件;注册方法:FMLJavaModLoadingContext.get().getModEventBus().register(<subscribed_event_obj>)
; -
Forge
总线:负责的就是除了生命周期事件外的所有事件;注册方法:MinecraftForge.EVENT_BUS.register(<subscribed_event_obj>)
;
显然,上面 “实体捡起物品” 的事件在初始化过程之外,所以加入 Forge
总线:
MinecraftForge.EVENT_BUS.register(new TestEventHandler());
显然这样注册的方法比较麻烦,被称为 “实例注册方式”。还有一种:
@Mod.EventBusSubscriber(modid = "mymod", bus = Bus.FORGE, value = Dist.CLIENT)
public class MyStaticClientOnlyEventHandler {
@SubscribeEvent
public static void drawLast(RenderWorldLastEvent event) {
System.out.println("Drawing!");
}
}
不管传给注解的参数(都是可选的),我们发现,这个类中的事件处理器是静态的(另注:由于是渲染事件,所以仅客户端)。这里,我们就可以在整个类前加上注解 @Mod.EventBusSubscriber([modid, bus, value])
,表示将类中的所有事件处理器 都加入指定 modid、总线、端中。
这里补充,如果是 Mod
总线,那么 bus = Bus.MOD
,更多信息可以转到 @Mod.EventBusSubscriber()
的源码查看。
更多事件、Mod 声明周期相关的信息,我们将在有这方面需求之后再介绍(毕竟如果一口气全讨论完但是不用,那很快就会忘记)。