written by SJTU-XHW

Reference: Minecraft Forge Doc 1.16.xBoson 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;

      例如 notchj 对应 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.pngmod1:textures/Alex.png
  • 模型和材质:在游戏中 3D 的对象基本上都有它的模型,模型和材质组合在一起规定了一个对象具体的样子。模型相当于是骨头,材质相当于是皮肤。在大部分时候,你的材质都是 png 图片【可能需要平面设计 和 PS 的相关功底】。

    注意保证材质背景是不透明的,也不要在材质中使用半透明像素,会有不可预知的问题。

0.5 开发环境

Minecraft Forge 是由 Gradle 管理的项目,而 Forge 官方写了一个叫做 ForgeGradle(以后简称FG)的插件来负责整个 mod 开发环境的配置,本节主要介绍这个环境的配置和使用;

  • 前提

    1. JDK 8(1.16.x 兼容版本)和 64 bit JVM;请确保您的操作系统已经安装并配置好环境变量 JAVA_HOMECLASS_PATH
    2. 官网获得 MDK(Mod Development Kit):我们这里 1.16.5 选择官网推荐的 forge-36.2.34 Downloads for Minecraft Forge for Minecraft 1.16.5
    3. Java 开发 IDE,可以选 VSCode / Eclipse / IDEA,本文以 IDEA 为例进行。
  • 开发准备工作

    1. 这里我们将下载的 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.gradlegradlew.batgradlewgradle/ 目录;

      只要有了上面几个的数据,就能创建一个新 mod 项目;

    2. 把上面说的重要的几个文件复制到新目录,以后这里是 mod 项目的根目录了(这么做是为了减少文件干扰,毕竟我们不需要版本控制,对吧);

    3. 用 IDEA 打开这个新目录,即可自动下载依赖配件、设置项目;如果中途出现错误,99% 是因为网络错误。请翻*GFW,或者上网找国内源解决;

    4. 打开后,在 IDEA Terminal 下执行 gradlew genIntellijRuns(如果是 eclipse/vscode,那么后面一个词分别是 genEclipseRunsgenVSCodeRuns),它会进一步下载游戏需要的资源,并且设置 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
      4
      minecraft {
      // 这是最后一次 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 package org.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.gradlemods.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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"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.tomlmodidbuild.gradleexamplemod 三者一致。因此,我们需要给 HelloMC@Mod() annotation。为了方便,我们另外创建一个类,专门存储全局变量,就定为 Utils(你也可以不用,看自己的编码习惯):

1
2
3
4
5
6
7
8
9
// File: HelloMC.java
package com.test.HelloMC;

import net.minecraftforge.fml.common.Mod;

@Mod(Utils.MOD_ID)
public class HelloMC {
// 这里先空着,因为没学
}
1
2
3
4
5
6
// File: Utils.java
package com.test.Utils;

public class Utils {
public static final String MOD_ID = "mymod"; // 自己写 modid
}

step 3. 现在去写 mods.toml,注意填写 mod_id

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
# 上面使用的 @Mod 就在这个 modLoader 中,和 loaderVersion 不用改
modLoader="javafml"
loaderVersion="[36,)"
# 自己选证书:https://choosealicense.com/.
license="All Rights Reserved"
# 自己定义 issue 提问网站,可选
issueTrackerURL="https://github.com/MinecraftForge/MinecraftForge/issues"
# 暂时用不到,不管它
showAsResourcePack=false

[[mods]]
modId="yourID"
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.yourID]]
modId="forge"
mandatory=true
versionRange="[36,)"
ordering="NONE"
side="BOTH"

[[dependencies.yourID]]
modId="minecraft"
mandatory=true
versionRange="[1.16.5,1.17)"
ordering="NONE"
side="BOTH"

step 4. 再到 build.gradle 中,将所有 examplemod 换成自己的 modid,修改个性化内容,然后构建!

step 5. 启动 IDEA 上的任务设置 “RunClient”,进入游戏查看自己的 mod 是否已经显示!

0.10 事件系统

早在 0.3 节说明运行模式的时候,我们提到了 Mod 总线和 Forge 总线,这里在开发前必须要说清楚。Forge 自己的事件系统内是独立于 Minecraft 的事件系统的。

使用 Forge 事件系统的方法有 2 种,先看个例子:

1
2
3
4
5
6
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 总线:

1
MinecraftForge.EVENT_BUS.register(new TestEventHandler());

