本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-Item-1.20.1

前述

视频教程里稍微提了一下,本系列教程为长线教程计划

教程模式和1.21的一样,我将从源代码的层面来讲模组开发

这里的话,顺便浅谈一下FabricForgeNeoForge区别

它们两个的话,你就只能用它们的那个注册系统,而不能像Fabric那样直接用原版的注册系统

不知道大家有没有像我一样去尝试过,如果你按照原版的方法去注册物品、方块等东西,
在游戏加载的时候,会直接抛出类似于注册表已冻结这样的信息,游戏崩溃

所以,我猜测(对,只是猜测,因为没去完全了解过他们具体的工作逻辑),
ForgeNeoForge在加载的时候,会先加载Minecraft的主程序,然后再加载模组

Fabric其实在它自己的Wiki上也说了,Fabric是完全通过Mixin来工作的,
它本身也就是个Mixin,我们的模组也是通过它直接注入游戏中的

所以Fabric很轻,加载速度比那两个快得多,但看起来没那么稳定

以前确实是有大型模组Forge开发,而轻量级的模组在Fabric开发这样的说法

不过现在,其实已经差不多了,你在哪个端开发都没关系。
只是,ForgeNeoForge有更多的轮子API),而Fabric的话,
相对欠缺一点,但也可以自己造

而且在FabricMixin也会用得多一点,不过,Mixin使用前提是你对Minecraft的源码有一定了解,
否则很容易把正常的游戏给改崩

本篇教程目标

  • 理解物品注册
  • 理解模组开发之路的第二块绊脚石——Identifier类
  • 理解物品的基本属性
  • 编写物品模型的json文件(贴图就自己学吧)

查看源代码

那么现在,我们正式开始讲解第一个物品

假设说,你现在什么都不会,不知道物品该怎么添加,然后其他的教程也没看过

那么,很简单,看源代码

我们利用之前讲过的IDEA的随处搜索,快捷键double shift,搜索item

item是物品的英文,玩Minecraft的应该都知道吧

注意,这里搜索的范围不只是我们的项目文件,我们要将范围拓展到所有位置

所有位置的话,它就包括了外部库里的那些东西

而后,会冒出来一堆类

但要注意分辨,我们要找的是Minecraft的源代码,不是其他的库

它是net.minecraft.item下的类,注意看各个类后面的包的名字

1

不过,我们要找的其实并不是Item这个类,而要找Items

这两个类都存在,也都是Minecraft的类,随着我们教程的开展,你还会发现另外的,
例如BlockBlocksItemGroupItemGroups等等

感兴趣的同学可以先自己去研究一下,我这里的话也简单说明一下

一般来说,带s的是Minecraft的注册类,而不带s的是定义各个类的基本属性

比如Item类,你可以在这个类中发现物品的一些基本属性,像默认最大堆叠数量64默认最大使用次数32稀有度等等

还有各种各样的方法,到时候我们讲解自定义物品类的时候可以重写它们,来实现自己的一些逻辑

Items类,这是物品的注册类,这里注册了所有的Minecraft物品,你会发现清一色的public static final字段,以及一系列的register方法

源代码中的注册方法

那么register翻译翻译,就是注册的意思

所以现在,我们就来看看源代码中的注册方法

不过先得挑一个,因为在这里不只有单纯的物品,还有像方块物品这样的,这个东西我们在后面讲方块的时候会讲到

还有像一些物品实例化的并不是Item类,而是实例化它们自己的类,像我们之后会讲到的盔甲武器

所以这里的话,我们挑一个简单的,几乎没什么特别的属性物品,DIAMOND,也就是钻石

搜索的快捷键是CTRL + F,搜索diamond,可以再加一个空格,就能定位到钻石这个物品了

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

register里面的东西我们先不用看,先看这个方法本身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Item register(String id, Item item) {
return register(new Identifier(id), item);
}

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

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);
}

DIAMOND使用的方法是第一个register,同名的重载方法有一堆,也层层叠叠调用了一堆方法

最终调用了最后一个register方法,也就是我们真正要看的那个方法

1
Registry.register(Registries.ITEM, key, item);

这里的RegistryMinecraft的注册类,在这个类的注释中,也有提示,告诉我们物品该怎么注册

不过,我们先不管他,还是回过头来看这个方法

这个方法接受三个参数,第一个是注册表,第二个是注册键,第三个是注册的物品

RegistriesMinecraft的注册表类,它里面定义了所有Minecraft的注册表,
之后我们注册方块物品栏都会用到其中的字段

