本篇教程的视频:

本篇教程源代码

GitHub地址:TutorialMod-Item-1.21

注意事项

本系列教程大体上是基于MC的源代码进行讲解的,我真正的目的并非简单地照着Wiki上讲一遍(那又和市面上现有的教程无异),
而是希望通过源代码的讲解,让大家起码会看源代码,知道如何去查找源代码、如何去理解源代码、如何去写出自己的模组

所以有些东西我会讲得比较细,不要觉得繁琐。而且图文教程会比视频教程更加详细,所以请耐心看完。

不过值得注意的是,我们所谓的源代码并不是真正意义上的源代码。
毕竟Minecraft是闭源的商业游戏,我们所说的源代码是通过各种组织利用他们自己的映射反编译得到的

比如说Fabric的Yarn映射(Forge现在采用Mojang给的映射,低版本还有MCP映射)。

由于映射的不同,所以我们看到的源代码也会不一样,不用觉得奇怪。
随着教程的深入,我们自然会接触这些东西,顺带一提映射之间的转换

查找源代码

那么现在,我们就开始编写第一个物品。

假设我们什么也不知道,Wiki也没看过,不知道怎样添加我们的物品。那么怎么办呢?
看源代码

上面也说了本系列教程的目的,所以我们现在就去看源代码。什么,你说怎么查?翻外部库或者随处搜索(使用方法见前一篇教程)

首先,我们来查看Items这个类(注意是Minecraft包中的类)。这个类是Minecraft中所有物品的注册的类。

不过,也许你在搜索的时候会发现还有一个Item类,这个类是物品的基类,也就是说这个类是定义所有物品都会有的属性和方法的类。
而特殊的物品也是继承这个类的。

在Minecraft中,带s的复数形式的类一般是用于注册的类,比如我们之后会讲的BlocksItemGroups等;
而不带s的类一般是用于定义的类,比如我们之后会讲的Block等。

查看注册方法

我们现在随便在Items类中找一个物品(先不要找方块的,因为方块对应的是方块物品),
比如DIAMOND。然后我们看看这个物品是怎么注册的。

查找快捷键是Ctrl + F

1
public static final Item DIAMOND = register("diamond", new Item(new Item.Settings()));

这里我们可以看到,DIAMOND是一个Item类型的常量,它的值是register方法的返回值。
那么我们就来看看register方法。

按住Ctrl键,点击register方法,我们就可以跳转到register方法的定义处。

1
2
3
public static Item register(String id, Item item) {
return register(Identifier.ofVanilla(id), item);
}

但是这还没完,因为这里还有一个register方法,我们再来看看。

1
2
3
public static Item register(Identifier id, Item item) {
return register(RegistryKey.of(Registries.ITEM.getKey(), id), item);
}

以及最后一个register方法。

1
2
3
4
5
6
7
public static Item register(RegistryKey<Item> key, Item item) {
if (item instanceof BlockItem) {
((BlockItem)item).appendBlocks(Item.BLOCK_ITEMS, item);
}

return Registry.register(Registries.ITEM, key, item);
}

而到了最后一个register方法,我们就可以看到,这个方法是真正的注册方法,它调用了Registryregister方法。

Minecraft中的注册方法一般是这样的,层层叠叠,写了好几个注册方法,最后调用Registryregister方法。

物品注册

创建ModItems类

那么我们现在就来创建一个ModItems类,用于注册我们的物品。

1
2
3
public class ModItems {

}

然后我们来写注册方法,如果你不想整合上面的那三个方法,可以直接把上面的代码复制到ModItems类中。

不过我还是来给它整合一下,毕竟这样更加简洁一点

1
2
3
4
private static Item registerItems(String id, Item item) {
// 由原版整合的方法
return Registry.register(Registries.ITEM, RegistryKey.of(Registries.ITEM.getKey(), Identifier.ofVanilla(id)), item);
}

整合的方法也很简单,只是把最后一个register方法中的key和item用上面两个方法进行了扩充。

不过,这里的方法还是有点问题的。
在这之前,我们解释一下register方法中的各个参数

1
2
3
4
static <V, T extends V> T register(Registry<V> registry, RegistryKey<V> key, T entry) {
((MutableRegistry)registry).add(key, (V)entry, RegistryEntryInfo.DEFAULT);
return entry;
}

这个方法的第一个参数是Registry,第二个参数是RegistryKey,第三个参数是entry

而对应到我们自己的方法中,第一个参数是Registries.ITEM,它是一个DefaultedRegistry<Item>类型的常量,在Registries类中定义。
这个类是Minecraft中所有的注册表的类,后续我们还会讲到Registries.BLOCKRegistries.ITEM_GROUP等。