显然这样注册的方法比较麻烦,被称为 “实例注册方式”。还有一种:

1
2
3
4
5
6
7
@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() 的源码查看。

Chapter 1. 创建物品

1.1 第一个物品

本节了解,创建一个物品就 3 步【所有物品都是这样】:

  1. 创建自己的物品类,一般继承于原版的物品的类 Item
  2. 在合适的位置实例化这个物品;
  3. 将此物品对象注册进游戏;

我们先以一个原版游戏中没有 “黑曜石锭” 为例:

1
2
3
4
5
public class ObsidianIngot extends Item {
public ObsdianIngot() {
super(new Properties().group(ItemGroup.MATERIALS));
}
}

这里我们就写了一个构造函数,向父类的构造函数传递了一个 Properties 类的对象,而这个 Properties 对象调用了 group()表示设置物品所在组为 Materials 组(杂项栏)

传给父类的 Properties 对象的作用是规定了物品的一些属性,比如:是不是食物,或者这个物品在创造模式的哪一个物品栏。这里的 group() 方法就是设置了物品在创造模式的杂项栏。

如果不调用 group() 方法设置,那么这个物品就不会出现在创造模式物品栏,只能用 /give 获取;

然后是实例化 + 注册。现在的 Forge 引入了 DeferredRegister的机制:

1
2
3
4
5
6
public class ItemRegistry {
public static final DeferredRegister<Item> item_reg =
DeferredRegister.create(ForgeRegistires.ITEMS, Utils.MOD_ID);
public static final RegistryObject<Item> obsdianIngot =
item_reg.register("obsidian_ingot", ObsidianIngot::new);
}

先使用 DeferredRegister 泛型类的静态方法 create(<ForgeRegistries.TYPE>, <mod_id>)mod_id 的 mod 创建了一个 DeferredRegister<Type> 对象,表示这是专门注册 Type 类型实体的类

紧接着告诉这个对象要注册 “注册名为 ‘obsdian_ingot’(不能大写)、其对象构造方法为 ObsidianIngot::new” 的一个类的 supplier

最后,由于这个 DeferredRegister 泛型类帮助我们注册物品需要依赖注入事件。显然注册是一种初始化的事件,所以将 DeferredRegister 对象注入 Mod 总线(我们以 0.9 中的项目为例):

1
2
3
4
5
6
7
8
9
10
@Mod(Utils.MOD_ID)
public class HelloMC {
public HelloMC() {
// DeferredRegister 的 register 方法如果参数是总线,就将自己注入总线中
ItemRegistry.item_reg.register(
// 这里前面说过,获取 Mod 总线
FMLJavaModLoadingContext.get().getModEventBus()
);
}
}

运行后会发现新的、没有材质的物品出现在创造模式的物品栏中。

1.2 物品的模型和材质

一个物品被注册后,还应该指定它的模型和材质。这种指定方式也是固定的,记忆即可。下面以上面的 ObsidianIngot 为例。

第一步,在 src/main/resources 创建 assets 目录,表示这个 mod 存储资源的位置。并将目录补充为:

1
2
3
4
5
6
7
8
9
10
resources/
├── META-INF/
│   └── mods.toml
├── assets/
│   └── mymod/ # 你的 modid
│   ├── models/
│   │   └── item/ # MC 中要求这个目录名,不能改,表示 item(物品)类型
│   └── textures/
│   └── item/
└── pack.mcmeta

一定要创建 pack.mcmeta,这相当于为 mod 规定了资源包。只有这么做,MC 才能发现 assets 中的资源。格式也是 JSON,模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
{ 
"pack": {
"pack_format": 10, // 看官网,pack_format = 10 时支持 MC 1.20 以下的版本
"description": "My Resource Pack"
},
"language": {
"LANG_COUNTRY": {
"name": "My Custom Language",
"region": "Country/Region name",
"bidirectional": false
}
}
}

第二步,在 models/item 的目录中创建 obsidian_ingot.json,文件名需要和之前物品注册名相同。

item(物品类)模型的 JSON 文件的模板如下:

1
2
3
4
5
6
7
8
{
// 父模型,即由什么模型加载而来
"parent": "item/generated",
"textures": {
// 指定了最底层的材质(格式在 0.4 中已经叙述)
"layer0": "mymod:item/obsidian_ingot"
}
}

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 的实例

