本篇教程的视频
(待发布)
本篇教程的源代码
(待发布)
本篇教程目标
为方块实体添加自定义的配方类型,不再直接使用硬编码的形式
介绍
还记得当时在配方那期教程中讲的type吗?每个方块实体能识别的配方就是由这个type决定的
那么在上期教程中,我们已经初步完成了一个方块实体,但是它的工作方式是由我们直接硬编码的,它只能识别钻石矿,而且也只能产出矿石
显然,这种做法太不优雅了,如果要用硬编码的形式来写,不仅会产生一堆史山代码,还不利于玩家拓展(或者是整合包修改)
所以,接下来我们就来为方块实体配置自定义的配方类型
配方类
相比较于前面的方块实体,写配方类倒是简单一点了,但仍然有很多地方容易出错,尤其是序列化与反序列化阶段
新建类
我们先创建一个OreRigRecipe,继承自Recipe,泛型为SimpleInventory
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| public class OreRigRecipe implements Recipe<SimpleInventory> { @Override public boolean matches(SimpleInventory inventory, World world) { return false; }
@Override public ItemStack craft(SimpleInventory inventory, DynamicRegistryManager registryManager) { return null; }
@Override public boolean fits(int width, int height) { return false; }
@Override public ItemStack getOutput(DynamicRegistryManager registryManager) { return null; }
@Override public Identifier getId() { return null; }
@Override public RecipeSerializer<?> getSerializer() { return null; }
@Override public RecipeType<?> getType() { return null; } }
|
这里要重写比较多的方法,不过也不是很复杂,我们一个个来看
添加字段
我们先添加一些必要的字段,并创建一个构造函数
1 2 3 4 5 6 7 8 9
| private final Identifier id; private final Ingredient input; private final ItemStack output;
public OreRigRecipe(Identifier id, Ingredient input, ItemStack output) { this.id = id; this.input = input; this.output = output; }
|
这里我们添加了id,input,output三个字段,分别对应配方类型id,输入物品,输出物品
那么为什么这里的输入和输出一个是Ingredient,一个是ItemStack呢?实际上这是为了贴合原版,这样我们就可以复用原版的一些序列化与反序列化方法了,就不用自己搓了,实际上是差不多的
当然,也可能有人会问,不是说这个案例是无输入单输出吗,咋还有输入呢?这里的输入实际上指的是矿机下方的方块,这个输入的概念和常规的塞个物品进方块实体的输入不太一样
重写 matches
1 2 3 4 5
| @Override public boolean matches(SimpleInventory inventory, World world) { if (world.isClient()) return false; return input.test(inventory.getStack(0)); }
|
这个方法是用来判断配方是否匹配,一般我们会选择用输入的物品来判断,毕竟输入对了输出才能对应
重写 craft
1 2 3 4
| @Override public ItemStack craft(SimpleInventory inventory, DynamicRegistryManager registryManager) { return output.copy(); }
|
这个方法会返回配方的输出物品,这里我们返回output.copy()
问题来了,为什么还要额外加一个copy()呢?这是为了避免共享同一个可变的ItemStack实例,防止数据污染,避免一些莫名其妙的bug(有时这些bug还难以复现)
重写 fits
1 2 3 4
| @Override public boolean fits(int width, int height) { return true; }
|
这个方法是判断配方能否放进一个给定尺寸的物品槽网格中(比如工作台3×3的网格),主要用于原生的合成界面和配方书的过滤
不过对于我们这种自定义配方实际上不会有什么作用,返回true是常见的做法(更何况我们这里还是单槽)
重写 getOutput
1 2 3 4
| @Override public ItemStack getOutput(DynamicRegistryManager registryManager) { return output; }
|
这个方法会返回配方的输出物品,这里我们返回output
回去看我们上面的craft方法,可以进一步说明为什么要使用output.copy(),如果两个方法同时返回output,这样会导致引用的是同一个ItemStack实例,有可能就会出问题
重写 getId
1 2 3 4
| @Override public Identifier getId() { return id; }
|
这个很简单,我就不说了
新建内部类 Serializer
在重写getSerializer方法之前,我们需要创建一个Serializer,这个Serializer会负责序列化与反序列化这个配方
新建类
这个类要继承RecipeSerializer,泛型为OreRigRecipe
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public static class Serializer implements RecipeSerializer<OreRigRecipe> { @Override public OreRigRecipe read(Identifier id, JsonObject json) { return null; }
@Override public OreRigRecipe read(Identifier id, PacketByteBuf buf) { return null; }
@Override public void write(PacketByteBuf buf, OreRigRecipe recipe) {
} }
|
添加字段
这里我们还需要添加两个字段
1 2
| public static final Serializer INSTANCE = new Serializer(); public static final String ID = "ore_rig";
|
它们之后注册需要使用,然后getSerializer也要返回这个INSTANCE
重写第一个 read
第一个read会从Json中读取配方
1 2 3 4 5 6
| @Override public OreRigRecipe read(Identifier id, JsonObject json) { Ingredient input = Ingredient.fromJson(json.get("input")); ItemStack output = ShapedRecipe.outputFromJson(JsonHelper.getObject(json, "output")); return new OreRigRecipe(id, input, output); }
|
这里的读取方法实际上就是原版的方法,通过这些方法去获取json文件中对应字段的输入和输出
重写第二个 read
第二个read会从PacketByteBuf中读取配方,看这个参数大家应该知道了,这是客户端读取服务端配方的方法
因为配方属于服务端数据,客户端要使用时,需要服务端将数据包发给客户端,客户端读取之后才能用
1 2 3 4 5 6
| @Override public OreRigRecipe read(Identifier id, PacketByteBuf buf) { Ingredient input = Ingredient.fromPacket(buf); ItemStack output = buf.readItemStack(); return new OreRigRecipe(id, input, output); }
|
还是读取输入和输出,不过用的是PacketByteBuf的方法
重写 write
这个方法其实就是第二个read的逆向操作,将输入和输出写入PacketByteBuf中,供服务端发给客户端使用
1 2 3 4 5
| @Override public void write(PacketByteBuf buf, OreRigRecipe recipe) { recipe.input.write(buf); buf.writeItemStack(recipe.output); }
|
这里需要严格注意顺序,写入和读取的顺序必须一致,如果说你的写入顺序是“输入”、“输出”,那么读取顺序一定是“输入”、“输出”,否则就会出问题
如果说你的写入顺序是“输入”、“输出”,而读取顺序写成了是“输出”、“输入”,那么客户端和服务端之间就无法正确通讯,而且这个问题会在多人游戏时出现(当你朋友想加入游戏时,屏幕上会有一堆乱码),单人游戏倒是没这个情况
重写 getSerializer
写完这个序列化器之后,我们就可以重写getSerializer方法了
1 2 3 4
| @Override public RecipeSerializer<?> getSerializer() { return Serializer.INSTANCE; }
|
新建内部类 Type
同样的,在重写getType方法之前,我们需要创建一个Type
这个类要继承RecipeType,泛型为OreRigRecipe
1 2 3 4
| public static class Type implements RecipeType<OreRigRecipe>{ public static final Type INSTANCE = new Type(); public static final String ID = "ore_rig"; }
|
这里就没有什么方法需要重写了,就这两句话
重写 getType
1 2 3 4
| @Override public RecipeType<?> getType() { return Type.INSTANCE; }
|
注册配方
接下来我们就要来注册配方了,首先我们需要新建一个ModRecipes类
1 2 3
| public class ModRecipes {
}
|
然后我们再写一个register方法来注册
1 2 3 4 5 6
| public static void register() { Registry.register(Registries.RECIPE_SERIALIZER, new Identifier(TutorialModRe.MOD_ID, OreRigRecipe.Serializer.ID), OreRigRecipe.Serializer.INSTANCE); Registry.register(Registries.RECIPE_TYPE, new Identifier(TutorialModRe.MOD_ID, OreRigRecipe.Type.ID), OreRigRecipe.Type.INSTANCE); }
|
注意,需要注册两个东西,一个是Serializer,一个是Type
另外还要到主类中调用这个方法
使用配方
接下来我们要改写之前的方块实体了,让方块实体使用我们的配方,而不再使用硬编码
新建 getMatchRecipe
首先我们来创建一个方法,用来获取匹配的配方
1 2 3 4 5 6 7 8 9 10
| private Optional<OreRigRecipe> getMatchRecipe(World world) { SimpleInventory inv = new SimpleInventory(1); BlockState belowState = world.getBlockState(this.pos.down()); ItemStack belowStack = belowState.getBlock().asItem().getDefaultStack(); inv.setStack(0, belowStack);
return world.getRecipeManager() .getFirstMatch(OreRigRecipe.Type.INSTANCE, inv, world) .map(recipe -> (OreRigRecipe) recipe); }
|
这里的主要逻辑是:
- 构造一个临时物品槽;
- 把方块实体
正下方的方块转换成对应的物品堆栈放入该槽(模拟配方的输入物品);
- 询问世界的
配方管理器(因为配方是由世界统一管理的)是否存在匹配的自定义配方类型;
- 如果找到匹配配方,就把它返回;否则返回空,也就是没有匹配的配方。
总的来说还是比较简单的吧
重写 hasCorrectRecipe
接下来我们用上面的方法重写hasCorrectRecipe
1 2 3 4 5
| private boolean hasCorrectRecipe(World world) { return getMatchRecipe(world) .map(recipe -> canOutputAccept(recipe.getOutput(world.getRegistryManager()))) .orElse(false); }
|
之前的canOutputAccept方法只是被我们搬进了配方判断中
如果该配方的产物能被输出槽接受(放得下),那么就返回true,否则返回false
重写 craftItem
1 2 3 4 5 6 7
| private void craftItem(World world) { getMatchRecipe(world).ifPresent(recipe -> { ItemStack result = recipe.getOutput(world.getRegistryManager()); ItemStack out = outputInv.getStack(0); outputInv.setStack(0, new ItemStack(result.getItem(), out.getCount() + result.getCount())); }); }
|
当配方匹配成功(getMatchRecipe返回的不是empty),就去执行相应的逻辑:
- 获取配方的产物;
- 获取当前输出槽中的物品堆栈;
- 根据配方产物设置输出槽的物品堆栈。
这样,我们的方块实体就可以识别配方了
自定义配方数据生成器
由于我们是自定义的配方类型,原版的数据生成方法不适用于我们的配方,而如果你不想自己手搓json的话,那么我们就来写一个自己的生成器吧
创建 OreRigRecipeBuilder
1 2 3
| public class OreRigRecipeBuilder {
}
|
添加字段
接下来我们添加一些字段
1 2 3 4 5 6 7 8 9
| private final ItemConvertible input; private final ItemConvertible output; private final int outputCount;
public OreRigRecipeBuilder(ItemConvertible input, ItemConvertible output, int outputCount) { this.input = input; this.output = output; this.outputCount = outputCount; }
|
这里的字段很简单,input是输入物品,output是输出物品,outputCount是输出物品的数量
创建 create 方法
1 2 3 4 5 6 7
| public static OreRigRecipeBuilder create(ItemConvertible input, ItemConvertible output, int tier) { return new OreRigRecipeBuilder(input, output, 1); }
public static OreRigRecipeBuilder create(ItemConvertible input, ItemConvertible output, int outputCount, int tier) { return new OreRigRecipeBuilder(input, output, outputCount); }
|
这里我们创建了两个方法,一个用于创建没有指定输出数量的配方,一个用于创建指定输出数量的配方,供后面的数据生成类调用
创建 offerTo 方法
上面的方法只是创建一个你要什么的配方,这个方法是实际用来写入数据文件的(可参考之前我们写的那些)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public void offerTo(Consumer<RecipeJsonProvider> exporter, Identifier id) { exporter.accept(new RecipeJsonProvider() {
@Override public void serialize(JsonObject json) { }
@Override public Identifier getRecipeId() { return null; }
@Override public RecipeSerializer<?> getSerializer() { return null; }
@Override public @Nullable JsonObject toAdvancementJson() { return null; }
@Override public @Nullable Identifier getAdvancementId() { return null; } }); }
|
其中我们再写exporter.accept,里面创建一个RecipeJsonProvider,并重写相应的方法
重写 serialize 方法
1 2 3 4 5 6 7 8 9 10 11 12
| @Override public void serialize(JsonObject json) { json.addProperty("type", TutorialModRe.MOD_ID + ":ore_rig"); JsonObject inputJson = new JsonObject(); inputJson.addProperty("item", Registries.ITEM.getId(input.asItem()).toString()); json.add("input", inputJson);
JsonObject outputJson = new JsonObject(); outputJson.addProperty("item", Registries.ITEM.getId(output.asItem()).toString()); outputJson.addProperty("count", outputCount); json.add("output", outputJson); }
|
这个方法就是将数据写入json中
输入和输出物品都是一个JsonObject,总体上差不多,只是输出物品多了一个count字段
重写 getRecipeId 方法
1 2 3 4
| @Override public Identifier getRecipeId() { return id; }
|
重写 getSerializer 方法
1 2 3 4
| @Override public RecipeSerializer<?> getSerializer() { return OreRigRecipe.Serializer.INSTANCE; }
|
这个方法是返回我们配方本身的序列化器
后面两个方法用于生成和这个配方相关的进度,但因为我尚未研究过,所以就先空着吧
数据生成
接下来就要由我们的数据生成类调用这个生成器了
1 2 3 4 5 6 7 8
| OreRigRecipeBuilder.create(Blocks.DIAMOND_ORE, Items.DIAMOND) .offerTo(exporter, Identifier.of(TutorialModRe.MOD_ID, "diamond_ore_rig")); OreRigRecipeBuilder.create(Blocks.DEEPSLATE_DIAMOND_ORE, Items.DIAMOND) .offerTo(exporter, Identifier.of(TutorialModRe.MOD_ID, "deepslate_diamond_ore_rig")); OreRigRecipeBuilder.create(Blocks.IRON_ORE, Items.RAW_IRON) .offerTo(exporter, Identifier.of(TutorialModRe.MOD_ID, "iron_ore_rig")); OreRigRecipeBuilder.create(Blocks.DEEPSLATE_IRON_ORE, Items.RAW_IRON) .offerTo(exporter, Identifier.of(TutorialModRe.MOD_ID, "deepslate_iron_ore_rig"));
|
这里我们创建了4个配方,分别对应钻石和铁的矿石
当然,这里的生成器显然还不够高级,大家也可以根据自己的需求,去创建可以传入tag的生成器
之后我们生成的文件在data/tutorialmod/recipes下,一个参考文件如下:
1 2 3 4 5 6 7 8 9 10
| { "type": "tutorial-mod-re:ore_rig", "input": { "item": "minecraft:diamond_ore" }, "output": { "count": 1, "item": "minecraft:diamond" } }
|
然后我们就可以进入游戏进行测试了