至于第二个注册键,我们可以根据上面三个方法进行整合

1
Registry.register(Registries.ITEM, RegistryKey.of(Registries.ITEM.getKey(), new Identifier(id)), item);

这里传入的idString类型的,然后,实例化Identifier类,传入id

Identifier类

那么在这里,我们就要重点讲讲Identifier类了

这是定义Minecraft中所有资源文件路径的类,我们可以看看它的注释

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

它这里也说了,这个类也被称为资源位置命名空间ID位置或者ID

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

它的格式也在注释这有说明,就是<命名空间>:<路径>,命名空间缺省的话,默认为minecraft

在之后的教程中,我提到的所有关于命名空间的事情,都是从这个类里来的

而我们模组命名空间,则是我们的modid

1
2
3
4
5
6
7
8
9
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 /.

敲黑板,划重点了

为什么说命名空间能成为模组开发之路上的第二块绊脚石(第一块是Gradle)?

因为不少人在定义物品方块的名字,其他的一些命名空间等东西的时候,会写非法字符

这里的注释说的很清楚了,命名空间和路径只能包含ASCII小写字母[a-z]
ASCII数字[0-9]下划线[_]点[.]短横线[-]

而路径还可以包含标准路径分隔符/

其余的全是非法字符,包括大写字母

所有,定义命名空间及路径的时候看清楚,不要写非法字符,再写我可要嘲笑你了

物品的基本属性

好,看完Identifier类,我们再回过头来看物品具体注册时候实例化的方法

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

还是DIAMOND,第一个参数是它的名字,后面实例化了一个Item类,并传入了一个Item.Settings()

在这个Settings()中就是物品的基本属性的类,嵌套在Item类中

1
2
3
4
5
6
7
8
9
10
11
12
public static class Settings {
int maxCount = 64;
int maxDamage;
@Nullable
Item recipeRemainder;
Rarity rarity = Rarity.COMMON;
@Nullable
FoodComponent foodComponent;
boolean fireproof;
FeatureSet requiredFeatures = FeatureFlags.VANILLA_FEATURES;
...
}

可以看到,默认定义好的最大堆叠数量64,稀有度为普通,另外的属性可以通过下面的方法加入,也可以更改默认的这两个

不过,具体的我们到后面再说,因为现在我们只需要注册一个最简单的物品即可

注册第一个物品

好了,前面铺垫工作基本完成了

你看,要讲的东西其实很多很多,要换种教程模式的话,我就直接从这里开始讲了

创建注册物品类

现在我们来真正开始写第一个物品,和原版一样,我们也先创建一个用于注册物品的类ModItems

1
2
3
public class ModItems {

}

然后我们把原版的注册方法搬过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static Item register(String id, Item item) {
return register(new Identifier(TutorialMod.MOD_ID, id), item);
}

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

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);
}

当然,这里的命名空间改成了我们模组的MOD ID,不然到后面找你资源文件的时候就跑到Minecraft命名空间下去了

优化注册方法

不过,上面我也提了一下整合方法,这四个方法其实是可以整合为一个方法的

1
2
3
public static Item registerItems(String id, Item item) {
return Registry.register(Registries.ITEM, RegistryKey.of(Registries.ITEM.getKey(), new Identifier(TutorialMod.MOD_ID, id)), item);
}

什么?还能优化?还有高手?

是的,Registry.register其实还有一个简化的重载方法,我们可以先看一下

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

static <V, T extends V> T register(Registry<V> registry, RegistryKey<V> key, T entry) {
((MutableRegistry)registry).add(key, (V)entry, Lifecycle.stable());
return entry;
}

这里的第二个是我们目前使用的方法,而第一个调用的也是第二个方法,唯一的区别就是第一个方法的第二个参数为Identifier类型,而第二个方法的第二个参数为RegistryKey类型的

我们可以直接利用第一个方法来简化我们的代码,顺带一提,它也是我们在后面讲方块时,原版采用的注册方法

1
2
3
public static Item registerItem(String id, Item item) {
return Registry.register(Registries.ITEM, new Identifier(TutorialMod.MOD_ID, id), item);
}

注册物品

好,现在我们就可以开始注册我们的第一个物品了

DIAMOND的写法一样,写一个最简单的物品

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

还是我们的老朋友ICE ETHER

那么,我们举一反三,再写一点

1
2
public static final Item RAW_ICE_ETHER = registerItem("raw_ice_ether", new Item(new Item.Settings()));
public static final Item CARDBOARD = registerItem("material/cardboard", new Item(new Item.Settings()));