所以,可以将 ItemStackItem 的关系看作类的聚集,在对 ItemStack 可以调用 getItem() 实例方法来获得其中含有的 Item 对象。

享元

另外有个极其重要的概念——“享元(Flyweight Pattern)”,即一种特定物品只对应一个实例,而 Item 就是一个享元。这意味着,以 Apple 类为例,你在游戏里见到所有苹果都是 Apple 类的唯一实例。正因如此,我们在编写 mod 中,可以直接用 ==(即 <ItemStack_obj>.getItem() == <Item_obj>)来判断 Item 种类,不用担心 == 比较的是地址。

Item.AIR

还有一件事,由于 ItemStacknon-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
2
3
4
5
6
7
8
9
10
11
public class ObsidianGroup extends ItemGroup {
public ObsidianGroup() {
super("obsidian_group");
}

@Override
public ItemStack createIcon() {
// ItemRegistry.obsidianIngot 对象在 1.1 中定义,忘了回去看
return new ItemStack(ItemRegistry.obsidianIngot.get());
}
}

这个简单的物品组类就做好了。但是想要将它显示在游戏里,还要将它在合适的位置实例化。

我们再定义一个类,专门存放 mod 中实例化的 ItemGroup 的类,就叫 ModGroup

1
2
3
public class ModGroup {
public static final ItemGroup og_group = new ObsidianGroup();
}

这样在物品类初始化的时候,用 Properties::group() 实例方法将物品加入 og_group 这个实例即可。

1
2
3
4
5
public class ObsidianIngot extends Item {
public ObsidianIngot() {
super(new Properties().group(ModGroup.itemGroup));
}
}

1.5 食物

食物就是一种特殊的 Item,也继承于 Item 来创建,只是比普通 Item 多几个属性(包括专门的类 Food 作为属性)。举个例子:

1
2
3
4
5
6
7
8
9
10
public class ObsidianApple extends Item {
// 定义食物的属性,使用 Food 类
private static final Food food = (
new Food.Builder().saturation(10)
.hunger(20)
// 使用接口创建 Supplier<EffectInstance>
.effect(() -> new EffectInstance(Effects.POISON, 3*20, 1), 1)
.build();
);
}

为 Java 基础补充知识:Java Supplier是一个功能接口,代表结果的提供者,存在于 java.util.function。常用方法是 get(),源码如下:

1
2
3
4
@FunctionalInterface
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
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
public enum ModItemTier implements ItemTier {
// 规范来说,enum 内部先定义实例
OBSIDIAN(3, 2000, 10.0F, 4.0F, 30);

private final int harvestLevel; // 精准采集等级
private final int maxUses; // 最大耐久值
private final float efficiency; // 效率值
private final float attackDamage; // 伤害
private final int enchantability; // 附魔能力

// 构造方法就是将属性值全部传进去
ModItemTier(int harvestIn, int maxUsesIn, float effIn, float attIn, int enchantabilityIn) {
this.harvestLevel = harvestIn;
this.maxUses = maxUsesIn;
this.efficiency = effIn;
this.attackDamage = attIn;
this.enchantability = enchantabilityIn;
}

// 下面的部分别看它长,就是把 get 方法简单覆盖一下 -------------------|
@Override
public int getHarvestLevel() { return this.harvestLevel; }
@Override
public int getMaxUses() { return this.maxUses; }
@Override
public float getEfficiency() { return this.efficiency; }
@Override
public float getAttackDamage() { return this.attackDamage; }
@Override
public int getEnchantability() { return this.enchantability; }
// ------------------------------------------------------------|

// 之前提到的,修复材料需要单独写一下
// 但具体的合成表还需要以后写
@Override
public Ingredient getRepairMaterial() {
return Ingredient.fromItems(ItemRegistry.obsidianIngot.get());
}
}

别问为什么要重新写属性并且 override。因为这是个 enum 类型,不存在继承的说法,只能 implements 方法。

再来看 “剑 / 稿 / 斧 / 锄 / 铲” 类的继承关系:

1
2
3
4
5
6
7
Item --> TieredItem (有工具等级的 item) --> SwordItem
|
└─--> ToolItem // 这里是下一节要说的 “工具”,也有工具等级
├─----------> PickaxeItem
├─----------> ShovelItem
├─----------> AxeItem
└─----------> HoeItem

这里简单到爆炸,只需要掌握这些类的初始化方法就行,以剑为例:

1
public SwordItem(ItemTier tier_obj, int, float, Properties prop);