第二个参数是RegistryKey.of(Registries.ITEM.getKey(), Identifier.ofVanilla(id))
这个方法是为注册表中的某个值创建注册表键值,同时创建根注册表中持有值注册表的注册表键值和值的标识符。
不过这里的Identifier我们待会着重会讲

第三个参数是item。也是我们后面会进行编写的物品的一些基本设置。

Identifier

Identifier是一个极其重要的类

1
2
3
4
An identifier used to identify things. This is also known as "resource location", "namespaced ID", "location", or just "ID". 

Format
Identifiers are formatted as <namespace>:<path>. If the namespace and colon are omitted, the namespace defaults to "minecraft".

这是Identifier的注释,说白了,它就是我们常说的命名空间 + id(我们自己物品、方块或者其他东西的),或者说一些特定文件的路径

这里的format<namespace>:<path>,如果省略了命名空间和冒号,那么命名空间默认为minecraft

而再往下,重点的注释是这个

1
2
The namespace and path must contain only ASCII lowercase letters ([a-z]), ASCII digits ([0-9]), or the characters _, ., and -.
The path can also contain the standard path separator /.

这里说的是命名空间和路径只能包含ASCII小写字母[a-z]ASCII数字[0-9]下划线[_]点[.]短横线[-]
而路径还可以包含标准路径分隔符/。而你一旦写了其他的非法字符,启动游戏就会直接崩溃,并抛出net.minecraft.util.InvalidIdentifierException: Non [a-z0-9_.-] ...异常。

而什么黑紫块、找不到文件、无法显示等等,都是因为命名空间和路径的问题,那些东西要重点检查。

现在我们看到自己代码中的Identifier.ofVanilla(id)

1
2
3
public static Identifier ofVanilla(String path) {
return new Identifier("minecraft", validatePath("minecraft", path));
}

我们可以看到,这里的ofVanilla方法,它的命名空间是minecraft
这不是我们希望看到的,我们的模组最好能够有独立的命名空间,
而且如果你的命名空间是minecraft,那么你的物品、方块等等资源文件都得放在minecraft文件夹下,
这样可能会和原版资源文件冲突(如果你的物品和原版物品同名的话)。

幸好,Identifier还有一个构造方法,我们可以自己定义命名空间

1
2
3
public static Identifier of(String namespace, String path) {
return ofValidated(namespace, path);
}

这个方法就是我们自己定义命名空间的方法,我们可以自己定义一个命名空间,还记得我们的MODID吗?tutorialmod在这里就可以用上了。

重新整合注册方法

那么现在我们就重新写一下那个注册方法

