本篇教程的视频
本篇教程的源代码
介绍
我们前面写了一个可以加工物品的方块实体
,但是这个方块实体加工的物品和加工后的物品是硬编码
的
假如说我们有很多物品可以加工,我们就得写很多代码,这显然不是我们希望看到的
所以,我们就要学习如何自定义配方类型,用json
文件定义配方,就像原版的合成表
一样,这样我们就只需考虑json
文件的格式,而不用考虑代码的问题
注册配方类型
这里我们就不讲解源代码了,原版的配方类型注册在RecipeType
类中,而它们具体的实现逻辑是对应的各个泛型,比如RecipeType<CraftingRecipe>
,具体逻辑在CraftingRecipe
类中
另外,配方类型在1.21
中的改动也较大,本教程不适用于1.21
以下版本
创建PolishingMachineRecipe类
这里我们创建一个PolishingMachineRecipe
类,实现Recipe
接口,泛型为SingleStackRecipeInput
(这个就是和1.20
不一样的地方)
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
| public class PolishingMachineRecipe implements Recipe<SingleStackRecipeInput> {
@Override public boolean matches(SingleStackRecipeInput input, World world) { return false; }
@Override public ItemStack craft(SingleStackRecipeInput input, RegistryWrapper.WrapperLookup lookup) { return null; }
@Override public boolean fits(int width, int height) { return false; }
@Override public ItemStack getResult(RegistryWrapper.WrapperLookup registriesLookup) { return null; }
@Override public RecipeSerializer<?> getSerializer() { return null; }
@Override public RecipeType<?> getType() { return null; } }
|
这里要重写一些方法,待会会进行改写
这里我们的泛型是SingleStackRecipeInput
,也就是只有一个输入物品的配方
你可以根据自己的需求,选择不同的输入类,有必要时可以创建自己的类
创建变量
1 2
| private final ItemStack output; private final List<Ingredient> recipeItems;
|
这里我们创建了两个变量,一个是输出物品,一个是输入物品
随后我们创建构造方法,传入这两个参数(recipeItems
写在前面,不然后面写编解码器的时候又会有问题)
1 2 3 4
| public PolishingMachineRecipe(List<Ingredient> recipeItems, ItemStack output) { this.output = output; this.recipeItems = recipeItems; }
|
重写getIngredients
方法
在重写下面的那些方法之前,我们先重写getIngredients
方法,这个方法返回一个List<Ingredient>
,表示这个配方的输入物品
重写这个方法也是为了方便我们之后写REI
相关内容
1 2 3 4 5 6
| @Override public DefaultedList<Ingredient> getIngredients() { DefaultedList<Ingredient> list = DefaultedList.ofSize(this.recipeItems.size()); list.addAll(recipeItems); return list; }
|
这里我们将recipeItems
中的Ingredient
添加到list
中,然后返回list
重写matches
方法
这个方法用于判断输入物品是否符合配方
1 2 3 4 5 6 7
| @Override public boolean matches(SingleStackRecipeInput input, World world) { if (world.isClient()) { return false; } return recipeItems.get(0).test(input.item()); }
|
这里我们再加一个客户端和服务端的判断,然后判断输入物品是否符合配方
get
里面填的是索引值,不过我们这里只有一个输入物品,所以填0
,也可以将get
改成getfirst
,效果是一样的
而test
方法是判断输入物品是否符合Ingredient
的条件
重写craft
方法
1 2 3 4
| @Override public ItemStack craft(SingleStackRecipeInput input, RegistryWrapper.WrapperLookup lookup) { return this.output.copy(); }
|
这个方法是返回配方的输出物品,这里我们直接返回output
的拷贝
重写fits
方法
1 2 3 4
| @Override public boolean fits(int width, int height) { return true; }
|
这个方法是判断配方是否符合CraftingTable
的大小,这里我们直接返回true
,表示不需要考虑大小
确切来讲,这个应该是我们的GUI
是不是和原版一样的分辨率,如果不是应该还得写额外的代码
重写getResult
方法
1 2 3 4
| @Override public ItemStack getResult(RegistryWrapper.WrapperLookup registriesLookup) { return this.output; }
|
这个方法是返回配方的输出物品,这里我们直接返回output
这个方法和craft
方法的区别是,craft
方法是真正的合成,而getResult
方法是用于配方书
等情景的显示
重写getType
方法
相比较于Serializer
,Type
更好写一点,它是返回这个配方的类型
我们可以看到原版的大多数配方都会有这两个东西,而且这两个是各个配方类的嵌套类,所以我们也要写
这里我们先创建Type
类
1 2 3 4
| public static class Type implements RecipeType<PolishingMachineRecipe> { public static final Type INSTANCE = new Type(); public static final String ID = "polishing_machine"; }
|
这里我们创建了一个Type
类,实现RecipeType
接口,泛型为PolishingMachineRecipe
这里我们创建了一个INSTANCE
对象,用于注册,一个ID
字符串polishing_machine
,这个是json
文件中的type
字段
随后我们重写getType
方法
1 2 3 4
| @Override public RecipeType<?> getType() { return Type.INSTANCE; }
|
就是返回Type
的INSTANCE
对象即可
重写getSerializer
方法
那么Serializer
就是用于序列化和反序列化的,它是编解码器,而且在高版本里面这玩意还挺不好写的(至少我是绕晕了)
Fabric
的文档
和Wiki
有对编解码器的介绍,Wiki
上可以直接搜索Codec
我们先创建一个Serializer
类
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 37 38 39 40 41 42 43 44 45 46 47 48 49
| public static class Serializer implements RecipeSerializer<PolishingMachineRecipe> { public static final Serializer INSTANCE = new Serializer(); public static final String ID = "polishing_machine";
public static final MapCodec<PolishingMachineRecipe> CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( (Ingredient.DISALLOW_EMPTY_CODEC.listOf().fieldOf("ingredients")).flatXmap(ingredients -> { Ingredient[] ingredients1 = (Ingredient[]) ingredients.stream().filter(ingredient -> !ingredient.isEmpty()).toArray(Ingredient[]::new); if (ingredients1.length == 0) { return DataResult.error(() -> "No ingredients"); } if (ingredients1.length > 9) { return DataResult.error(() -> "Too many ingredients"); } return DataResult.success(DefaultedList.copyOf(Ingredient.EMPTY, ingredients1)); }, DataResult::success).forGetter(recipe -> recipe.getIngredients()), (ItemStack.VALIDATED_CODEC.fieldOf("output")).forGetter(recipe -> recipe.output)).apply(instance, PolishingMachineRecipe::new));
public static final PacketCodec<RegistryByteBuf, PolishingMachineRecipe> PACKET_CODEC = PacketCodec.ofStatic( Serializer::write, Serializer::read);
private static PolishingMachineRecipe read(RegistryByteBuf registryByteBuf) { DefaultedList<Ingredient> inputs = DefaultedList.ofSize(registryByteBuf.readInt(), Ingredient.EMPTY);
for (int i = 0; i < inputs.size(); i++) { inputs.set(i, Ingredient.PACKET_CODEC.decode(registryByteBuf)); } ItemStack output = ItemStack.PACKET_CODEC.decode(registryByteBuf); return new PolishingMachineRecipe(inputs, output); }
private static void write(RegistryByteBuf registryByteBuf, PolishingMachineRecipe polishingMachineRecipe) { registryByteBuf.writeInt(polishingMachineRecipe.getIngredients().size());
for (Ingredient ingredient : polishingMachineRecipe.getIngredients()) { Ingredient.PACKET_CODEC.encode(registryByteBuf, ingredient); } ItemStack.PACKET_CODEC.encode(registryByteBuf, polishingMachineRecipe.getResult(null)); }
@Override public MapCodec<PolishingMachineRecipe> codec() { return CODEC; }
@Override public PacketCodec<RegistryByteBuf, PolishingMachineRecipe> packetCodec() { return PACKET_CODEC; } }
|
这里我们创建了一个Serializer
类,实现RecipeSerializer
接口,泛型为PolishingMachineRecipe
对比1.2O
的例子,可以发现这玩意变得相当复杂了
这里面有一个CODEC
对象,就是编解码器,这个对象是用于序列化和反序列化的
里面的fieldOf("ingredients")
是我们读取输入物品的键,fieldOf("output")
是我们读取输出物品的键
我们可以先看一下待会要写的json
文件,这里的ingredients
和output
就是对应的键
1 2 3 4 5 6 7 8 9 10 11
| { "type": "tutorialmod:polishing_machine", "ingredients": [ { "item": "minecraft:coal" } ], "output": { "id": "minecraft:diamond" } }
|
具体的参数我们待会再解释
CODEC
中间的那些条件判断是判断配方文件输入物品数量是否大于0
小于等于9
,如果不符合条件就返回错误,可写可不写吧,根据你的需要来
还有一个PACKET_CODEC
对象,这个对象是用于网络传输
的编解码器,为了它还要创建两个方法read
和write
,这两个方法是用于读取和写入数据的
read
里面用的是decode
方法,write
里面用的是encode
方法,分别对应解码和编码
其实你要是它俩的本质就是读取json
文件,从中获得对应的输入物品和输出物品,只是我们得按照游戏的底层来写
这里我们重写getSerializer
方法
1 2 3 4
| @Override public RecipeSerializer<?> getSerializer() { return Serializer.INSTANCE; }
|
就是返回Serializer
的INSTANCE
对象即可
那么到这里我们的PolishingMachineRecipe
类就写完了
创建ModRecipeTypes类
接下来我们创建一个ModRecipeTypes
类,用于注册我们的配方类型
1 2 3 4 5
| public class ModRecipeTypes { public static void registerRecipeTypes() { } }
|
我们这里直接创建一个registerRecipeTypes
方法,用于初始化的方法,在这个方法中我们注册我们的配方类型
1 2 3 4 5
| Registry.register(Registries.RECIPE_SERIALIZER, Identifier.of(TutorialMod.MOD_ID, PolishingMachineRecipe.Serializer.ID), PolishingMachineRecipe.Serializer.INSTANCE);
Registry.register(Registries.RECIPE_TYPE, Identifier.of(TutorialMod.MOD_ID, PolishingMachineRecipe.Type.ID), PolishingMachineRecipe.Type.INSTANCE);
|
这里要注册的东西是两个,一个是Serializer
,一个是Type
,原版的配方类型注册在RecipeType
类中,而配方的编解码器注册是在RecipeSerializer
类中,这里我们就写一起好了
当然,这里我们并没有提炼一个通用注册方法出来,你可以根据自己的需求来写
随后,我们到主类中调用这个方法
1
| ModRecipeTypes.registerRecipeTypes();
|
方块实体改写
我们的配方类型写好了,接下来我们就要改写我们的方块实体里面的判断方法了
重写craftItem
方法
1 2 3 4 5 6 7
| private void craftItem() { Optional<RecipeEntry<PolishingMachineRecipe>> recipe = getCurrentRecipe(); this.setStack(OUTPUT_SLOT, new ItemStack(recipe.get().value().getResult(null).getItem(), getStack(OUTPUT_SLOT).getCount() + recipe.get().value().getResult(null).getCount()));
this.removeStack(INPUT_SLOT, 1); }
|
这里我们调用了getCurrentRecipe
方法,这个方法是用于获取当前的配方的,当然这是一个自定义的方法,我们得创建一下
这里我们获取了配方的输出物品,然后将其添加到输出槽中,最后将输入槽中的物品减少1
只是在这其中的物品是从配方中获取的,getResult
是填一个recipeManger
,这里我们直接填null
即可,后面的同理
创建getCurrentRecipe
方法
1 2 3 4 5 6 7 8
| private Optional<RecipeEntry<PolishingMachineRecipe>> getCurrentRecipe() { SimpleInventory inventory = new SimpleInventory(this.size()); for (int i = 0; i < this.size(); i++) { inventory.setStack(i, this.getStack(i)); } return getWorld().getRecipeManager().getFirstMatch(PolishingMachineRecipe.Type.INSTANCE, new SingleStackRecipeInput(inventory.getStack(INPUT_SLOT)), getWorld()); }
|
这里我们创建了getCurrentRecipe
方法,这个方法是用于获取当前的配方的,默认情况下是返回Optional.empty()
,也就是没有配方;但是当有配方对应的输入物品时,就返回这个配方
重写hasRecipe
方法
1 2 3 4 5
| private boolean hasRecipe() { Optional<RecipeEntry<PolishingMachineRecipe>> recipe = getCurrentRecipe(); return recipe.isPresent() && canInsertAmountIntoOutputSlot(recipe.get().value().getResult(null)) && canInsertIntoOutputSlot(recipe.get().value().getResult(null).getItem()); }
|
这里我们也调用了getCurrentRecipe
方法
其他的判断是和之前差不多的
配方文件
接下来我们就要写json
文件了,路径是resources/data/tutorialmod/recipe/test.json
(文件名无所谓,你看得懂就行)
1 2 3 4 5 6 7 8 9 10 11
| { "type": "tutorialmod:polishing_machine", "ingredients": [ { "item": "minecraft:coal" } ], "output": { "id": "minecraft:diamond" } }
|
这个就是上面的那个,键对应我们在编解码器里面写的,ingredients
是输入物品,output
是输出物品
因为我们这里用的是原版的编解码器(啊对,我们的编解码器里面还有编解码器,只不过是原版的,它们负责键里面的东西的读取),所以我们按照原版的写法来写里面的键值
ingredients
里面的键是item
,output
里面的键是id
,同时output
里面可以再加一个count
键,表示输出物品的数量
那么ingredients
能不能写数量,不能,至少原版的编解码器里面没有这个,你可以自己写一个编解码器,但是未来会有教程吗?猜猜看
1 2 3 4 5 6 7 8 9 10 11 12
| { "type": "tutorialmod:polishing_machine", "ingredients": [ { "item": "tutorialmod:raw_ice_ether" } ], "output": { "count": 3, "id": "tutorialmod:ice_ether" } }
|
举一反三再写一个,这个就是将raw_ice_ether
加工成ice_ether
,数量是3
在此之后,我们就可以运行游戏了进行测试了