像这里以黑曜石剑为例:

1
2
3
4
5
6
7
public class ObsidianSword extends SwordItem {
public ObsidianSword() {
// 这里的 OBSIDIAN 是之前定义 enum ModItemTier 时创建的实例
// ItemGroup.COMBAT 是战斗物品组,对应创造模式物品栏“铁剑”图标一栏
super(ModIterTier.OBSIDIAN, 3, -2.4F, new Properties().group(ItemGroup.COMBAT));
}
}

SwordItem 构造函数的第一参数 ItemTier 后面的两个参数分别是攻击伤害、攻击速度

之后的模型、材质、物品注册也是完全和普通 item 相同。

稿子、铲子、斧头、锄头也一样。

注意:以上数据面板理论上只要不溢出都可以自行设置。但如果想要写成好的 Mod,这边建议自己试试平衡性,再调整数值。

你问为什么不介绍 “弓” 这种远程武器?这大概是因为 minecraft java 到处是高耦合度的代码(尤其是弓这个动作复杂的 item),哪怕仅修改拉弓速度都要引入 7~8 个包,故不再这里叙述。实在想要改,建议直接改材质 + 附魔;

1.7 可穿戴装备

和近战武器/工具类似,可穿戴装备也有 “工具等级” 来管理和装备位置无关的数据,这就是 IArmorMaterial 接口类,我们模仿 1.6 中的 ModItemTier 用一个枚举类来自定义:

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
public enum ObsidianArmorMaterial implements IArmorMaterial {
OBSIDIAN(
"obsidian", 40, new int[]{5,8,10,5}, 20, SoundEvents.ITEM_ARMOR_EQUIP_DIAMOND, 2.0F, 0.0F, () -> {
return Ingredient.fromItems(ItemRegistry.obsidianIngot.get());
}
);
// 下面是 IArmorMaterial 需要拥有的属性:
// 最大耐久数值数组
private static final int[] MAX_DAMAGE_ARRAY = new int[]{13,15,16,11};
// 这个名字是给渲染端用的(也就是客户端),也是找材质用的名字。请确保此处、注册名、材质名三者写的是一个名字
private final String name;
private final int maxDamageFactor; // 最大耐久衰减因数
private final int[] damageReductionAmountArray; // 减伤数组(保护效果)
private final int enchantability; // 附魔能力
private final SoundEvent soundEvent; // 穿着音效
private final float toughness; // 文档没说,可以自己尝试
private final float knockbackResistance; // 击退抵抗数值
// 这个 LazyValue 可以理解为 Forge 包里面内含的一种容器,实现懒加载。泛型参数是懒加载的结果,而它初始化参数是对应泛型的 Supplier
// 这个属性相当于 ItemTier 中的 getRepairMaterial()
private final LazyValue<Ingredient> repairMaterial;

// 构造函数还是就把之前的属性给它们赋值一遍
ObsidianArmorMaterial(
String armorName, int maxDamageF, int[]damageReductionArray,
int enchantAbility, SoundEvent se, float tough, float knockResist,
Supplier<Ingredient> repairMaterialSupplier
) {
this.name = armorName; this.maxDamageFactor = maxDamageF;
this.damageReductionAmountArray = damageReductionArray;
this.enchantability = enchantAbility;
this.soundEvent = se; this.toughness = tough;
this.knockbackResistance = knockResist;
this.repairMaterial = new LazyValue<>(repairMaterialSupplier);
}
// 下面实现 IArmorMaterial 接口,只要给出所有的 get 方法就行。注意名字和参数。
// 注意,getName 因为是为客户端准备的函数,所以加上 annotation @OnlyIn 就行

public int getDurability(EquipmentSlotType slotIn) {
return MAX_DAMAGE_ARRAY[slotIn.getIndex()] * this.maxDamageFactor;
}
public int getDamageReductionAmount(EquipmentSlotType slotIn) {
return this.damageReductionAmountArray[slotIn.getIndex()];
}
public int getEnchantability() {
return this.enchantability;
}
public SoundEvent getSoundEvent() {
return this.soundEvent;
}
public Ingredient getRepairMaterial() {
return this.repairMaterial.getValue();
}
@OnlyIn(Dist.CLIENT)
public String getName() {
return this.name;
}
public float getToughness() {
return this.toughness;
}
public float getKnockbackResistance() {
return this.knockbackResistance;
}

}

