第一个物品
提醒一下
本教程相当于是我在写笔记
在遇到问题的时候可以选择,先阅读fabricmc官网的文档
以及通过工具如AI,chatGPT去搜索一下,或者去必应一下
也可以先去b站搜索一下,b站上有很多大佬,可以去学习一下
本教程适用于Minecraft 1.21 Fabric的模组开发,不是Java教程。在开始之前也请先学习Java,具有一定的Java基础后再来学习
在此真诚感谢北山大佬的模组开发教程
什么是物品
像这个一格一格我们都可以统称物品,不管是方块物品还是普通物品都是物品
这节我们先实现在原版物品栏中添加物品
查找源代码
首先创建第一个物品最先要做的得是什么
怎么创建对吧
就好比sout打印Hello World
一样,至少你得认识哪个是打印
java可以看API文档,我们Minecraft可以看wiki
看源代码
既然在IDEA中我们可以就近选择看源代码
即翻我们的外部库
或者双击Shift键
,从而弹出全局搜索框
外部库里面Gradle: net.minecraft:minecraft-merged-4eb0fe4bb6:1.21-net.fabricmc.yarn.1_21.1.21+build.1-v2
Items类
首先,我们来查看Items
这个类(注意是Minecraft包中的类)。这个类是Minecraft中所有物品的注册的类。
随便挑几个简单的来讲,比如第二,三个字段STONE
,GRANITE
即mc里面的石头
和花岗岩
这两个字段通过register
这个方法来进行注册,里面的形参简单的都是些Blocks.XX
简单易懂的命名都是一些将方块注册成物品
Item类
这些字段都是一个Item
类,这个类是物品的基类,也就是说这个类是定义所有物品都会有的属性和方法的类。
而特殊的物品也是继承这个类的。
1 | public class Item implements ToggleableFeature, ItemConvertible, FabricItem { |
1 | private static final Logger LOGGER = LogUtils.getLogger(); |
LOGGER
:一个静态的Logger
对象,用于记录日志信息。LogUtils.getLogger()
是一个获取日志记录器的方法。
1 | public static final Map<Block, Item> BLOCK_ITEMS = Maps.<Block, Item>newHashMap(); |
BLOCK_ITEMS
:一个静态的Map
,键是Block
对象,值是Item
对象。这个映射用于存储块(Block)和对应的物品(Item)之间的关系。
1 | public static final Identifier BASE_ATTACK_DAMAGE_MODIFIER_ID = Identifier.ofVanilla("base_attack_damage"); |
BASE_ATTACK_DAMAGE_MODIFIER_ID
和BASE_ATTACK_SPEED_MODIFIER_ID
:两个静态的Identifier
对象,分别表示基础攻击力和基础攻击速度的标识符。Identifier.ofVanilla
方法用于创建一个标准的标识符。1
2
3public static final int DEFAULT_MAX_COUNT = 64;
public static final int MAX_MAX_COUNT = 99;
public static final int ITEM_BAR_STEPS = 13;DEFAULT_MAX_COUNT
:默认的最大堆叠数量,通常是 64。MAX_MAX_COUNT
:最大允许的最大堆叠数量,通常是 99。ITEM_BAR_STEPS
:物品栏中的步数,通常用于显示物品的耐久度等信息。
在Minecraft中,带s
的复数形式的类一般是用于register
注册的类,比如我们之后会讲的Blocks
、ItemGroups
等;
而不带s
的类一般是用于定义的类,比如我们之后会讲的Block
等。
register注册
看这个注册方块物品,大家肯定都意识到了我们不是注册物品吗
对,所以我们得看一个简单的原材料物品来加深我们的认识,例如DIAMOND
即钻石
我们通过Ctrl
+F
查找快捷键来查找diamond,通过代码的命名我们可以排除不是想找的物品
最终我们锁定在第919
行
来到这个我们发现还是有register
这个方法看来是逃不掉了,那我们可以按住Ctrl
+鼠标左键
查看register
注册方法来弄懂如何注册物品的
我们跳转到这个类的末尾,我们目前先只需要关注2019行下面的,上面都是方块的一些注册吧1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public static Item register(String id, Item item) {
return register(Identifier.ofVanilla(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);
}
仔细看上面三个方法
下面是对每个方法的详细解析:
1. register(String id, Item item)
1 | public static Item register(String id, Item item) { |
参数
id
:一个字符串,表示物品的唯一标识符。item
:要注册的Item
对象。
功能
将字符串
id
转换为Identifier
对象,然后调用register(Identifier id, Item item)
方法进行注册。Identifier.ofVanilla(id)
:创建一个标准的Identifier
对象,通常用于游戏中的资源标识。```java
public static Identifier ofVanilla(String path) {return new Identifier("minecraft", validatePath("minecraft", path));
}
1
2
3
4
5
6
7
8
9
#### 2. `register(Identifier id, Item item)`
```java
public static Item register(Identifier id, Item item) {
return register(RegistryKey.of(Registries.ITEM.getKey(), id), item);
}
参数
id
:一个Identifier
对象,表示物品的唯一标识符。item
:要注册的Item
对象。
功能
- 将
Identifier
对象转换为RegistryKey<Item>
对象,然后调用register(RegistryKey<Item> key, Item item)
方法进行注册。 RegistryKey.of(Registries.ITEM.getKey(), id)
:创建一个RegistryKey<Item>
对象,用于在注册表中标识物品。- 关于
Identifier
对象可以在类中详细注释,包括但不限于: 只能使用小写字母、数字、部分字符
- 将
3. register(RegistryKey<Item> key, Item item)
1 | public static Item register(RegistryKey<Item> key, Item item) { |
- 参数
key
:一个RegistryKey<Item>
对象,表示物品在注册表中的唯一标识。item
:要注册的Item
对象。
- 功能
- 检查
item
是否是BlockItem
的实例。如果是,则调用appendBlocks
方法将该物品添加到BLOCK_ITEMS
映射中。 ((BlockItem)item).appendBlocks(Item.BLOCK_ITEMS, item)
:将item
添加到BLOCK_ITEMS
映射中,以便后续使用。- 最后,调用
Registry.register(Registries.ITEM, key, item)
方法将物品注册到注册表中。 Registry.register(Registries.ITEM, key, item)
:将item
对象注册到Registries.ITEM
注册表中,并返回注册后的Item
对象。
总结
- 注册流程
register(String id, Item item)
:将字符串id
转换为Identifier
,然后调用register(Identifier id, Item item)
。register(Identifier id, Item item)
:将Identifier
转换为RegistryKey<Item>
,然后调用register(RegistryKey<Item> key, Item item)
。register(RegistryKey<Item> key, Item item)
:检查item
是否是BlockItem
,如果是则将其添加到BLOCK_ITEMS
映射中,最后将item
注册到Registries.ITEM
注册表中。Item
是一直没有变的- 从后往前看就是
物品注册
ModItems
懂清楚之后我们创建一个类开始我们的注册
首先创建一个包item
,存放在mod名文件夹下
创建一个ModItems
类,用于注册我们的物品。
然后我们来写注册方法,如果你不想整合上面的那三个方法,可以直接把上面的代码复制到ModItems类中。
不过我还是来给它整合一下,毕竟这样更加简洁一点
1 | public class ModItems { |
关于Registry
接口中register
方法中的各个参数
1 | static <V, T extends V> T register(Registry<V> registry, RegistryKey<V> key, T entry) { |
这个方法的第一个参数是Registry
,第二个参数是RegistryKey
,第三个参数是entry
。
而对应到我们自己的方法中,第一个参数是Registries.ITEM
,它是一个DefaultedRegistry<Item>
类型的常量,在Registries
类中定义。
这个类是Minecraft中所有的注册表的类,后续我们还会讲到Registries.BLOCK
、Registries.ITEM_GROUP
等。
第二个参数是RegistryKey.of(Registries.ITEM.getKey(), Identifier.ofVanilla(id))
,
这个方法是为注册表中的某个值创建注册表键值,同时创建根注册表中持有值注册表的注册表键值和值的标识符。
不过这里的Identifier
我们待会着重会讲
第三个参数是item
。也是我们后面会进行编写的物品的一些基本设置。
Identifier
Identifier
是一个极其重要的类
1 | An identifier used to identify things. This is also known as "resource location", "namespaced ID", "location", or just "ID". |
这是Identifier
的注释,说白了,它就是我们常说的命名空间
+ id
(我们自己物品、方块或者其他东西的),或者说一些特定文件的路径
这里的format
是<namespace>:<path>
,如果省略了命名空间和冒号,那么命名空间默认为minecraft
而再往下,重点的注释是这个
1 | The namespace and path must contain only ASCII lowercase letters ([a-z]), ASCII digits ([0-9]), or the characters _, ., and -. |
这里说的是命名空间和路径只能包含ASCII小写字母[a-z]
、ASCII数字[0-9]
、下划线[_]
、点[.]
和短横线[-]
。
而路径还可以包含标准路径分隔符/
。而你一旦写了其他的非法字符,启动游戏就会直接崩溃,并抛出net.minecraft.util.InvalidIdentifierException: Non [a-z0-9_.-] ...
异常。
而什么黑紫块、找不到文件、无法显示等等,都是因为命名空间和路径的问题,那些东西要重点检查。
现在我们看到自己代码中的Identifier.ofVanilla(id)
1 | public static Identifier ofVanilla(String path) { |
我们可以看到,这里的ofVanilla
方法,它的命名空间是minecraft
。
这不是我们希望看到的,我们的模组最好能够有独立的命名空间,
而且如果你的命名空间是minecraft
,那么你的物品、方块等等资源文件都得放在minecraft
文件夹下,
这样可能会和原版资源文件冲突(如果你的物品和原版物品同名的话)。
幸好,Identifier
还有一个构造方法,我们可以自己定义命名空间
1 | public static Identifier of(String namespace, String path) { |
这个方法就是我们自己定义命名空间的方法,我们可以自己定义一个命名空间,还记得我们的MODID吗?tutorialmod
在这里就可以用上了。
重新整合注册方法
那么现在我们就重新写一下那个注册方法
1 | private static Item registerItems(String id, Item item) { |
这样的话,命名空间就是我们自己定义的tutorialmod
,而不是minecraft
了。
而这个方法也就可以使用了。
不过,你是否觉得这个方法还是有点繁琐,毕竟有点长对吧?
那么其实我们还可以进一步简化,采用Registry.register
的另一个同名不同参的方法
1 | private static Item registerItems(String name, Item item) { |
而这个register
方法调用的是我们前面写的那个register
方法,本质上是一样的。
1 | static <V, T extends V> T register(Registry<V> registry, Identifier id, T 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 | public static void registerModItems(){ |
这个方法就是我们的初始化方法,这里写了一个日志输出,用于在启动游戏的时候输出一些信息。其实这个方法空着也没事
然后到我们主类中的onInitialize
调用这个方法
1 | ModItems.registerModItems(); |
这里的onInitialize
方法是在游戏启动的时候被调用的,所以我们在这里调用我们的初始化方法。
这也是利用Java的特性,当我们调用一个类的方法的时候,这个类会被初始化。而这个类的静态代码块也会被初始化,
而我们的物品是static final
即静态常量修饰的,所以在这个时候,我们的物品也就完成了注册。
整体代码
1 | public class ModItems { |
资源文件
那么我们的物品注册完了,但是我们现在进入游戏会发现一个黑紫块,所以我们还需要它的资源文件,包括模型文件、语言文件和贴图文件。
模型文件
我们先来写模型文件,我们可以先看原版的物品模型文件,然后修改一下。比如说这个diamond.json
文件
1 | { |
稍加改动,我们就可以得到我们的物品模型文件,
路径是src/main/resources/assets/tutorialmod/models/item/ice_ether.json
1 |
|
语言文件
然后我们来写语言文件,
我们可以先看原版的物品语言文件,然后修改一下。比如说这个en_us.json
文件
1 |
|
稍加改动,我们就可以得到我们的物品语言文件,
路径是src/main/resources/assets/tutorialmod/lang/en_us.json
1 |
|
那么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,虽然不会报错,但会有警告
不想自己画就拿这里的好了
测试
那么我们现在就可以启动游戏了,看看我们的物品是否注册成功。
因为我们的物品并没有加入到任何物品栏中,所以我们也只能使用指令去获取这个物品。
使用/give
命令来给自己一个物品,看看是否显示正常。
1 | /give @s tutorialmod:ice_ether |
如果你能够得到一个带有正确材质的物品,那么恭喜你,你的物品注册成功了
[]
另外,在常规开发过程中,/give
命令可以用来测试物品、方块的注册情况。因为它一旦注册成功,那么就可以通过这个命令来获取这个物品。