第一个物品 26.1 Fabric
本篇教程的视频:
本篇教程源代码
GitHub地址:Tutorial Mod - Item
本篇教程目标
- 理解原版物品注册
- 学会物品注册
查看源代码
当我们实际开发时,假设我们什么也不知道,Wiki也没看过,不知道怎样添加我们的物品。那么怎么办呢?看源代码
在所有的Fabric教程系列中,我们一般都会从底层的注册开始讲,所以这里我们来看看物品是怎么注册的
前面一期教程的最后,我们也说了查找源代码的方法——翻外部库或者随处搜索(使用方法见前一篇教程)
那么这里,我们来查看Items这个类(注意是Minecraft包中的类)
这个类是Minecraft中所有物品的注册的类
不过,也许你在搜索的时候会发现还有一个Item类,这个类是物品的基类,也就是说这个类是定义所有物品都会有的属性和方法的类。
而特殊的物品也是继承这个类的。
在Minecraft中,带s的复数形式的类一般是用于注册的类,比如我们之后会讲的Blocks、CreativeModeTabs等;
而不带s的类一般是用于定义的类,比如我们之后会讲的Block等。
查找一个物品
使用查找的快捷键Ctrl + F,我们来找到DIAMOND
1 | public static final Item DIAMOND = registerItem("diamond", new Item.Properties().trimMaterial(TrimMaterials.DIAMOND)); |
这一条语句就是注册DIAMOND物品的语句
方法里面一个是字符串,一个是Item.Properties,后者是物品的属性,在未来的教程中我们再逐步讲解
这里可以看到,DIAMOND是一个Item类型的常量,它的值是registerItem方法的返回值
那么我们就来看看registerItem方法
查看注册方法
按住Ctrl键,点击registerItem方法,我们就可以跳转到registerItem方法的定义处
1 | private static Item registerItem(final String name, final Item.Properties properties) { |
我们可以发现它调用了另外一个重载方法
实际上,这里有一堆的registerItem重载,因为你可以看到各种Item五花八门的注册语句(包括物品和方块对应的方块物品)
这里的vanillaItemId方法,是去生成了一个物品的ID
1 | private static ResourceKey<Item> vanillaItemId(final String name) { |
这里的Identifier,也是我们熟知的ResourceLocation,即命名空间,这里我们到后面再展开讲
不过,最终所有的registerItem指向的都是最后一个registerItem(在代码文件中的相对位置)
1 | private static Item registerItem(final ResourceKey<Item> key, final Function<Item.Properties, Item> itemFactory, final Item.Properties properties) { |
这里可以看到,registerItem方法最终调用了Registry.register方法
当然,如果学习过之前的低版本的模组开发,看到这里应该能注意到它的不同
Function<Item.Properties, Item> itemFactory
我们首先可以注意到第二个参数的不同,现在它从之前的Item类型,变成了Function类型
Function是Java中的函数式接口(Java 8就有了),其泛型的第一个参数是函数输入的类型,第二个是函数结果的类型
一行新代码
1 | Item item = (Item)itemFactory.apply(properties.setId(key)); |
这个是相对于1.21.2之前的版本新增的
结合上面的Function的解释,它就是根据传入的Item.Properties创建并返回一个新的Item实例,类似于直接new Item
再通过apply方法,在已配置的Item.Properties基础上再加上一个setId方法,这个方法就是用来给物品配置注册键的
虽然我上面说它类似于直接new Item,那么为什么不直接这么写呢?
对于实际开发而言,如果直接new Item这就太局限了,因为像一些武器、工具,比如剑、斧头、镐子等,它们都有自己的物品类,这些类虽然是Item的子类,但要实现它们特有的逻辑,并不能简单地直接new Item
所以这里就用了Function来实现更加灵活的注册
对于这里的setId方法,我们展开讲讲,下面是当时Fabric Blog上的介绍
Minecraft 1.21.2 uses registry keys to pre-compute certain block or item settings.
This allows referencing them in default item components.
For example, rather than computing a default name based on an item’s registry key insidegetNamemethod if theminecraft:item_namecomponent is not present,
every item now contains theminecraft:item_namecomponent.
简单来说,自Minecraft 1.21.2中使用registry keys(注册键)来预处理物品和方块的一些设置
现在物品和方块都有了一个与他们注册键相关联的组件,其实就是他们的名字
这个名字呢,在语言文件缺失时,就会以item.命名空间.物品注册名的形式显示
之前的版本中,有些地方要用到这个组件(比如显示名字),如果这个组件缺失,就会调用getName方法来生成一个默认的名字
但是现在,所有物品和方块都包含这个组件了,如果你不给物品和方块设置注册键,游戏就直接崩溃了
所有现在物品必须加上setId方法,以正确配置它的注册键
不然你启动游戏就会出现Item id not set的错误
注册物品
创建ModItems类
那么我们现在就来创建一个ModItems类,用于注册我们的物品。
1 | public class ModItems { |
写注册方法
然后我们来写注册方法,如果你不想像原版的那些注册方法那样,搞那么多重载,可以按照我的整合版来写,可以稍微简洁一点
1 | private static Item registerItem(final String name, final Function<Item.Properties, Item> itemFactory, final Item.Properties properties) { |
这里我们写了三个,其中第一个实际上是原版的最终方法,不过我把ResourceKey的创建给扒拉到方法里面了,这样第一个形参就可以用String了
另外的两个都是调用第一个方法,不同程度的简化
Identifier
Identifier是一个极其重要的类
我这里引用一下以前版本的注释(因为现在没了混淆,导致原来yarn映射下的注释都没了)
An identifier used to identify things. This is also known as “resource location”, “namespaced ID”, “location”, or just “ID”.
Identifiers are formatted as
: . If the namespace and colon are omitted, the namespace defaults to “minecraft”.
这是Identifier的注释,说白了,它就是我们常说的命名空间 + id(我们自己物品、方块或者其他东西的),或者说一些特定文件的路径
这里的格式(Format)是<namespace>:<path>,如果省略了命名空间和冒号,那么命名空间默认为minecraft
而再往下,重点的注释是这个
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_.-] ...异常。
而什么黑紫块、找不到文件、无法显示等等,都是因为命名空间和路径的问题,那些东西要重点检查
回过头去看vanillaItemId方法,它调用的是Identifier.withDefaultNamespace方法
1 | public static Identifier withDefaultNamespace(final String path) { |
那么它这里的默认命名空间就是minecraft
而看我们调用的Identifier.fromNamespaceAndPath
1 | public static Identifier fromNamespaceAndPath(final String namespace, final String path) { |
传入的是我们自己的命名空间,当然,它会通过另外的方法检测命名空间和路径是否合法
注册物品
好了,折腾半天,接下来我们就来注册物品吧
1 | public static final Item ICE_ETHER = registerItem("ice_ether"); |
这里我们注册三个物品,都调用那个最简单的方法,因为这些都是最简单的物品
注意!前面也说过了,不要有大写字母!不要有大写字母!不要有大写字母!
不过,其中的CARDBOARD稍微有些特殊,因为这个物品的注册名是material/cardboard,这个我们会在之后的数据文件编写中看出它们的区别
初始化注册方法
接下来我们还要写一个方法
1 | public static void register() { |
这个方法需要被主类的onInitialize方法调用
1 |
|
主类的onInitialize方法是在游戏启动的时候被调用的,所以我们在这里调用我们的初始化方法。
这也是利用Java的特性——当我们调用一个类的方法的时候,这个类会被初始化
这个类的静态代码块也会被初始化,而我们的物品是static final修饰的,所以在这个时候,我们的物品也就完成了注册
当然在register方法里面我们可以写点东西,比如日志输出、添加物品栏等
1 | public static void register() { |
有日志输出的话,你就可以看到这个方法是在何时被调用的
至于添加物品到物品栏,这里我们先用Fabric的CreativeModeTabEvents事件,将物品添加到原版的物品栏里面
后面的教程我们再来自己创建自定义物品栏
就按照我前面说的,CreativeModeTabs是原版物品栏的注册类,感兴趣的同学可以预先研究一下
数据文件
那么我们的物品注册完了,但是我们现在进入游戏会发现一个黑紫块,所以我们还需要它的资源文件,包括模型文件、语言文件和贴图文件
接下来我们就来写这些物品的数据文件,当然,未来大多数数据文件我们都会使用Data Gen,也就是数据生成去跑,而不是手搓
当然,前期教程为了让大家熟悉这些文件里面有什么东西,所以这里我们还是会手搓一些数据文件
模型文件
我们先来写模型文件,我们可以先看原版的物品模型文件,然后修改一下。比如说这个diamond.json文件
1 | { |
稍加改动,我们就可以得到我们的物品模型文件
路径是assets/tutorial/models/item/ice_ether.json
1 | { |
注意!文件名需要和我们的物品注册名一致
parent指的是父模型,generated是一般物品模型,也就是在二维图的基础上给你加厚了一层
textures指的是材质,layer0指的是材质的层,tutorial:item/ice_ether指的是材质的路径,命名空间和路径
另外的raw_ice_ether.json:
1 | { |
而cardboard有些特殊,还记得我们在注册时写的是material/cardboard吗?
那么相应的,它的文件位置是assets/tutorial/models/item/material/cardboard.json
1 | { |
cardboard的贴图文件是assets/tutorial/textures/item/material/cardboard.png
这里我举这个例子其实是为了说明我们可以对一些内容进行分级,这样当你有成百上千个模型时,通过目录分级可以更好地归类整理,也方便后期维护
贴图文件
贴图拿PS这种软件画一个贴图就行了,然后放到assets/tutorial/textures/item文件夹下(注意cardboard.png还有一层)
贴图文件的名字要和模型文件中的layer0的值一样,不然游戏会找不到贴图文件,导致物品显示不出来。
不过值得注意的是,贴图的格式要是PNG格式,不然无法加载,分辨率使用2^N,比如16x16、32x32、64x64等等
不要取个诡异的分辨率,比如说17x17,虽然不会报错,但会有警告
语言文件
然后我们来写语言文件,
我们可以先看原版的物品语言文件,然后修改一下。比如说这个en_us.json文件
1 | { |
稍加改动,我们就可以得到我们的物品语言文件
路径是assets/tutorial/lang/en_us.json
1 | { |
en_us是英文(美式)语言文件,也是默认情况下会使用的语言文件。也就是说假设你的游戏是中文的,但缺失了中文的语言文件,它会采用英文的语言文件进行显示
如果你要支持其他语言,可以在这个文件夹下新建另一个文件
比如简体中文是zh_cn.json,然后把en_us.json的内容复制过去,然后翻译一下就行了。
假设说你不写语言文件,那么游戏会直接显示物品的注册名,比如ice_ether会显示item.tutorial.ice_ether这一串
这里还是要注意cardboard,因为多了一层material,所以它的注册名是item.tutorial.material.cardboard
物品描述文件
特别的,自1.21.4开始,我们还需要写物品描述文件(Item Model Description),现在也被称为客户端物品(Client Item)
实际上就是告诉游戏去哪里找我们的模型文件,用于在物品栏中的显示
这些文件的统一路径是assets/tutorial/items
各物品的描述文件还是与注册名同名,我们来写一个ice_ether.json文件
1 | { |
type指的是模型类型,model指向我们物品的模型文件
raw_ice_ether.json:
1 | { |
特别的,cardboard.json的位置是assets/tutorial/items/material/cardboard.json
1 | { |
测试
那么我们现在就可以启动游戏了,看看我们的物品是否注册成功
我们可以到原版物品栏中的原材料一栏的最后找到我们新添加的三个物品
如果它们都显示正常,没有黑紫块的问题,那么恭喜你,你成功注册物品
另外,在常规开发过程中,/give命令可以用来测试物品、方块的注册情况。因为它一旦注册成功,那么就可以通过这个命令来获取这个物品,有时候如果你找不到自己注册的物品可以用指令试试