接着,和其他 item 一样设置可穿戴装备对应的对象,并且注册。

但是!一般情况我们无需继承不同的类,只需要创建 ArmorItem 类的不同实例,就能获得不同部位的 item 对象。这多半是因为可穿戴装备是盔甲,都设计成一套,例如注册时:

1
2
3
4
5
6
7
8
9
// 回忆一下,item_reg 是之前在 ItemRegistry 类中定义的 DeferredRegister 类的实例
public static final RegistryObject<Item> obsidianHelmet = item_reg.register("obsidian_helmet", () -> new ArmorItem(ModArmorMaterial.OBSIDIAN, EquipmentSlotType.HEAD, (new Item.Properties()).group(ModGroup.itemGroup)));

public static final RegistryObject<Item> obsidianChestplate = item_reg.register("obsidian_chestplate", () -> new ArmorItem(ModArmorMaterial.OBSIDIAN, EquipmentSlotType.CHEST, (new Item.Properties()).group(ModGroup.itemGroup)));

public static final RegistryObject<Item> obsidianLeggings = item_reg.register("obsidian_leggings", () -> new ArmorItem(ModArmorMaterial.OBSIDIAN, EquipmentSlotType.LEGS, (new Item.Properties()).group(ModGroup.itemGroup)));

public static final RegistryObject<Item> obsidianBoots = item_reg.register("obsidian_boots", () -> new ArmorItem(ModArmorMaterial.OBSIDIAN, EquipmentSlotType.FEET, (new Item.Properties()).group(ModGroup.itemGroup)));

现在传给 DeferredRegister<Item>::register() 的第二参数不再是类自带的 new 方法,而是临时写的匿名函数,利用已存在的类 ArmorItem 的构造方法:

1
2
3
4
// 第一参数是之前定义的 可穿戴装备的“等级枚举类”
// 第二参数是盔甲的部位,会针对性读取材质的某一区域
// 第三参数是原始的 item 对象的 supplier
public ArmorItem(IArmorMaterial material, EquipmentSlotType type, Supplier<Item> item);

现在最后一件事需要注意,就是盔甲的材质添加和其他 item 不一样

首先从前面共用一个类可以看出,这里盔甲的材质也在一个图中。说 “一个图” 不是很准确,准确来说是穿着状态的盔甲全套一组图,物品栏中的盔甲又是另一组图。所以这里需要两组材质图,分布在不同 “layer” 中。因此,如果想自己创作,还需要专门的软件,例如上面提到的 blockbench;

其次,minecraft 自己把盔甲穿着的材质写死到 minecraft:/ 资源作用域下,这意味着,我们不能在原来的地方加材质图,应该新建一个 minecraft 目录;而同时盔甲在物品栏的贴图也要自己设计,和普通 item 一样加 model json 和 扁平材质:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
resources/
├── META-INF/
│   └── mods.toml
├── assets/
│   ├── mymod/ # 你的 modid,之前材质存放的地方(作用域为 mymod:/)
│   | ├── models/
│   | │   └── item/ # 盔甲物品栏图标模型 json 存放位置
│   | └── textures/
│   | └── item/ # 盔甲物品栏材质图片存放位置
| |
| └── minecraft/ # 新建的目录,材质作用域是 minecraft:/
| └── textures/
| └── models/
| └── armor/ # 盔甲穿着材质图片存放位置
| └── ...
|
└── pack.mcmeta

注意,盔甲材质图命名格式:<Ingredient名称ID>_layer_1(穿着贴图)和 <Ingredient名称ID>_layer_2(物品栏贴图);

1.8 Item 属性重写

现在回忆一下,在 minecraft 中拉弓,是不是随着时间延长,弓的贴图会变化?这是怎么做到的呢?这里利用了模型 JSON 中的 "overrides" 项的设定,我们来看原版 minecraft 弓的模型 JSON 的一部分:

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
{ 
// ...

"overrides": [
{
"predicate": {
"pulling": 1
},
"model": "item/bow_pulling_0"
},
{
"predicate": {
"pulling": 1,
"pull": 0.65
},
"model": "item/bow_pulling_1"
},
{
"predicate": {
"pulling": 1,
"pull": 0.9
},
"model": "item/bow_pulling_2"
}
]

// ...
}

overrides 配置项中,可以看到许多组类似这样:

1
2
3
4
5
6
{
"predicate": {
"pulling": 1
},
"model": "item/bow_pulling_0"
}

