本篇教程的视频
本篇教程的源代码
介绍
我们前面写了一个可以加工物品的方块实体,但是这个方块实体加工的物品和加工后的物品是硬编码的
假如说我们有很多物品可以加工,我们就得写很多代码,这显然不是我们希望看到的
所以,我们就要学习如何自定义配方类型,用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
在此之后,我们就可以运行游戏了进行测试了