提醒一下

本教程相当于是我在写笔记

什么是物品

像这个一格一格我们都可以统称物品,不管是方块物品还是普通物品都是物品
image-20241113172802407

这节我们先实现在原版物品栏中添加物品

查找源代码

首先创建第一个物品最先要做的得是什么
怎么创建对吧

就好比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中所有物品的注册的类。

50991010-f4b0-4476-a6ae-43e4fda26d22

随便挑几个简单的来讲,比如第二,三个字段STONEGRANITE即mc里面的石头花岗岩

这两个字段通过register这个方法来进行注册,里面的形参简单的都是些Blocks.XX简单易懂的命名都是一些将方块注册成物品

Item类

这些字段都是一个Item类,这个类是物品的基类,也就是说这个类是定义所有物品都会有的属性和方法的类。
而特殊的物品也是继承这个类的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Item implements ToggleableFeature, ItemConvertible, FabricItem {
private static final Logger LOGGER = LogUtils.getLogger();
public static final Map<Block, Item> BLOCK_ITEMS = Maps.<Block, Item>newHashMap();
public static final Identifier BASE_ATTACK_DAMAGE_MODIFIER_ID = Identifier.ofVanilla("base_attack_damage");
public static final Identifier BASE_ATTACK_SPEED_MODIFIER_ID = Identifier.ofVanilla("base_attack_speed");
public static final int DEFAULT_MAX_COUNT = 64;
public static final int MAX_MAX_COUNT = 99;
public static final int ITEM_BAR_STEPS = 13;
private final RegistryEntry.Reference<Item> registryEntry = Registries.ITEM.createEntry(this);
private final ComponentMap components;
@Nullable
private final Item recipeRemainder;
@Nullable
private String translationKey;
private final FeatureSet requiredFeatures;
...
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
2
public static final Identifier BASE_ATTACK_DAMAGE_MODIFIER_ID = Identifier.ofVanilla("base_attack_damage");
public static final Identifier BASE_ATTACK_SPEED_MODIFIER_ID = Identifier.ofVanilla("base_attack_speed");
  • BASE_ATTACK_DAMAGE_MODIFIER_IDBASE_ATTACK_SPEED_MODIFIER_ID:两个静态的 Identifier 对象,分别表示基础攻击力和基础攻击速度的标识符。Identifier.ofVanilla 方法用于创建一个标准的标识符。

    1
    2
    3
    public 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注册的类,比如我们之后会讲的BlocksItemGroups等;
而不带s的类一般是用于定义的类,比如我们之后会讲的Block等。

register注册

看这个注册方块物品,大家肯定都意识到了我们不是注册物品吗

对,所以我们得看一个简单的原材料物品来加深我们的认识,例如DIAMOND钻石

我们通过Ctrl+F查找快捷键来查找diamond,通过代码的命名我们可以排除不是想找的物品

最终我们锁定在第919

image-20241111113619107

来到这个我们发现还是有register这个方法看来是逃不掉了,那我们可以按住Ctrl+鼠标左键查看register注册方法来弄懂如何注册物品的

image-20241111115513235

我们跳转到这个类的末尾,我们目前先只需要关注2019行下面的,上面都是方块的一些注册吧

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(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
2
3
public static Item register(String id, Item item) {
return register(Identifier.ofVanilla(id), 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
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);
}
  • 参数
  • 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 对象。

总结

  • 注册流程
  1. register(String id, Item item):将字符串 id 转换为 Identifier,然后调用 register(Identifier id, Item item)
  2. register(Identifier id, Item item):将 Identifier 转换为 RegistryKey<Item>,然后调用 register(RegistryKey<Item> key, Item item)
  3. register(RegistryKey<Item> key, Item item):检查 item 是否是 BlockItem,如果是则将其添加到 BLOCK_ITEMS 映射中,最后将 item 注册到 Registries.ITEM 注册表中。
  4. Item是一直没有变的
  5. 从后往前看就是image-20241111153533618

物品注册

ModItems

懂清楚之后我们创建一个类开始我们的注册

首先创建一个包item,存放在mod名文件夹下

创建一个ModItems类,用于注册我们的物品。

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

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

1
2
3
4
5
public class ModItems {
private static Item registerItems(String id, Item item){
return Registry.register(Registries.ITEM, RegistryKey.of(Registries.ITEM.getKey(), Identifier.ofVanilla(id)), item);
}
}

关于Registry接口中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
4

{
"item.minecraft.diamond": "Diamond"
}

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

1
2
3
4

{
"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,虽然不会报错,但会有警告

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

测试

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

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

1
/give @s tutorialmod:ice_ether

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

[success]

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