本篇教程的视频

本篇教程的源代码

Github地址:TutorialMod-RecipeType-1.21

介绍

我们前面写了一个可以加工物品的方块实体,但是这个方块实体加工的物品和加工后的物品是硬编码

假如说我们有很多物品可以加工,我们就得写很多代码,这显然不是我们希望看到的

所以,我们就要学习如何自定义配方类型,用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方法

相比较于SerializerType更好写一点,它是返回这个配方的类型

我们可以看到原版的大多数配方都会有这两个东西,而且这两个是各个配方类的嵌套类,所以我们也要写

这里我们先创建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;
}

就是返回TypeINSTANCE对象即可

重写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文件,这里的ingredientsoutput就是对应的键

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对象,这个对象是用于网络传输的编解码器,为了它还要创建两个方法readwrite,这两个方法是用于读取和写入数据的

read里面用的是decode方法,write里面用的是encode方法,分别对应解码和编码

其实你要是它俩的本质就是读取json文件,从中获得对应的输入物品和输出物品,只是我们得按照游戏的底层来写

这里我们重写getSerializer方法

1
2
3
4
@Override
public RecipeSerializer<?> getSerializer() {
return Serializer.INSTANCE;
}

就是返回SerializerINSTANCE对象即可

那么到这里我们的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里面的键是itemoutput里面的键是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

在此之后,我们就可以运行游戏了进行测试了