的组,这好理解,predicate 项就是编程语言中的 IF(如果判断),在其中的是条件对,如果满足了条件对,那么模型和材质会采用同组中的 model 项对应的材质。

按照这个原理,我们可以设计一种 “根据堆叠数量变色” 的特殊 item。这种 item 的类的定义和普通 item 一样,但注册有所差别:需要向 MOD 总线中添加 propertyOverrideRegistry 的特殊事件,用来初始化并在游戏中监听物品变化,及时覆盖材质。

假设我们注册好的对象叫:RegisterObject<Item> magicIngot,那么需要写这么个类来作为属性覆盖的事件处理器:

这是我们第一次向总线中加入事件,不知道含义的可以回到 0.10 事件系统 这节复习一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Mod.EventBusSubscriber(bus = Mod.EventBusSubscriber.Bus.MOD, value = Dist.CLIENT)
public class PropertyRegistry {
@SubscribeEvent
public static void propertyOverrideRegistry(FMLClientSetupEvent event) {
event.enqueueWork(
() -> {
ItemModelProperties.registerProperty(
ItemRegistry.magicIngot.get(),
new ResourceLocation(Utils.MOD_ID, "size"),
(itemStack, clientWorld, livingEntity) -> itemStack.getCount()
);
}
);
}
}

首先 @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
2
3
4
5
ItemModelsProperties.registerProperty(
RegisterObject<Item> registered_item,
ResourceLocation resourceLocation,
Supplier<?> supplier // ? 为 游戏中想要监测的属性类型
); // supplier 就是调用获取数据的匿名函数

注意,从上面的实例代码可以看出,这里的 ResourceLocation 的作用就是将获取的数据绑定到指定的资源名称,方便在接下来的 JSON 文件中使用并 overrides;它的初始化函数第二参数是资源名称,必须要记住,它和外面的 JSON 文件 predicate 条件中的键的名称要一致(如果用到的话);

上面示例中的 itemStack.getCount() 就是获取 itemStack 的堆叠数量。

最后来看补充的模型文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"parent": "item/generated",
"textures": {
"layer0": "item/iron_ingot" // 这里投机取巧,普通模式就用了铁锭的材质
}, // 下面是 overrides 添加的部分
"overrides": [
{
"predicate": {
"<modid>:size": 16 // 这里注意,<modid> 替换为自己的 modid
}, // size 就是要求记住的、之前注册的名称
"model": "item/gold_ingot"
}
]
}

最后放上相应的 texture 试运行一下,应该能见到预期的效果。

Chapter EX-1. Localization & Language

这一章不是必要的,主要讲述如何给之前的物品添加名称,并且提供语言支持。前面的章节创建的 item 名称都是内部的类名,开发者也许懂,但使用者可能会不理解。

这是 minecraft 为了提升可维护性,方便本地化,默认是没有物体名称,item 只有自己的编号(transitionKey,也就是之前看到的和类名/包名/属性名很像的 XXX.XXX.XXX

实际命名和语言的本地化则需要开发者自己完成,有两者方法,第一种比较简单,用 JSON 文件组成从前面的编号到名字的映射工作;第二种是使用 I18n.format 的方法,以后用到再提。

前者使用比较容易,首先找到前面说的 item 的编号(在 Item 类内部,大家可以自己到源码中找一找);下一步,是创建一个 lang 目录,位置如下:

1
2
3
4
5
6
7
8
9
10
11
12
resources/
├── META-INF/
│   └── mods.toml
├── assets/
│   ├── mymod/ # 你的 modid,之前材质存放的地方(作用域为 mymod:/)
│   | ├── models/
│   | ├── textures/
| | └── lang/ # 这里是新建的 lang 目录
| |
| └── minecraft/
|
└── pack.mcmeta

lang/ 中写一个简体中文的 JSON map,举个例子:

1
2
3
4
5
6
7
// file: zh_cn.json
{
"item.XXX.obsidian_ingot": "黑曜石锭",
"item.XXX.obsidian_apple":"黑曜石苹果",
"item.XXX.obsidian_sword":"黑曜石剑",
"itemGroup.obsidian_group": "黑曜石物品栏"
}

键是之前说的 item 编号,后面是名字/翻译内容;

当然如果想让自己的 mod 国际化,还可以写 zh_tw.jsonen_us.json,具体还有哪些语言可用,可以参阅 wiki

To be continued……

—-EOF—-