MC-Forge开发笔记(一)
written by SJTU-XHW
Reference: Minecraft Forge Doc 1.16.x、Boson 1.16(导论部分)
注意:以 MC 版本 1.16.5 为例(同一大版本 1.16.x 的操作方式变化不大)
本人知识浅薄,文章难免有问题或缺漏,欢迎批评指正!
Chapter 0. 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
里添加一些内容,那么你必须做的一件事就是注册。注册是一种机制,告诉游戏本身,有哪东西可以使用。你注册时需要的东西基本上可以分成两个部分:一个注册名和一个实例;资源地址(
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 开发环境的配置,本节主要介绍这个环境的配置和使用;
前提
JDK 8
(1.16.x 兼容版本)和64 bit JVM
;请确保您的操作系统已经安装并配置好环境变量JAVA_HOME
、CLASS_PATH
;- 官网获得
MDK(Mod Development Kit)
:我们这里 1.16.5 选择官网推荐的forge-36.2.34
Downloads for Minecraft Forge for Minecraft 1.16.5; - Java 开发 IDE,可以选 VSCode / Eclipse / IDEA,本文以 IDEA 为例进行。
开发准备工作
这里我们将下载的 MDK 解压到一个空目录下;
FG 项目结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28├─ .gitattributes # Git 用来记录某些文件属性
├─ .gitignore # Git 用来忽略版本控制的记录文件
├─ build.gradle # Gradle 构建脚本
├─ changelog.txt # Forge 项目版本迭代情况
├─ CREDITS.txt # Forge 项目致谢和 credits
├─ 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
、gradlew.bat
、gradlew
、gradle/
目录;
只要有了上面几个的数据,就能创建一个新
mod
项目;把上面说的重要的几个文件复制到新目录,以后这里是 mod 项目的根目录了(这么做是为了减少文件干扰,毕竟我们不需要版本控制,对吧);
用 IDEA 打开这个新目录,即可自动下载依赖配件、设置项目;如果中途出现错误,99% 是因为网络错误。请翻*GFW,或者上网找国内源解决;
打开后,在 IDEA Terminal 下执行
gradlew genIntellijRuns
(如果是 eclipse/vscode,那么后面一个词分别是genEclipseRuns
和genVSCodeRuns
),它会进一步下载游戏需要的资源,并且设置Run Configure
;
mod 的 gradle 脚本个性化:刚入门,介绍简单的个性化方式,主要是编辑
build.gradle
的内容。注意,在不知道含义的情况下,不能编辑其中的
buildscript {}
块。这些代码对于ForgeGradle
来说是必须的;更改构建的结果文件名:编辑
archivesBaseName
的值;更改
maven
构建系统的根目录:编辑group
的值;更改 mod 版本信息:编辑
version
的值;将其中所有 “examplemod” 都替换为自己 mod id(自己定,唯一,只能有小写字母,不能有大写/空格/其他字符,切记!!!);
设置项目名称映射(mappings):在
minecraft{}
块中,第一个就是mappings
,默认官方的 mappings。但是,官方的没有参数和javadocs
提示。如果之后用的不习惯,可以换成以前MCP
的 mappings(现在已经停止维护):1
2
3
4minecraft {
// 这是最后一次 mappings 的更新
mappings channel: 'snapshot', version: '20210309-1.16.5'
}
注意:修改完
build.gradle
脚本后需要重新进行前面的步骤进行设置;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.6 Mod 结构设计
和普通 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/
下;一个
mods.toml
最基本的模板如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader="javafml"
# A version range to match for said mod loader - for regular FML @Mod it will be the forge version
# Forge for 1.16.5 is version 36
loaderVersion="[36,)"
# The license for your mod. This is mandatory and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license="All Rights Reserved"
# A URL to refer people to when problems occur with this mod
issueTrackerURL="github.com/MinecraftForge/MinecraftForge/issues"
# If the mods defined in this file should show as separate resource packs
showAsResourcePack=false
[[mods]]
modId="examplemod"
version="1.0.0.0"
displayName="Example Mod"
updateJSONURL="minecraftforge.net/versions.json"
displayURL="minecraftforge.net"
logoFile="logo.png"
credits="I'd like to thank my mother and father."
authors="Author"
description='''
Lets you craft dirt into diamonds. This is a traditional mod that has existed for eons. It is ancient. The holy Notch created it. Jeb rainbowfied it. Dinnerbone made it upside down. Etc.
'''
[[dependencies.examplemod]]
modId="forge"
mandatory=true
versionRange="[36,)"
ordering="NONE"
side="BOTH"
[[dependencies.examplemod]]
modId="minecraft"
mandatory=true
versionRange="[1.16.5,1.17)"
ordering="NONE"
side="BOTH"以下是必须要填写的信息列表:
Property Type Default Description modid string mandatory The modid this file is linked to (also the modid in build.gradle). version string mandatory The version of the mod. It should be just numbers separated by dots, ideally conforming to Forge’s Semantic Versioning structure. displayName string mandatory The user-friendly name of this mod. updateJSONURL string "" The URL to a version JSON. displayURL string "" A link to the mod’s homepage. logoFile string "" The filename of the mod’s logo. It must be placed in the root resource folder, not in a subfolder. credits string "" A string that contains any acknowledgements you want to mention. authors string "" The authors of this mod. description string mandatory A description of this mod. dependencies [list] [] A list of dependencies of this mod.
@Mod
annotation:在编写 mod 中,这个标识是用来提示Forge Mod Loader
,这个类是一个 Mod entry point;另外,这个标识的值需要是modid
(在build.gradle
和mods.toml
都写过);一个建议(不强制):比起将源文件散落在文件夹中,使用
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
;
0.7 Mod 更新系统
Forge 提供了一个可选的、轻量级的更新检查的框架,在作者提交更新后,使用 mod 的用户会在游戏中 mod 管理的按钮上看到更新,并且会写入 changelogs.txt
,但不会自动下载升级;
为了集成这个功能,只需设置上面 mods.toml
的可选参数 updateJSONURL
,这个 URL 可以指向您提供 “update json” 的网站服务器或者 github 上(只要别人能访问到);
而这个 “update json” 的格式为:
1 | { |
值得注意的是:
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
(你也可以不用,看自己的编码习惯):
1 | // File: HelloMC.java |
1 | // File: Utils.java |
step 3. 现在去写 mods.toml
,注意填写 mod_id
:
1 | # 上面使用的 @Mod 就在这个 modLoader 中,和 loaderVersion 不用改 |
step 4. 再到 build.gradle
中,将所有 examplemod
换成自己的 modid,修改个性化内容,然后构建!
step 5. 启动 IDEA 上的任务设置 “RunClient”,进入游戏查看自己的 mod 是否已经显示!
0.10 事件系统
早在 0.3 节说明运行模式的时候,我们提到了 Mod
总线和 Forge
总线,这里在开发前必须要说清楚。Forge 自己的事件系统内是独立于 Minecraft 的事件系统的。
使用 Forge 事件系统的方法有 2 种,先看个例子:
1 | public class TestEventHandler { |
这里定义了一个类 TestEventHandler
,里面有个实例方法 pickupEvent()
。注意 @SubscribeEvent
标记,它的作用就是指示下方的方法为事件处理器,而它监听的事件类型由它的参数决定(EntityItemPickupEvent
,实体捡起物品这个事件)。
但是只是让他声明为事件处理器还不够,还需要在合适的位置将含有事件处理器的类实例化,并注入事件总线中。是 Forge
总线还是 Mod
总线?
Mod
总线:负责游戏的生命周期事件,也就是初始化过程的事件;注册方法:FMLJavaModLoadingContext.get().getModEventBus().register(<subscribed_event_obj>)
;Forge
总线:负责的就是除了生命周期事件外的所有事件;注册方法:MinecraftForge.EVENT_BUS.register(<subscribed_event_obj>)
;
显然,上面 “实体捡起物品” 的事件在初始化过程之外,所以加入 Forge
总线:
1 | MinecraftForge.EVENT_BUS.register(new TestEventHandler()); |
显然这样注册的方法比较麻烦,被称为 “实例注册方式”。还有一种:
1 | "mymod", bus = Bus.FORGE, value = Dist.CLIENT) .EventBusSubscriber(modid = |
不管传给注解的参数(都是可选的),我们发现,这个类中的事件处理器是静态的(另注:由于是渲染事件,所以仅客户端)。这里,我们就可以在整个类前加上注解 @Mod.EventBusSubscriber([modid, bus, value])
,表示将类中的所有事件处理器都加入指定 modid、总线、端中。
这里补充,如果是 Mod
总线,那么 bus = Bus.MOD
,更多信息可以转到 @Mod.EventBusSubscriber()
的源码查看。
Chapter 1. 创建物品
1.1 第一个物品
本节了解,创建一个物品就 3 步【所有物品都是这样】:
- 创建自己的物品类,一般继承于原版的物品的类
Item
; - 在合适的位置实例化这个物品;
- 将此物品对象注册进游戏;
我们先以一个原版游戏中没有 “黑曜石锭” 为例:
1 | public class ObsidianIngot extends Item { |
这里我们就写了一个构造函数,向父类的构造函数传递了一个 Properties
类的对象,而这个 Properties
对象调用了 group()
,表示设置物品所在组为 Materials
组(杂项栏)。
传给父类的 Properties
对象的作用是规定了物品的一些属性,比如:是不是食物,或者这个物品在创造模式的哪一个物品栏。这里的 group()
方法就是设置了物品在创造模式的杂项栏。
如果不调用 group()
方法设置,那么这个物品就不会出现在创造模式物品栏,只能用 /give
获取;
然后是实例化 + 注册。现在的 Forge
引入了 DeferredRegister
的机制:
1 | public class ItemRegistry { |
先使用 DeferredRegister
泛型类的静态方法 create(<ForgeRegistries.TYPE>, <mod_id>)
向 mod_id
的 mod 创建了一个 DeferredRegister<Type>
对象,表示这是专门注册 Type
类型实体的类。
紧接着告诉这个对象要注册 “注册名为 ‘obsdian_ingot’(不能大写)、其对象构造方法为 ObsidianIngot::new
” 的一个类的 supplier
。
最后,由于这个 DeferredRegister
泛型类帮助我们注册物品需要依赖注入事件。显然注册是一种初始化的事件,所以将 DeferredRegister
对象注入 Mod
总线(我们以 0.9 中的项目为例):
1 |
|
运行后会发现新的、没有材质的物品出现在创造模式的物品栏中。
1.2 物品的模型和材质
一个物品被注册后,还应该指定它的模型和材质。这种指定方式也是固定的,记忆即可。下面以上面的 ObsidianIngot
为例。
第一步,在 src/main/resources
创建 assets
目录,表示这个 mod 存储资源的位置。并将目录补充为:
1 | resources/ |
一定要创建 pack.mcmeta
,这相当于为 mod 规定了资源包。只有这么做,MC 才能发现 assets
中的资源。格式也是 JSON
,模板:
1 | { |
第二步,在 models/item
的目录中创建 obsidian_ingot.json
,文件名需要和之前物品注册名相同。
item
(物品类)模型的 JSON 文件的模板如下:
1 | { |
在 textures/item
的目录中放入自己设计的材质 obsidian_ingot.png
(游戏中 1:1,仅贴图最好不要大于 32×32 像素),文件名也是物品注册名。
最后启动游戏就会发现游戏自动读取了模型和材质。
注:自己制作模型的网站戳 这里;
补充:BlockBench 笔记
- 绑骨:按照父级模型生长方向移动、旋转等操作,可以用作设计动画;
- 顶点识别;
- 隐藏的菜单:x/y/z 轴翻转;
- 缩放参数:可以在不改变材质的情况下改变模型大小,允许小数;
- 大纲视图的锁定:可以防止误操作;
- 背景图的作用大部分是用来拾色的;
- 后期添加模型过大,放不下空白材质时,可以为材质调整大小;
1.3 Item
类和 ItemStack
类
前面两节叙述了加入普通自定义物品(item
)的大致流程。现在深入一下更抽象的层面,考虑这些物品背后的类,以便我们进一步对它们进行自定义。
首先,很方便理解的是 Item
类,就是规定每种物品属性的类。细心的同学可能会提问,我们之前创建的 ObsidianIngot
这个类在游戏的框中显示的数量呢?一组应该是多少个呢?Item
类都没有定义。
这是因为,设计者考虑到 item
的有些信息不会因为数量、耐久等参数的改变而改变,例如:
- 使用/放置(左键/右键)的行为;
- 在客户端上显示的名称及 Tooltip;
- 合成配方;
- et cetera;
这些抽象的属性都在 Item
类中得到定义;而数量、是否有/有多少耐久等信息,就单独提出去打包为一个类,其名曰 ItemStack
。游戏中,玩家的物品栏、背包、手持物品等,都是 ItemStack
的实例。
所以,可以将 ItemStack
和 Item
的关系看作类的聚集,在对 ItemStack
可以调用 getItem()
实例方法来获得其中含有的 Item
对象。
享元
另外有个极其重要的概念——“享元(Flyweight Pattern)”,即一种特定物品只对应一个实例,而 Item
就是一个享元。这意味着,以 Apple
类为例,你在游戏里见到所有苹果都是 Apple
类的唯一实例。正因如此,我们在编写 mod 中,可以直接用 ==
(即 <ItemStack_obj>.getItem() == <Item_obj>
)来判断 Item
种类,不用担心 ==
比较的是地址。
Item.AIR
还有一件事,由于 ItemStack
是 non-null
的属性,永远是非空引用,所以 MC 中 “空的” 栏的 ItemStack
所含的 Item
对象是个特殊对象:Items.AIR
。判断 ItemStack
对应的栏目物品是否为空时,应该使用专用的实例函数 isEmpty()
,不能直接判断是否为 null
。
1. 4 自定义物品组
物品组可以形象地理解为创造模式物品栏的分栏。我们在最开始就提到的类 ItemGroup
,所以自定义物品组很简单——就写一个继承于 ItemGroup
类的子类就行。
这个继承的类只需要写两个地方,一个是构造函数,直接使用父类的即可,第一个参数是物品组的名称 String
。(这个组就以 “ObsidianGroup” 为例)
另一个是重写类里面的 createIcon()
实例方法(实际用途是创建物品栏的图标),要求返回一个 ItemStack
实例(毕竟需要将物品呈现在物品栏中),这个也很简单,用 ItemStack
构造函数构造个实例就行。ItemStack
构造函数的第一参数就是已注册的 Item
实例对象。
从哪获取已注册的
Item
实例?简单,之前我们注册完物品后,DeferredRegister::register
方法不是返回了一个RegisterObject<Item>
,它就是包裹了注册信息的Item
,直接用get()
实例方法就能返回已注册的Item
实例。
1 | public class ObsidianGroup extends ItemGroup { |
这个简单的物品组类就做好了。但是想要将它显示在游戏里,还要将它在合适的位置实例化。
我们再定义一个类,专门存放 mod 中实例化的 ItemGroup
的类,就叫 ModGroup
:
1 | public class ModGroup { |
这样在物品类初始化的时候,用 Properties::group()
实例方法将物品加入 og_group
这个实例即可。
1 | public class ObsidianIngot extends Item { |
1.5 食物
食物就是一种特殊的 Item
,也继承于 Item
来创建,只是比普通 Item
多几个属性(包括专门的类 Food
作为属性)。举个例子:
1 | public class ObsidianApple extends Item { |
为 Java 基础补充知识:
Java
Supplier
是一个功能接口,代表结果的提供者,存在于java.util.function
包。常用方法是get()
,源码如下:
1
2
3
4
public interface Supplier<T> {
T get();
}可以用来返回一个特定的结果对象。我们可以用
lambda
表达式,例如上面() -> new EffectInstance(Effects.POISON, 3*20, 1)
创建一个调用返回新EffectInstance
对象的匿名函数。这个格式得到的左值就是supplier
。也可以定义一个完整的匿名函数:
() -> { return XXX; }
其中,Food::Builder()
静态方法初始化 Food.Builder
类实例,用来在 Food
属性对象生成前设置好参数,支持 saturation()
、hunger()
、effect()
方法。
需要说明的是,其中 effect()
方法的第一参数是 Supplier<EffectInstance>
,第二参数是触发的概率。EffectInstance
类的初始化方法第一参数是 Effects
类的枚举量(含有 MC 中几乎所有效果),第二参数是效果持续的游戏 Tick 时间,第三参数是对应的药水等级。
一个游戏刻
Tick
就是主程序循环一次的时间,固定是0.05 s
;上面的3*20
就是 3 秒。
最后的 Food.Builder::build()
方法将 Food.Builder
及其中的设置构造为 Food
类的实例,可以在初始化物品时对 Properties
对象使用 food()
方法指定(和 group()
一样)。
接下来的物品注册、模型和材质都与普通 Item
相差无几。
1.6 近战武器 和 工具
MC 原版 1.16.5 的近战武器可以认为只有剑,如果想要自定义近战物品,就可以按剑的类的做法来做(入门不介绍攻击动画的制作,就复用剑的攻击动画)。
MC 原版 1.16.5 的工具就是耳熟能详几件:稿子、铲子、斧头、锄头。
首先,细心的同学可以发现,木头、石头、铁、黄金和钻石种类的武器面板基础数据(除去附魔等加成的效率、耐久、伤害……)是不同的,并且损坏后修复的材料不同。但它们都是 “剑 / 稿 / 斧 / 锄 / 铲” 这个属性,所以这些额外的数据可以抽象为一个单独的类 / 枚举类型来管理,在初始化时再传给 “剑 / 稿 / 斧 / 锄 / 铲” 这个类。在 MC 中,它就是枚举类型 ItemTier
,即 “工具等级”(Tiered adj. 阶梯式的、分层的)。
官方选择了接口类来设计 ItemTier
,而由于 Java 的枚举类型在能够大量定义常量的同时,允许添加方法,因此我们只需写一个枚举类来 implements
它。举个例子:
1 | public enum ModItemTier implements ItemTier { |
别问为什么要重新写属性并且 override
。因为这是个 enum
类型,不存在继承的说法,只能 implements
方法。
再来看 “剑 / 稿 / 斧 / 锄 / 铲” 类的继承关系:
1 | Item --> TieredItem (有工具等级的 item) --> SwordItem |
这里简单到爆炸,只需要掌握这些类的初始化方法就行,以剑为例:
1 | public SwordItem(ItemTier tier_obj, int, float, Properties prop); |
像这里以黑曜石剑为例:
1 | public class ObsidianSword extends SwordItem { |
SwordItem
构造函数的第一参数 ItemTier
后面的两个参数分别是攻击伤害、攻击速度;
之后的模型、材质、物品注册也是完全和普通 item
相同。
稿子、铲子、斧头、锄头也一样。
注意:以上数据面板理论上只要不溢出都可以自行设置。但如果想要写成好的 Mod,这边建议自己试试平衡性,再调整数值。
你问为什么不介绍 “弓” 这种远程武器?这大概是因为 minecraft java 到处是高耦合度的代码(尤其是弓这个动作复杂的 item),哪怕仅修改拉弓速度都要引入 7~8 个包,故不再这里叙述。实在想要改,建议直接改材质 + 附魔;
1.7 可穿戴装备
和近战武器/工具类似,可穿戴装备也有 “工具等级” 来管理和装备位置无关的数据,这就是 IArmorMaterial
接口类,我们模仿 1.6 中的 ModItemTier
用一个枚举类来自定义:
1 | public enum ObsidianArmorMaterial implements IArmorMaterial { |
接着,和其他 item 一样设置可穿戴装备对应的对象,并且注册。
但是!一般情况我们无需继承不同的类,只需要创建 ArmorItem
类的不同实例,就能获得不同部位的 item 对象。这多半是因为可穿戴装备是盔甲,都设计成一套,例如注册时:
1 | // 回忆一下,item_reg 是之前在 ItemRegistry 类中定义的 DeferredRegister 类的实例 |
现在传给 DeferredRegister<Item>::register()
的第二参数不再是类自带的 new
方法,而是临时写的匿名函数,利用已存在的类 ArmorItem
的构造方法:
1 | // 第一参数是之前定义的 可穿戴装备的“等级枚举类” |
现在最后一件事需要注意,就是盔甲的材质添加和其他 item 不一样。
首先从前面共用一个类可以看出,这里盔甲的材质也在一个图中。说 “一个图” 不是很准确,准确来说是穿着状态的盔甲全套一组图,物品栏中的盔甲又是另一组图。所以这里需要两组材质图,分布在不同 “layer
” 中。因此,如果想自己创作,还需要专门的软件,例如上面提到的 blockbench;
其次,minecraft 自己把盔甲穿着的材质写死到 minecraft:/
资源作用域下,这意味着,我们不能在原来的地方加材质图,应该新建一个 minecraft
目录;而同时盔甲在物品栏的贴图也要自己设计,和普通 item 一样加 model json 和 扁平材质:
1 | resources/ |
注意,盔甲材质图命名格式:<Ingredient名称ID>_layer_1
(穿着贴图)和 <Ingredient名称ID>_layer_2
(物品栏贴图);
1.8 Item 属性重写
现在回忆一下,在 minecraft
中拉弓,是不是随着时间延长,弓的贴图会变化?这是怎么做到的呢?这里利用了模型 JSON 中的 "overrides"
项的设定,我们来看原版 minecraft 弓的模型 JSON 的一部分:
1 | { |
在 overrides
配置项中,可以看到许多组类似这样:
1 | { |
的组,这好理解,predicate
项就是编程语言中的 IF(如果判断),在其中的是条件对,如果满足了条件对,那么模型和材质会采用同组中的 model
项对应的材质。
按照这个原理,我们可以设计一种 “根据堆叠数量变色” 的特殊 item。这种 item 的类的定义和普通 item 一样,但注册有所差别:需要向 MOD
总线中添加 propertyOverrideRegistry
的特殊事件,用来初始化并在游戏中监听物品变化,及时覆盖材质。
假设我们注册好的对象叫:RegisterObject<Item> magicIngot
,那么需要写这么个类来作为属性覆盖的事件处理器:
这是我们第一次向总线中加入事件,不知道含义的可以回到 0.10 事件系统 这节复习一下。
1 | .EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) |
首先 @Mod
和 @SubscribeEvent
就是经典的静态的事件注册方法,在 0.10 中介绍过;
那么在接收 FMLClientSetupEvent
事件中,方法 propertyOverrideRegistry
做了什么?
首先了解 FMLClientSetupEvent
是在 Java FML(Forge Model Loader)加载时必然触发的事件,所以这里的意思是在 Java FML 加载的时候运行 PropertyRegistry#propertyOverrideRegistry
这个方法,方法体就一行 event.enqueueWork(Supplier<?>)
,这个可以理解为运行里面的 enqueueWork
的 supplier,我们唯一要做的就是把 overrides 这个 item 的情况注册一下。看看注册 item overrides 的函数声明:
1 | ItemModelsProperties.registerProperty( |
注意,从上面的实例代码可以看出,这里的 ResourceLocation
的作用就是将获取的数据绑定到指定的资源名称,方便在接下来的 JSON 文件中使用并 overrides
;它的初始化函数第二参数是资源名称,必须要记住,它和外面的 JSON 文件 predicate
条件中的键的名称要一致(如果用到的话);
上面示例中的 itemStack.getCount()
就是获取 itemStack 的堆叠数量。
最后来看补充的模型文件:
1 | { |
最后放上相应的 texture 试运行一下,应该能见到预期的效果。
Chapter EX-1. Localization & Language
这一章不是必要的,主要讲述如何给之前的物品添加名称,并且提供语言支持。前面的章节创建的 item 名称都是内部的类名,开发者也许懂,但使用者可能会不理解。
这是 minecraft 为了提升可维护性,方便本地化,默认是没有物体名称,item 只有自己的编号(transitionKey
),也就是之前看到的和类名/包名/属性名很像的 XXX.XXX.XXX
;
实际命名和语言的本地化则需要开发者自己完成,有两者方法,第一种比较简单,用 JSON 文件组成从前面的编号到名字的映射工作;第二种是使用 I18n.format
的方法,以后用到再提。
前者使用比较容易,首先找到前面说的 item 的编号(在 Item
类内部,大家可以自己到源码中找一找);下一步,是创建一个 lang
目录,位置如下:
1 | resources/ |
在 lang/
中写一个简体中文的 JSON map,举个例子:
1 | // file: zh_cn.json |
键是之前说的 item 编号,后面是名字/翻译内容;
当然如果想让自己的 mod 国际化,还可以写 zh_tw.json
、en_us.json
,具体还有哪些语言可用,可以参阅 wiki;
To be continued……
—-EOF—-