我们再加入RAW ICE ETHERCARDBOARD
其中CARDBOARD我加了一个路径material,我们在后面写模型文件的时候会看到它和另外两个的区别

初始化注册方法

我们的物品已经写好了,但还没用,你到游戏中还是找不到添加的物品

因为我们这个类没有初始化

这里我们再写一个无返回值的空方法

1
2
3
public static void registerItems() {

}

随后,到模组主类onInitialize方法中去调用它

1
2
3
4
5
6
7
@Override
public void onInitialize() {

ModItems.registerItems();

LOGGER.info("Hello Fabric world!");
}

onInitialize方法会在模组加载的时候调用,而此时我们的方法也会被调用

那么为什么加了这个方法就可以完成初始化了呢?

因为你会发现,我们写的这三个物品注册,都是static final修饰的,这个类被调用了,那么它们就必须初始化

初始化的结果呢,就是调用注册方法,从而让物品注册到游戏中

资源文件

物品注册到这里就好了,不过现在进入游戏,
虽然可以获得你的物品,但你会发现它是一个大大的黑紫块,名字还老长一串

因为现在,我们的物品没有模型,也没有材质

原版的资源文件都可以在外部库中找到,在其assets文件夹下,另外一边的data则为数据文件

那么我们的模组也有一个assets文件夹,这里是存放我们模组的资源文件

语言文件

这里我们先来写语言文件,在assets/<modid>下新建一个lang文件夹,然后新建一个en_us.json文件

注意文件路径Minecraft的文件读取是有严格的规范的,你不能自己乱写

这里的en_us.json文件是英文语言文件,这是默认的语言文件,在其他语言文件缺省时,会默认使用它

当然你也可以新建一个zh_cn.json文件,用于简体中文的语言文件

1
2
3
4
5
{
"item.tutorial-mod.ice_ether": "Ice Ether",
"item.tutorial-mod.raw_ice_ether": "Raw Ice Ether",
"item.tutorial-mod.material.cardboard": "Cardboard"
}

这里我们定义了三个物品的名称,其中item.tutorial-mod.ice_ether是实际注册在Minecraft中的名字

它是由你注册的类型.你的MOD ID.你的物品名字组成的

也就是你不写语言文件则会显示这么长一串,而后面的值则会翻译这一串东西

中文的语言文件同样如此,这里就不再赘述

物品模型

语言文件写好了,现在我们再来看模型文件

assets/<modid>/models/item下新建一个ice_ether.json文件,用于存放ICE ETHER的模型文件

1
2
3
4
5
6
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "tutorial-mod:item/ice_ether"
}
}

这个模型继承自minecraft:item/generated,也就是原版物品的模型

它是原版最基本的物品模型,也就是在你贴图基础上加厚一层

其中layer0是贴图,我们这里贴图路径是tutorial-mod:item/ice_ether

那么RAW ICE ETHER的模型文件也差不多

1
2
3
4
5
6
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "tutorial-mod:item/raw_ice_ether"
}
}

CARDBOARD呢?前面我们多加了一个路径

所以,在这里我们就要在assets/<modid>/models/item/material下新建一个cardboard.json文件

注意文件路径多了一个material

1
2
3
4
5
6
{
"parent": "minecraft:item/generated",
"textures": {
"layer0": "tutorial-mod:item/material/cardboard"
}
}

我在这里加这么一个路径的原因呢很简单,一方面让大家再感受一下Identifier的用法,
另一方面呢,也是方便管理

虽然我们现在的东西不多,但你自己的开发过程中,或许会有成百上千个东西要写,不给它分类的话,如果要修改某些文件,找就得找半天

方舟家具模组全面重写之后,也就采用了这种方法,毕竟成百上千个模型,后面要改的话,光是找就得要命了

不过,这里贴图路径的material可以不写,为了方便管理也可以写

物品材质

物品材质的绘制的话,Photoshop就可以画,也可以拿专业的像素画软件来画,如Aseprite

我们要将贴图文件放在assets/<modid>/textures/item

另外,如果上面cardboard的贴图文件路径不加material的话,则和另外两个放一起就行

如果写上,则得将贴图放在assets/<modid>/textures/item/material

启动游戏测试

那么现在,我们就可以启动我们的游戏去测试一下

由于我们现在没有将物品加入到创造模式物品栏中,只能通过give命令来获得物品

give输入我们的物品时,命名空间改为我们模组的modid,而不是minecraft

另外,以后当你找不到你新加的物品时,也可以用这种方法来检测一下

成功注册的物品都可以用give命令来获得