1
2
3
private static Item registerItems(String id, Item item) {
// 由原版整合的方法
return Registry.register(Registries.ITEM, RegistryKey.of(Registries.ITEM.getKey(), Identifier.of(TutorialMod.MOD_ID, name)), item);

这样的话,命名空间就是我们自己定义的tutorialmod,而不是minecraft了。
而这个方法也就可以使用了。

不过,你是否觉得这个方法还是有点繁琐,毕竟有点长对吧?
那么其实我们还可以进一步简化,采用Registry.register的另一个同名不同参的方法

1
2
3
4
private static Item registerItems(String name, Item item) {
// 采用register的另一个方法
return Registry.register(Registries.ITEM, Identifier.of(TutorialMod.MOD_ID, id), item);
}

而这个register方法调用的是我们前面写的那个register方法,本质上是一样的。

1
2
3
static <V, T extends V> T register(Registry<V> registry, Identifier id, T entry) {
return register(registry, RegistryKey.of(registry.getKey(), id), entry);
}

中间的RegistryKey方法就是我们前面写的那个RegistryKey方法,而这里的register方法是Blocks注册用的,我们后面会讲到。

注册物品

经过了一系列铺垫,我们终于可以开始写我们的物品了。但是在写之前,我们还是要先看看DIAMOND这个物品是怎么注册的。

1
public static final Item DIAMOND = register("diamond", new Item(new Item.Settings()));

我们可以看到,DIAMOND的注册中,实例化了一个Item对象,而这个Item对象的构造方法中传入了一个Item.Settings对象。
这里的Item.Settings是一个物品的设置类,我们可以在这个类中设置物品的一些属性。

这里的diamond是个最简单的物品,没有什么特殊的属性,所以直接传入一个Item.Settings对象即可。

后续会讲到的最大耐久值(maxDamage)最大堆叠数(maxCount)抗火特性(fireproof)等等,都是在这个Settings中设置的。感兴趣的话可以自己先去看看其他的一些物品的设置。

另外,这里也可以提一点,上面的这些设置,最终都会被转换成组件(Component)的形式进行储存,这个组件的前身便是我们熟知的NBT,只是高版本的NBT变成了Component

好了,我们现在就来写我们的物品。

1
public static final Item ICE_ETHER = registerItems("ice_ether", new Item(new Item.Settings()));

这里的ICE_ETHER是我们的物品,延用1.20的东西,ice_ether是我们的物品的id(记好了,不能有非法字符)new Item(new Item.Settings())是我们的物品的实例化对象。
物品的设置我们暂时也没有,所以就和DIAMOND一样,简单写一下即可

初始化方法

那好了,注册完了吗?当然没有,因为我们的这个类还没有被初始化,启动游戏也没有用的。

这里我们需要一个初始化方法,并在主类中调用这个初始化方法。

1
2
3
public static void registerModItems(){
TutorialMod.LOGGER.info("Registering Items");
}

这个方法就是我们的初始化方法,这里写了一个日志输出,用于在启动游戏的时候输出一些信息。其实这个方法空着也没事

然后到我们主类中的onInitialize调用这个方法

1
ModItems.registerModItems();

这里的onInitialize方法是在游戏启动的时候被调用的,所以我们在这里调用我们的初始化方法。

这也是利用Java的特性,当我们调用一个类的方法的时候,这个类会被初始化。而这个类的静态代码块也会被初始化,
而我们的物品是static final修饰的,所以在这个时候,我们的物品也就完成了注册。

整体代码

1
2
3
4
5
6
7
8
9
10
public class ModItems {
public static final Item ICE_ETHER = registerItems("ice_ether", new Item(new Item.Settings()));
private static Item registerItems(String id, Item item){
// return Registry.register(Registries.ITEM, RegistryKey.of(Registries.ITEM.getKey(), Identifier.of(TutorialMod.MOD_ID, id)), item);
return Registry.register(Registries.ITEM, Identifier.of(TutorialMod.MOD_ID, id), item);
}
public static void registerModItems(){
TutorialMod.LOGGER.info("Registering Items");
}
}

资源文件

那么我们的物品注册完了,但是我们现在进入游戏会发现一个黑紫块,所以我们还需要它的资源文件,包括模型文件、语言文件和贴图文件。

模型文件

我们先来写模型文件,我们可以先看原版的物品模型文件,然后修改一下。比如说这个diamond.json文件

1
2
3
4
5
6
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "minecraft:item/diamond"
}
}

稍加改动,我们就可以得到我们的物品模型文件,
路径是src/main/resources/assets/tutorialmod/models/item/ice_ether.json

1
2
3
4
5
6
7

{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "tutorialmod:item/ice_ether"
}
}

语言文件

然后我们来写语言文件,
我们可以先看原版的物品语言文件,然后修改一下。比如说这个en_us.json文件

1
2
3
{
"item.minecraft.diamond": "Diamond"
}

稍加改动,我们就可以得到我们的物品语言文件,
路径是src/main/resources/assets/tutorialmod/lang/en_us.json

1
2
3
{
"item.tutorialmod.ice_ether": "Ice Ether"
}

那么en_us是英文(美式)语言文件,也是默认情况下会使用的语言文件。也就是说假设你的游戏是中文的,但缺失了中文的语言文件,它会采用英文的语言文件进行显示。

如果你要支持其他语言,可以在这个文件夹下新建一个文件,
比如简体中文是zh_cn.json,然后把en_us.json的内容复制过去,然后翻译一下就行了。

假设说你不写语言文件,那么游戏会直接显示物品的注册名,也就是item.tutorialmod.ice_ether这一串。

贴图文件

这个的话就拿PS这种软件画一个贴图就行了,然后放到src/main/resources/assets/tutorialmod/textures/item文件夹下

贴图文件的名字要和模型文件中的layer0的值一样,不然游戏会找不到贴图文件,导致物品显示不出来。

不过值得注意的是,贴图的格式要是PNG格式,不然无法加载,分辨率推荐2的n次方,比如16x16、32x32、64x64等等。
不要取个诡异的分辨率,比如说17x17,虽然不会报错,但会有警告

不想自己画就拿这里的好了

ice_ether

测试

那么我们现在就可以启动游戏了,看看我们的物品是否注册成功。
因为我们的物品并没有加入到任何物品栏中,所以我们也只能使用指令去获取这个物品。

使用/give命令来给自己一个物品,看看是否显示正常。

1
/give @s tutorialmod:ice_ether

如果你能够得到一个带有正确材质的物品,那么恭喜你,你的物品注册成功了

success

另外,在常规开发过程中,/give命令可以用来测试物品、方块的注册情况。因为它一旦注册成功,那么就可以通过这个命令来获取这个物品。