本篇教程的视频

(待发布)

本篇教程的源代码

(待发布)

本篇教程目标

编写一个精炼炉

介绍

前面的教程我们已经编写了一个矿机,那么这期教程我们就来写一个精炼炉

当然,特别的,它是一个单输入单输出的方块实体

同样的,我们也要为其配置GUI自定义配方REI 适配

配方这里会有一些特殊,这里我们会让输入物品也可以接受数量;方块上也会特殊一点,因为这是一个底座3*3大小的方块,实际上我们得写一个多方块

视频教程会分为几个部分来讲,毕竟东西有点多,但这里的图文教程就写在一起了,请大家仔细阅读

这里的案例我们还是使用GeckoLib来添加我们的方块实体,模型是当时终末地二测时的精炼炉模型

当然,如果你不想像我一样搞GeckoLib的方块,也可以跳过相关部分

带有方块朝向的方块基类

首先我们先创建一个带有方块朝向的方块基类,这个类继承自BlockWithEntity

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
public abstract class ModBlockEntityWithFacing extends BlockWithEntity {
public static final DirectionProperty FACING = Properties.HORIZONTAL_FACING;

public ModBlockEntityWithFacing(Settings settings) {
super(settings);
this.setDefaultState(this.stateManager.getDefaultState().with(FACING, Direction.NORTH));
}

@Override
protected void appendProperties(StateManager.Builder<Block, BlockState> builder) {
builder.add(FACING);
}

@Override
public @Nullable BlockState getPlacementState(ItemPlacementContext ctx) {
return this.getDefaultState().with(FACING, ctx.getHorizontalPlayerFacing().getOpposite());
}

@Override
public BlockState rotate(BlockState state, BlockRotation rotation) {
return state.with(FACING, rotation.rotate(state.get(FACING)));
}

@Override
public BlockState mirror(BlockState state, BlockMirror mirror) {
return state.rotate(mirror.getRotation(state.get(FACING)));
}
}

这个类主要是为了让我们后续的方块可以有朝向,方便我们在后续的教程中使用

这里面的东西我们都在前面的方块实体小系列中讲过了,这里就不再赘述

方块类

这里我们先说明一下这里的多方块结构

因为它是的底座是3*3大小的,而在游戏中,如果直接设置那么大的方块并不现实,碰撞箱就会有问题,更不用说方块实体了

所以我们干脆拆开来,分成一个个方块,只是放置和破坏时,一起放下或破坏所有方块

这里我们需要一个主方块和8个侧面方块,方块实体上也是如此

主方块类

这里我们先创建一个RefiningUnitBlock类,它继承自ModBlockEntityWithFacing

1
2
3
4
5
6
7
8
9
10
11
public class RefiningUnitBlock extends ModBlockEntityWithFacing {

public RefiningUnitBlock(Settings settings) {
super(settings);
}

@Override
public @Nullable BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return null;
}
}

侧面方块

同样的,我们创建一个RefiningUnitSideBlock类,它继承自ModBlockEntityWithFacing,这个是侧面方块

因为考虑到后面我们要限定的输入输出方向,所以它也需要方块朝向

1
2
3
4
5
6
7
8
9
10
public class RefiningUnitSideBlock extends ModBlockEntityWithFacing {
public RefiningUnitSideBlock(Settings settings) {
super(settings);
}

@Override
public @Nullable BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return null;
}
}

注册方块

这里我们先把方块注册了,然后来写它的放置和破坏逻辑

1
2
3
4
public static final Block REFINING_UNIT = registerBlocksWithoutItem("refining_unit",
new RefiningUnitBlock(AbstractBlock.Settings.create().strength(0.5f).nonOpaque()));
public static final Block REFINING_UNIT_SIDE = register("refining_unit_side",
new RefiningUnitSideBlock(AbstractBlock.Settings.create().strength(0.5f)));

主方块的话,我们还是使用registerBlocksWithoutItem方法注册,因为我们是使用GeckoLib来添加方块的,物品依旧是单独写的

方块实体类

我们先把方块实体创建了,再去完善方块的逻辑

RefiningUnitBlockEntity

我们先创建RefiningUnitBlockEntity类,它继承自BlockEntity,并实现ExtendedScreenHandlerFactoryGeoBlockEntity,然后实现相关方法

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
public class RefiningUnitBlockEntity extends BlockEntity implements ExtendedScreenHandlerFactory, GeoBlockEntity {
public RefiningUnitBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
}

@Override
public void writeScreenOpeningData(ServerPlayerEntity player, PacketByteBuf buf) {

}

@Override
public Text getDisplayName() {
return null;
}

@Override
public @Nullable ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
return null;
}

@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {

}

@Override
public AnimatableInstanceCache getAnimatableInstanceCache() {
return null;
}
}

同样的,构造函数也改写一下

1
2
3
public RefiningUnitBlockEntity(BlockPos pos, BlockState state) {
super(, pos, state);
}

RefiningUnitSideBlockEntity

创建RefiningUnitBlockEntity类,它继承自BlockEntity,这里的构造函数我们一起改了

1
2
3
4
5
public class RefiningUnitSideBlockEntity extends BlockEntity {
public RefiningUnitSideBlockEntity(BlockPos pos, BlockState state) {
super(, pos, state);
}
}

方块实体注册

1
2
3
4
5
public static final BlockEntityType<RefiningUnitBlockEntity> REFINING_UNIT = create("refining_unit",
BlockEntityType.Builder.create(RefiningUnitBlockEntity::new, ModBlocks.REFINING_UNIT));

public static final BlockEntityType<RefiningUnitSideBlockEntity> REFINING_UNIT_SIDE = create("refining_unit_side",
BlockEntityType.Builder.create(RefiningUnitSideBlockEntity::new, ModBlocks.REFINING_UNIT_SIDE));

然后我们把各自的构造函数完善一下

1
2
3
4
5
6
7
8
9
// RefiningUnitBlockEntity
public RefiningUnitBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.REFINING_UNIT, pos, state);
}

// RefiningUnitSideBlockEntity
public RefiningUnitSideBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.REFINING_UNIT_SIDE, pos, state);
}

GeokoLib相关逻辑

这里我们先把RefiningUnitBlockEntity里面的GeckoLib的相关逻辑写了

1
2
3
4
5
6
7
8
9
10
11
private final AnimatableInstanceCache cache = GeckoLibUtil.createInstanceCache(this);

@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {

}

@Override
public AnimatableInstanceCache getAnimatableInstanceCache() {
return cache;
}

这个前面讲过了,这里就不再赘述

另外加一个tick方法和getItems方法,后续我们会在这里写具体的逻辑

1
2
3
4
5
6
7
public static void tick(World world, BlockPos pos, BlockState state, RefiningUnitBlockEntity be) {

}

public Inventory getItems() {
return null;
}

初步完善 RefiningUnitSideBlockEntity

添加字段

这里我们添加一个字段

1
private BlockPos parentPos;

这是用来存储中央方块的位置的,方便我们后续的逻辑使用

setParentPos

1
2
3
4
public void setParentPos(BlockPos parentPos) {
this.parentPos = parentPos;
markDirty();
}

这个是设置中央方块位置的方法,markDirty是标记方块实体数据发生了变化,触发数据同步

getParentBlock

1
2
3
4
5
6
7
8
9
@Nullable
public RefiningUnitBlockEntity getParentBlock() {
if (parentPos == null || world == null) return null;
BlockEntity entity = world.getBlockEntity(parentPos);
if (entity instanceof RefiningUnitBlockEntity e) {
return e;
}
return null;
}

这个是获取中央方块实体的方法,如果没有获取到,那么就返回null

数据持久化 & 数据同步

接下来是两个常规的NBT数据持久化方法,和两个数据同步方法

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
@Override
protected void writeNbt(NbtCompound nbt) {
super.writeNbt(nbt);
if (parentPos != null) {
nbt.putLong("parent", parentPos.asLong());
}
}

@Override
public void readNbt(NbtCompound nbt) {
super.readNbt(nbt);
if (nbt.contains("parent")) {
parentPos = BlockPos.fromLong(nbt.getLong("parent"));
}
}

@Override
public NbtCompound toInitialChunkDataNbt() {
return this.createNbt();
}

@Override
public @Nullable Packet<ClientPlayPacketListener> toUpdatePacket() {
return BlockEntityUpdateS2CPacket.create(this);
}

RefiningUnitBlock

getAdjacentPositions

回到RefiningUnitBlock类,这里我们先写一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private BlockPos[] getAdjacentPositions(BlockState state, BlockPos pos) {
Direction facing = state.get(FACING);
Direction left = facing.rotateYCounterclockwise();
Direction right = facing.rotateYClockwise();
Direction back = facing.getOpposite();
Direction backLeft = back.rotateYClockwise();
Direction backRight = back.rotateYCounterclockwise();

return new BlockPos[]{
pos.offset(facing), pos.offset(facing).offset(left),
pos.offset(right), pos.offset(left),
pos.offset(facing).offset(right), pos.offset(back),
pos.offset(back).offset(backLeft), pos.offset(back).offset(backRight)
};
}

这个是一个获取当前方块周围8个方块位置的方法,方便我们后续的放置和破坏逻辑使用

onPlaced

接下来我们重写onPlaced方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onPlaced(World world, BlockPos pos, BlockState state, @Nullable LivingEntity placer, ItemStack itemStack) {
if (!world.isClient()) {
BlockPos[] adjacentPositions = getAdjacentPositions(state, pos);

for (BlockPos p : adjacentPositions) {
world.setBlockState(p, ModBlocks.REFINING_UNIT_SIDE.getDefaultState().with(FACING, state.get(FACING)));
BlockEntity be = world.getBlockEntity(p);
if (be instanceof RefiningUnitSideBlockEntity adjunct) {
adjunct.setParentPos(pos);
}
}
}
}

这个是放置逻辑,setParentPos我们后续写

canPlaceAt

然后重写canPlaceAt方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public boolean canPlaceAt(BlockState state, WorldView world, BlockPos pos) {
if (!world.isClient()) {
BlockPos[] sidePositions = getAdjacentPositions(state, pos);

for (BlockPos p : sidePositions) {
if (!world.getBlockState(p).getBlock().getDefaultState().isReplaceable()) {
return false;
}
}
return true;
}
return false;
}

这个方法是判断当前方块是否可以放置,主要是判断周围8个方块是否可以被替换,如果有一个不能被替换,那么就返回false,否则返回true

onStateReplaced

最后我们还需要重写onStateReplaced方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) {
if (state.getBlock() != newState.getBlock()) {
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof RefiningUnitBlockEntity be) {
ItemScatterer.spawn(world, pos, be.getItems());
world.updateComparators(pos, this);
}

BlockPos[] adjacentPositions = getAdjacentPositions(state, pos);

for (BlockPos p : adjacentPositions) {
if (world.getBlockState(p).getBlock() == ModBlocks.REFINING_UNIT_SIDE) {
world.breakBlock(p, false);
}
}

super.onStateReplaced(state, world, pos, newState, moved);
}
}

这个是破坏逻辑,首先判断当前方块是否被替换,如果是,那么就获取当前方块的方块实体,然后获取周围8个方块的位置,如果周围的方块是RefiningUnitSideBlock,那么就破坏它们

当然这里方块实体相关的getItems逻辑也没写,我们之后再来写

createBlockEntity

重写createBlockEntity方法,返回RefiningUnitBlockEntity

1
2
3
4
@Override
public @Nullable BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new RefiningUnitBlockEntity(pos, state);
}

getTicker

重写getTicker方法,返回RefiningUnitBlockEntitytick,当然现在还没写

1
2
3
4
@Override
public @Nullable <T extends BlockEntity> BlockEntityTicker<T> getTicker(World world, BlockState state, BlockEntityType<T> type) {
return checkType(type, ModBlockEntities.REFINING_UNIT, RefiningUnitBlockEntity::tick);
}

onUse

重写onUse方法,这是打开GUI的逻辑

1
2
3
4
5
6
7
8
9
10
11
@Override
public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
if (!world.isClient()) {
NamedScreenHandlerFactory screenHandlerFactory = ((RefiningUnitBlockEntity) world.getBlockEntity(pos));
if (screenHandlerFactory != null) {
player.openHandledScreen(screenHandlerFactory);
return ActionResult.SUCCESS;
}
}
return ActionResult.CONSUME;
}

RefiningUnitSideBlock

getRenderType

1
2
3
4
@Override
public BlockRenderType getRenderType(BlockState state) {
return BlockRenderType.INVISIBLE;
}

这是让侧面方块不可见的逻辑

createBlockEntity

1
2
3
4
@Override
public @Nullable BlockEntity createBlockEntity(BlockPos pos, BlockState state) {
return new RefiningUnitSideBlockEntity(pos, state);
}

这个也是返回方块实体的逻辑

onUse

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
if (!world.isClient()) {
BlockEntity entity = world.getBlockEntity(pos);
if (entity instanceof RefiningUnitSideBlockEntity entity1) {
RefiningUnitBlockEntity parent = entity1.getParentBlock();
if (parent != null) {
player.openHandledScreen(parent);
return ActionResult.SUCCESS;
}
}
}
return ActionResult.CONSUME;
}

这是打开GUI的逻辑,当然我们需要获取中央方块实体,然后打开它的GUI,侧面方块实体的全部逻辑会委托给中央方块实体处理

getParentBlock我们后面写

onStateReplaced

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) {
if (state.getBlock() != newState.getBlock()) {
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof RefiningUnitSideBlockEntity sideBlockEntity) {
RefiningUnitBlockEntity parent = sideBlockEntity.getParentBlock();
if (parent != null) {
BlockPos parentPos = parent.getPos();
world.breakBlock(parentPos, true);
}
}
}
super.onStateReplaced(state, world, pos, newState, moved);
}

这是破坏逻辑,破坏侧面方块时,会破坏中央方块,中央方块的破坏逻辑会处理掉所有的侧面方块

getPickStack

1
2
3
4
@Override
public ItemStack getPickStack(BlockView world, BlockPos pos, BlockState state) {
return new ItemStack(ModItems.REFINING_UNIT_ITEM);
}

这是鼠标中键快捷获取物品的逻辑,侧面方块获取的物品是中央方块的物品

当然目前还没注册物品

物品类

接下来我们创建RefiningUnitItem类,它继承自BlockItem,实现GeoItem接口,然后实现相关方法

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
public class RefiningUnitItem extends BlockItem implements GeoItem {
public RefiningUnitItem(Block block, Settings settings) {
super(block, settings);
}

@Override
public void createRenderer(Consumer<Object> consumer) {

}

@Override
public Supplier<Object> getRenderProvider() {
return null;
}

@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {

}

@Override
public AnimatableInstanceCache getAnimatableInstanceCache() {
return null;
}
}

里面的逻辑我们后面再来修改

不过先把物品注册了

1
2
3
4
5
6
7
8
9
public static final Item REFINING_UNIT_ITEM = registerSameBlockItem("refining_unit",
new RefiningUnitItem(ModBlocks.REFINING_UNIT, new Item.Settings()), ModBlocks.REFINING_UNIT_SIDE);

private static Item registerSameBlockItem(String name, BlockItem blockItem, Block... blocks){
for (Block b : blocks) {
Item.BLOCK_ITEMS.put(b, blockItem);
}
return Registry.register(Registries.ITEM, new Identifier(TutorialModRe.MOD_ID, name), blockItem);
}

这里我们搞一个新的方法registerSameBlockItem,它的作用是注册一个物品,并且把这个物品和多个方块绑定在一起,这样我们就可以让多个方块使用同一个物品

不要忘记将物品添加到物品栏

1
entries.add(ModItems.REFINING_UNIT_ITEM);

Model类

接下来我们写GeokoLib的模型类

RefiningUnitModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RefiningUnitModel extends GeoModel<RefiningUnitBlockEntity> {
@Override
public Identifier getModelResource(RefiningUnitBlockEntity animatable) {
return new Identifier(TutorialModRe.MOD_ID, "geo/refining_unit.geo.json");
}

@Override
public Identifier getTextureResource(RefiningUnitBlockEntity animatable) {
return new Identifier(TutorialModRe.MOD_ID, "textures/block/refining_unit.png");
}

@Override
public Identifier getAnimationResource(RefiningUnitBlockEntity animatable) {
return null;
}
}

RefiningUnitItemModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RefiningUnitItemModel extends GeoModel<RefiningUnitItem> {
@Override
public Identifier getModelResource(RefiningUnitItem animatable) {
return new Identifier(TutorialModRe.MOD_ID, "geo/refining_unit.geo.json");
}

@Override
public Identifier getTextureResource(RefiningUnitItem animatable) {
return new Identifier(TutorialModRe.MOD_ID, "textures/block/refining_unit.png");
}

@Override
public Identifier getAnimationResource(RefiningUnitItem animatable) {
return null;
}
}

Renderer类

接下来我们写GeckoLib相关的渲染器类

RefiningUnitBlockRenderer

1
2
3
4
5
public class RefiningUnitBlockRenderer extends GeoBlockRenderer<RefiningUnitBlockEntity> {
public RefiningUnitBlockRenderer(BlockEntityRendererFactory.Context context) {
super(new RefiningUnitModel());
}
}

记得在客户端类注册这个渲染器

1
BlockEntityRendererFactories.register(ModBlockEntities.REFINING_UNIT, RefiningUnitBlockRenderer::new);

RefiningUnitItemRenderer

1
2
3
4
5
public class RefiningUnitItemRenderer extends GeoItemRenderer<RefiningUnitItem> {
public RefiningUnitItemRenderer() {
super(new RefiningUnitItemModel());
}
}

写好物品的渲染器后,我们就可以去完善物品类了

完善 RefiningUnitItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private final AnimatableInstanceCache cache = new SingletonAnimatableInstanceCache(this);
private final Supplier<Object> renderProvider = GeoItem.makeRenderer(this);

public RefiningUnitItem(Block block, Settings settings) {
super(block, settings);
SingletonGeoAnimatable.registerSyncedAnimatable(this);
}

@Override
public Supplier<Object> getRenderProvider() {
return renderProvider;
}

@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {

}

@Override
public AnimatableInstanceCache getAnimatableInstanceCache() {
return cache;
}

而后是createRenderer方法

1
2
3
4
5
6
7
8
9
10
@Override
public void createRenderer(Consumer<Object> consumer) {
consumer.accept(new RenderProvider() {
private final RefiningUnitItemRenderer renderer = new RefiningUnitItemRenderer();
@Override
public BuiltinModelItemRenderer getCustomRenderer() {
return renderer;
}
});
}

顺便我们写一个place方法,这个方法用来检测当前方块是否可以放置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Override
public ActionResult place(ItemPlacementContext context) {
World world = context.getWorld();
if (!world.isClient()) {
BlockPos pos = context.getBlockPos();
for (BlockPos p: BlockPos.iterate(pos.add(1, 0, 1), pos.add(-1, 0, -1))) {
if (!world.getBlockState(p).isReplaceable()) {
PlayerEntity player = context.getPlayer();
if (player != null) {
player.sendMessage(Text.translatable("message.no_enough_area"), false);
}
return ActionResult.FAIL;
}
}
}
return super.place(context);
}

当空间不够时,我们就给玩家发送一个消息,告诉玩家空间不够

数据文件

语言文件

1
2
translationBuilder.add(ModItems.REFINING_UNIT_ITEM, "Refining Unit");
translationBuilder.add("message.no_enough_area", "No Enough Area");

模型文件

1
blockStateModelGenerator.registerNorthDefaultHorizontalRotation(ModBlocks.REFINING_UNIT);

好,那么到这里为止,我们第一阶段结束,现在可以进入游戏检查一下方块能否正常放置

那么接下来我们先来写配方,完成方块实体的逻辑

自定义配方类

InputEntry

那么接下来我们先搞一个InputEntry类,它是我们的输入物品类,不过比我们矿机用的Ingredient多了一个count字段,用来表示输入物品的数量

1
2
3
public record InputEntry(Ingredient ingredient, int count) {
public static final InputEntry EMPTY = new InputEntry(Ingredient.EMPTY, 0);
}

另外定义一个EMPTY常量,表示空的输入物品,与Ingredient.EMPTY类似

RefiningUnitRecipe

然后我们创建RefiningUnitRecipe类,它实现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
37
public class RefiningUnitRecipe 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;
}
}

添加字段

这里我们添加三个字段,分别是idinputoutput,与之前的配方差不多

1
2
3
4
5
6
7
8
9
private final Identifier id;
private final InputEntry input;
private final ItemStack output;

public RefiningUnitRecipe(Identifier id, InputEntry input, ItemStack output) {
this.id = id;
this.input = input;
this.output = output;
}

重写方法

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
@Override
public boolean matches(SimpleInventory inventory, World world) {
if (world.isClient()) return false;
ItemStack inputs = inventory.getStack(0);
return input.ingredient().test(inputs);
}

@Override
public ItemStack craft(SimpleInventory inventory, DynamicRegistryManager registryManager) {
return output.copy();
}

@Override
public boolean fits(int width, int height) {
return true;
}

@Override
public ItemStack getOutput(DynamicRegistryManager registryManager) {
return output;
}

@Override
public Identifier getId() {
return id;
}

这里的方法与前面的差不多,这里就不赘述了

Type 和 Serializer

接下来是配方的类型和序列化器,直接创建内部类

1
2
3
4
5
6
7
8
9
@Override
public RecipeType<?> getType() {
return Type.INSTANCE;
}

public static class Type implements RecipeType<RefiningUnitRecipe> {
public static final Type INSTANCE = new Type();
public static final String ID = "refining_unit";
}

后面是序列化器

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
private InputEntry getInput() {
return input;
}

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

public static class Serializer implements RecipeSerializer<RefiningUnitRecipe> {

public static final Serializer INSTANCE = new Serializer();
public static final String ID = "refining_unit";

@Override
public RefiningUnitRecipe read(Identifier id, JsonObject json) {
JsonObject ingredients = JsonHelper.getObject(json, "input");
ItemStack output = ShapedRecipe.outputFromJson(JsonHelper.getObject(json, "output"));
InputEntry inputs;

Ingredient ingredient = Ingredient.fromJson(ingredients);
int count = ingredients.has("count") ? JsonHelper.getInt(ingredients, "count") : 1;
inputs = new InputEntry(ingredient, count);

return new RefiningUnitRecipe(id, inputs, output);
}

@Override
public RefiningUnitRecipe read(Identifier id, PacketByteBuf buf) {
Ingredient ingredient = Ingredient.fromPacket(buf);
int count = buf.readInt();
InputEntry inputs = new InputEntry(ingredient, count);
ItemStack output = buf.readItemStack();

return new RefiningUnitRecipe(id, inputs, output);
}

@Override
public void write(PacketByteBuf buf, RefiningUnitRecipe recipe) {
InputEntry entry = recipe.getInput();
entry.ingredient().write(buf);
buf.writeInt(entry.count());
buf.writeItemStack(recipe.output);
}
}

由于输入物品是InputEntry类型,所以我们在读取和写入时都要处理count字段,其他的倒是和前面的矿机配方差不多

注册配方

写完配方后,我们就可以注册配方了

1
2
3
4
Registry.register(Registries.RECIPE_SERIALIZER, new Identifier(TutorialModRe.MOD_ID, RefiningUnitRecipe.Serializer.ID),
RefiningUnitRecipe.Serializer.INSTANCE);
Registry.register(Registries.RECIPE_TYPE, new Identifier(TutorialModRe.MOD_ID, RefiningUnitRecipe.Type.ID),
RefiningUnitRecipe.Type.INSTANCE);

配方构造器

和前面的矿机配方一样,我们也写一个构造器,方便我们在数据生成中使用

ItemCountInput

不过我们先写一个ItemCountInput类,比ItemConvertible多了一个count字段,用来表示输入物品的数量

1
2
3
public record ItemCountInput(ItemConvertible itemConvertible, int count) {

}

RefiningUnitRecipeBuilder

然后我们写一个RefiningUnitRecipeBuilder类,它是一个构造器类

1
2
3
public class RefiningUnitRecipeBuilder {

}

添加字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private ItemCountInput input;
private ItemConvertible output;
private final int outputCount;

private RefiningUnitRecipeBuilder(ItemCountInput input, ItemConvertible output, int outputCount) {
this.input = input;
this.output = output;
this.outputCount = outputCount;
}

public static RefiningUnitRecipeBuilder create(ItemCountInput input, ItemConvertible output) {
return new RefiningUnitRecipeBuilder(input, output, 1);
}

public static RefiningUnitRecipeBuilder create(ItemConvertible input, ItemConvertible output) {
return new RefiningUnitRecipeBuilder(new ItemCountInput(input, 1), output, 1);
}

public static RefiningUnitRecipeBuilder create(ItemCountInput input, ItemConvertible output, int outputCount) {
return new RefiningUnitRecipeBuilder(input, output, outputCount);
}

下面的三个create方法是为了方便我们创建构造器,与前面的矿机配方类似

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
30
31
32
33
34
35
36
37
public void offerTo(Consumer<RecipeJsonProvider> exporter, Identifier id) {
exporter.accept(new RecipeJsonProvider() {
@Override
public void serialize(JsonObject json) {
json.addProperty("type", TutorialModRe.MOD_ID + ":refining_unit");
JsonObject inputJson = new JsonObject();
inputJson.addProperty("item", Registries.ITEM.getId(input.itemConvertible().asItem()).toString());
inputJson.addProperty("count", input.count());
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);
}

@Override
public Identifier getRecipeId() {
return id;
}

@Override
public RecipeSerializer<?> getSerializer() {
return RefiningUnitRecipe.Serializer.INSTANCE;
}

@Override
public @Nullable JsonObject toAdvancementJson() {
return null;
}

@Override
public @Nullable Identifier getAdvancementId() {
return null;
}
});
}

这个是把配方写入JSON文件的方法,和前面的矿机配方差不多,这里就不赘述了

数据生成

接下来我们就来写一些配方

1
2
3
4
RefiningUnitRecipeBuilder.create(new ItemCountInput(ModItems.RAW_ICE_ETHER, 2), ModItems.ICE_ETHER)
.offerTo(exporter,new Identifier(TutorialModRe.MOD_ID, "ice_ether_refining"));
RefiningUnitRecipeBuilder.create(Items.RAW_IRON, Items.IRON_INGOT)
.offerTo(exporter,new Identifier(TutorialModRe.MOD_ID, "iron_ingot_refining"));

这里我们就可以将3个RAW_ICE_ETHER精炼成2个ICE_ETHER,在原版的熔炉只能一个烧一个,最终自定义数量是做不到的

在格雷科技中的蒸汽锻造锤就有3个铁锭锻造成2块铁板这样的配方

RefiningUnitBlockEntity 逻辑完善

那么接下来就是方块实体逻辑的全面铺开

添加字段

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 int progress = 0;
public int maxProgress = 60;

public final PropertyDelegate propertyDelegate;

public RefiningUnitBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.REFINING_UNIT, pos, state);
this.propertyDelegate = new PropertyDelegate() {

@Override
public int get(int index) {
return switch (index) {
case 0 -> RefiningUnitBlockEntity.this.progress;
case 1 -> RefiningUnitBlockEntity.this.maxProgress;
default -> 0;
};
}

@Override
public void set(int index, int value) {

}

@Override
public int size() {
return 2;
}
};
}

这几个字段都是常规操作了,这里就不再赘述

writeScreenOpeningData

1
2
3
4
@Override
public void writeScreenOpeningData(ServerPlayerEntity player, PacketByteBuf buf) {
buf.writeBlockPos(pos);
}

getDisplayName

1
2
3
4
@Override
public Text getDisplayName() {
return Text.translatable("blockEntity.refining_unit");
}

输入输出槽

接下来我们定义输入槽和输出槽,它依旧使用FabricTransfer API

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
protected final SimpleInventory inputInv = new SimpleInventory(1) {
@Override
public void markDirty() {
super.markDirty();
RefiningUnitBlockEntity.this.markDirty();
}

@Override
public boolean isValid(int slot, ItemStack stack) {
if (stack == null || stack.isEmpty()) return false;
ItemStack current = this.getStack(slot);
if (current.isEmpty()) {
return true;
}
return ItemStack.canCombine(current, stack) && current.getCount() < current.getMaxCount();
}
};
protected final SimpleInventory outputInv = new SimpleInventory(1) {
@Override
public void markDirty() {
super.markDirty();
RefiningUnitBlockEntity.this.markDirty();
}

@Override
public boolean isValid(int slot, ItemStack stack) {
return false;
}
};
protected final InventoryStorage inputStorage = InventoryStorage.of(inputInv, null);
protected final InventoryStorage outputStorage = InventoryStorage.of(outputInv, null);

特别的,这里我们重写了isValid方法,输入槽可以输入物品,输出槽不可以输入物品,这是用于自动化的

另外再写两个GUI获取输入输出槽的方法

1
2
3
4
5
6
7
public SimpleInventory getInputInv() {
return inputInv;
}

public SimpleInventory getOutputInv() {
return outputInv;
}

配置 Storge

而后我们添加一个getFacing方法,用于获取方块朝向,这会用于获取输入输出槽实际可输入或输出的方向,同样也用于自动化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private Direction getFacing(BlockState state) {
return state.get(FACING);
}

@Nullable
public Storage<ItemVariant> getStorage(BlockState state, Direction side) {
Direction facing = getFacing(state);

if (side == facing) {
return inputStorage;
}
if (side == facing.getOpposite()) {
return outputStorage;
}
return null;
}

这里我们限定了方块自身的北面(方块自己的局部坐标系)是输入方向,南面是输出方向,其他方向都不可以输入输出物品

然后我们就可以用这个方法在ModStorages类注册我们的这个方块实体的Storage

1
2
3
4
5
6
7
ItemStorage.SIDED.registerForBlockEntity((be, side) -> be.getStorage(be.getCachedState(), side), ModBlockEntities.REFINING_UNIT);
ItemStorage.SIDED.registerForBlockEntity(
(sideBe, side) -> {
RefiningUnitBlockEntity parent = sideBe.getParentBlock();
if (parent == null) return null;
return parent.getStorage(sideBe.getCachedState(), side);
}, ModBlockEntities.REFINING_UNIT_SIDE);

这个前面也讲过了,这是Fabric版的Capability

这里侧面方块的Storage是委托给中央方块的,这样我们就可以在侧面方块上使用自动化了

数据持久化 & 数据同步

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
@Override
protected void writeNbt(NbtCompound nbt) {
super.writeNbt(nbt);
NbtCompound inputTag = new NbtCompound();
Inventories.writeNbt(inputTag, inputInv.stacks);
nbt.put("input", inputTag);

NbtCompound outputTag = new NbtCompound();
Inventories.writeNbt(outputTag, outputInv.stacks);
nbt.put("output", outputTag);
nbt.putInt("progress", this.progress);
}

@Override
public void readNbt(NbtCompound nbt) {
super.readNbt(nbt);
if (nbt.contains("input")) {
Inventories.readNbt(nbt.getCompound("input"), inputInv.stacks);
}
if (nbt.contains("output")) {
Inventories.readNbt(nbt.getCompound("output"), outputInv.stacks);
}
this.progress = nbt.getInt("progress");
}

@Override
public NbtCompound toInitialChunkDataNbt() {
return this.createNbt();
}

@Override
public @Nullable Packet<ClientPlayPacketListener> toUpdatePacket() {
return BlockEntityUpdateS2CPacket.create(this);
}

getItems

1
2
3
4
5
6
7
8
9
10
public DefaultedList<ItemStack> getItems() {
DefaultedList<ItemStack> combined = DefaultedList.ofSize(inputInv.size() + outputInv.size(), ItemStack.EMPTY);
for (int i = 0; i < inputInv.size(); i++) {
combined.set(i, this.inputInv.getStack(i));
}
for (int i = 0; i < outputInv.size(); i++) {
combined.set(i + inputInv.size(), this.outputInv.getStack(i));
}
return combined;
}

这个方法是用来获取所有的输入输出物品的,方便我们在破坏方块时把物品掉落出来

不过这里的是自适应的写法,不论输入输出槽的数量是多少,都可以获取到所有的物品

加工部分方法

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
private Optional<RefiningUnitRecipe> getMatchRecipe(World world) {
SimpleInventory inv = new SimpleInventory(1);
inv.setStack(0, inputInv.getStack(0));
return world.getRecipeManager()
.getFirstMatch(RefiningUnitRecipe.Type.INSTANCE, inv, world);
}

private void craftItem(World world) {
getMatchRecipe(world).ifPresent(r -> {
ItemStack result = r.getOutput(world.getRegistryManager());
ItemStack out = outputInv.getStack(0);
outputInv.setStack(0, new ItemStack(result.getItem(), out.getCount() + result.getCount()));

InputEntry recipeInput = r.getInput();
ItemStack stack = inputInv.getStack(0);
if (recipeInput.ingredient().test(stack) && stack.getCount() >= recipeInput.count()) {
inputInv.removeStack(0, recipeInput.count());
}
});
}

private boolean hasCorrectRecipe(World world) {
Optional<RefiningUnitRecipe> match = getMatchRecipe(world);

if (match.isPresent()) {
RefiningUnitRecipe recipe = match.get();

InputEntry recipeInput = recipe.getInput();
boolean matched = false;
ItemStack stack = inputInv.getStack(0);
if (recipeInput.ingredient().test(stack) && stack.getCount() >= recipeInput.count()) {
matched = true;
}
if (!matched) return false;

ItemStack result = recipe.getOutput(world.getRegistryManager());
return canOutputAccept(result);
}

return false;
}

private void resetProgress() {
this.progress = 0;
}

private void incrementProgress() {
this.progress++;
}

private boolean canOutputAccept(ItemStack result) {
ItemStack out = outputInv.getStack(0);
return (out.isEmpty() || out.getItem() == result.getItem())
&& out.getCount() + result.getCount() <= out.getMaxCount();
}

private boolean isOutputSlotAvailable() {
return outputInv.getStack(0).isEmpty() || outputInv.getStack(0).getCount() < outputInv.getStack(0).getMaxCount();
}

private boolean hasCraftingFinished() {
return progress >= maxProgress;
}

这里的方法与前面的矿机差不多,实际上就是从一个模板里出来的,所以大家大可以写成基类,不过教程我们就不写了

  • getMatchRecipe:获取当前输入物品匹配的配方
  • craftItem:执行加工逻辑,将输入物品加工成输出物品
  • hasCorrectRecipe:检查当前输入物品是否有匹配的配方
  • resetProgress:重置加工进度
  • incrementProgress:增加加工进度
  • canOutputAccept:检查输出槽是否可以接受加工结果
  • isOutputSlotAvailable:检查输出槽是否有空间
  • hasCraftingFinished:检查加工是否完成

tick

然后就是最重要的tick方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public static void tick(World world, BlockPos pos, BlockState state, RefiningUnitBlockEntity be) {
if (world.isClient()) return;

if (be.isOutputSlotAvailable()) {
boolean hasRecipe = be.hasCorrectRecipe(world);
be.isWorking = hasRecipe;
be.markDirty();
world.updateListeners(pos, state, state, 3);

if (hasRecipe) {
be.incrementProgress();
if (be.hasCraftingFinished()) {
be.craftItem(world);
be.resetProgress();
}
} else {
be.resetProgress();
}
} else {
be.resetProgress();
}
be.markDirty();
}

其实写来写去也就是这样的逻辑,并不复杂,未来增加额外的能量系统和流体系统,也只是在上面加模块而已

当然,还是不要一上来就想着一下子实现所有逻辑,因为这不现实

配置 GUI

RefiningUnitScreenHandler

我们首先创建一个RefiningUnitScreenHandler类,它继承自ScreenHandler,并实现相关方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class RefiningUnitScreenHandler extends ScreenHandler {
protected RefiningUnitScreenHandler(@Nullable ScreenHandlerType<?> type, int syncId) {
super(type, syncId);
}

@Override
public ItemStack quickMove(PlayerEntity player, int slot) {
return null;
}

@Override
public boolean canUse(PlayerEntity player) {
return false;
}
}

添加字段

1
2
3
4
5
private final SimpleInventory inputInv;
private final SimpleInventory outputInv;
private final PropertyDelegate propertyDelegate;

public final RefiningUnitBlockEntity entity;

这里我们先添加4个常规字段

重写构造函数

而后我们重写构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public RefiningUnitScreenHandler(int syncId, PlayerInventory playerInventory, PacketByteBuf packetByteBuf) {
this(syncId, playerInventory, Objects.requireNonNull(getClientEntity(playerInventory, packetByteBuf)),
new ArrayPropertyDelegate(2));
}
public RefiningUnitScreenHandler(int syncId, PlayerInventory playerInventory, RefiningUnitBlockEntity blockEntity, PropertyDelegate propertyDelegate) {
super(, syncId);
checkSize(playerInventory, 2);
this.inputInv = blockEntity.getInputInv();
this.outputInv = blockEntity.getOutputInv();
this.propertyDelegate = propertyDelegate;
this.entity = blockEntity;
}

@Environment(EnvType.CLIENT)
private static RefiningUnitBlockEntity getClientEntity(PlayerInventory playerInventory, PacketByteBuf buf) {
BlockPos pos = buf.readBlockPos();
BlockEntity be = playerInventory.player.getWorld().getBlockEntity(pos);
return be instanceof RefiningUnitBlockEntity e ? e : null;
}

添加槽位

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
public RefiningUnitScreenHandler(int syncId, PlayerInventory playerInventory, RefiningUnitBlockEntity blockEntity, PropertyDelegate propertyDelegate) {
...
this.addSlot(new Slot(inputInv, 0, 80, 11));
this.addSlot(new Slot(outputInv, 0, 80, 59) {
@Override
public boolean canInsert(ItemStack stack) {
return false;
}
});

addPlayerInventory(playerInventory);
addPlayerHotbar(playerInventory);

addProperties(propertyDelegate);

}

private void addPlayerInventory(PlayerInventory playerInventory) {
for (int i = 0; i < 3; ++i) {
for (int l = 0; l < 9; ++l) {
this.addSlot(new Slot(playerInventory, l + i * 9 + 9, 8 + l * 18, 84 + i * 18));
}
}
}

private void addPlayerHotbar(PlayerInventory playerInventory) {
for (int i = 0; i < 9; ++i) {
this.addSlot(new Slot(playerInventory, i, 8 + i * 18, 142));
}
}

一个输入槽,一个输出槽,其中输出槽禁止放入物品

还有就是玩家物品栏和快捷栏

quickMove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@Override
public ItemStack quickMove(PlayerEntity player, int invSlot) {
ItemStack newStack = ItemStack.EMPTY;
Slot slot = this.slots.get(invSlot);
if (slot != null && slot.hasStack()) {
ItemStack originalStack = slot.getStack();
newStack = originalStack.copy();
if (invSlot < this.inputInv.size()) {
if (!this.insertItem(originalStack, this.inputInv.size(), this.slots.size(), true)) {
return ItemStack.EMPTY;
}
} else if (!this.insertItem(originalStack, 0, this.inputInv.size(), false)) {
return ItemStack.EMPTY;
}

if (originalStack.isEmpty()) {
slot.setStack(ItemStack.EMPTY);
} else {
slot.markDirty();
}
}

return newStack;
}

重写quickMove方法,实现物品的快速移动逻辑,主要是实现从输入槽到玩家物品栏的移动,以及从玩家物品栏到输入槽的移动

canUse

1
2
3
4
5
6
@Override
public boolean canUse(PlayerEntity player) {
return this.entity != null
&& this.entity.getWorld() != null
&& this.entity.getPos().isWithinDistance(player.getBlockPos(), 8);
}

重写canUse方法,判断玩家是否可以使用这个GUI,主要是判断玩家与方块的距离是否小于等于8

isCrafting

1
2
3
public boolean isCrafting(){
return propertyDelegate.get(0) > 0;
}

这个方法是用来判断当前是否在加工中,主要是判断progress是否大于0

getScaledProgress

1
2
3
4
5
6
7
public int getScaledProgress() {
int progress = this.propertyDelegate.get(0);
int maxProgress = this.propertyDelegate.get(1);
int progressArrowSize = 26;

return maxProgress != 0 && progress != 0 ? progress * progressArrowSize / maxProgress : 0;
}

这个方法是用来获取加工箭头的长度,主要是根据progressmaxProgress计算出当前进度条的长度

注册屏幕

1
2
3
public static final ScreenHandlerType<RefiningUnitScreenHandler> REFINING_UNIT_SCREEN = 
Registry.register(Registries.SCREEN_HANDLER, new Identifier(TutorialModRe.MOD_ID, "refining_unit_screen"),
new ExtendedScreenHandlerType<>(RefiningUnitScreenHandler::new));

然后记得完善RefiningUnitScreenHandler的构造函数

1
super(ModScreens.REFINING_UNIT_SCREEN, syncId);

方块实体 createMenu 方法

方块实体中的createMenu我们也可以完善了

1
2
3
4
@Override
public @Nullable ScreenHandler createMenu(int syncId, PlayerInventory playerInventory, PlayerEntity player) {
return new RefiningUnitScreenHandler(syncId, playerInventory, this, this.propertyDelegate);
}

RefiningUnitScreen

接下来我们创建一个RefiningUnitScreen类,它继承自HandledScreen<RefiningUnitScreenHandler>,并实现相关方法

1
2
3
4
5
6
7
8
9
10
public class RefiningUnitScreen extends HandledScreen<RefiningUnitScreenHandler> {
public RefiningUnitScreen(RefiningUnitScreenHandler handler, PlayerInventory inventory, Text title) {
super(handler, inventory, title);
}

@Override
protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {

}
}

添加字段

1
2
3
4
5
6
7
private final Identifier TEXTURE = new Identifier(TutorialModRe.MOD_ID, "textures/gui/refining_unit.png");
private final RefiningUnitBlockEntity entity;

public RefiningUnitScreen(RefiningUnitScreenHandler handler, PlayerInventory inventory, Text title) {
super(handler, inventory, title);
this.entity = handler.entity;
}

一个是GUI的纹理,一个是方块实体

当然,这里的方块实体其实没用到,大家可以用它添加开关,作为作业给大家了

drawBackground

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected void drawBackground(DrawContext context, float delta, int mouseX, int mouseY) {
RenderSystem.setShader(GameRenderer::getPositionTexProgram);
RenderSystem.setShaderColor(1f, 1f, 1f, 1f);
RenderSystem.setShaderTexture(0, TEXTURE);
int x = (this.width - this.backgroundWidth) / 2;
int y = (this.height - this.backgroundHeight) / 2;

context.drawTexture(TEXTURE, x, y, 0, 0, backgroundWidth, backgroundHeight);

renderProgressArrow(context, x, y);
}

private void renderProgressArrow(DrawContext context, int x, int y) {
if (handler.isCrafting()) {
context.drawTexture(TEXTURE, x + 85, y + 30, 176, 0, 8, handler.getScaledProgress());
}
}

绘制箭头的方法也大差不差,不过这里变成了竖向的

render

1
2
3
4
5
6
@Override
public void render(DrawContext context, int mouseX, int mouseY, float delta) {
renderBackground(context);
super.render(context, mouseX, mouseY, delta);
drawMouseoverTooltip(context,mouseX,mouseY);
}

最后我们还要注册这个Screen

1
HandledScreens.register(ModScreens.REFINING_UNIT_SCREEN, RefiningUnitScreen::new);

跑完数据生成,添加好资源文件后,我们就可以进入游戏测试了

至此,我们第二阶段完成了

最后一个阶段就是REI的适配

REI 适配

RefiningUnitCategory

这里我们新建一个RefiningUnitCategory类,它实现DisplayCategory<RefiningUnitDisplay>接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RefiningUnitCategory implements DisplayCategory<RefiningUnitDisplay> {
@Override
public CategoryIdentifier<? extends RefiningUnitDisplay> getCategoryIdentifier() {
return null;
}

@Override
public Text getTitle() {
return null;
}

@Override
public Renderer getIcon() {
return null;
}
}

getCategoryIdentifier

1
2
3
4
5
6
public static final CategoryIdentifier<RefiningUnitDisplay> REFINING_UNIT =
CategoryIdentifier.of(TutorialModRe.MOD_ID, "refining_unit");
@Override
public CategoryIdentifier<? extends RefiningUnitDisplay> getCategoryIdentifier() {
return REFINING_UNIT;
}

这里先定义一个CategoryIdentifier,然后在getCategoryIdentifier方法中返回它

getTitle

1
2
3
4
@Override
public Text getTitle() {
return Text.translatable("blockEntity.refining_unit");
}

getIcon

1
2
3
4
@Override
public Renderer getIcon() {
return EntryStacks.of(ModBlocks.REFINING_UNIT.asItem().getDefaultStack());
}

setupDisplay

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public List<Widget> setupDisplay(RefiningUnitDisplay display, Rectangle bounds) {
List<Widget> widgets = new ArrayList<>();
widgets.add(Widgets.createRecipeBase(bounds));

Point start = new Point(bounds.getCenterX() - 60, bounds.getCenterY() - 8);

widgets.add(Widgets.createSlot(new Point(start.x, start.y))
.entries(display.getInputEntries().get(0))
.markInput());
widgets.add(Widgets.createArrow(new Point(start.x + 50, start.y + 1)));
widgets.add(Widgets.createSlot(new Point(start.x + 100, start.y))
.entries(display.getOutputEntries().get(0))
.markOutput());
return widgets;
}

这个方法是用来设置REI的显示界面,主要是添加输入槽、输出槽和箭头

RefiningUnitDisplay

接下来我们创建一个RefiningUnitDisplay类,它实现Display接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RefiningUnitDisplay implements Display {
@Override
public List<EntryIngredient> getInputEntries() {
return List.of();
}

@Override
public List<EntryIngredient> getOutputEntries() {
return List.of();
}

@Override
public CategoryIdentifier<?> getCategoryIdentifier() {
return null;
}
}

添加字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private final List<EntryIngredient> inputs;
private final List<EntryIngredient> outputs;

public RefiningUnitDisplay(RefiningUnitRecipe recipe) {
InputEntry entry = recipe.getInput();
List<ItemStack> stacks = List.of(entry.ingredient().getMatchingStacks());
this.inputs = List.of(EntryIngredients.ofItemStacks(
stacks.stream().map(stack -> {
ItemStack copy = stack.copy();
copy.setCount(entry.count());
return copy;
}).toList()));
this.outputs = List.of(EntryIngredients.of(recipe.getOutput(null)));
}

一个是输入物品列表,一个是输出物品列表,构造函数中根据配方初始化这两个列表

重写方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
public List<EntryIngredient> getInputEntries() {
return inputs;
}

@Override
public List<EntryIngredient> getOutputEntries() {
return outputs;
}

@Override
public CategoryIdentifier<?> getCategoryIdentifier() {
return RefiningUnitCategory.REFINING_UNIT;
}

剩下的方法就是返回输入输出物品列表和分类标识符

注册 REI 内容

registerCategories

1
2
3
4
5
6
@Override
public void registerCategories(CategoryRegistry registry) {
registry.add(new RefiningUnitCategory());

registry.addWorkstations(RefiningUnitCategory.REFINING_UNIT, EntryStacks.of(ModBlocks.REFINING_UNIT));
}

这个方法是用来注册REI的分类和标签页,主要是添加一个RefiningUnitCategory分类

registerDisplays

1
2
3
4
@Override
public void registerDisplays(DisplayRegistry registry) {
registry.registerRecipeFiller(RefiningUnitRecipe.class, RefiningUnitRecipe.Type.INSTANCE, RefiningUnitDisplay::new);
}

这个方法是用来注册REI的显示内容

好了,到这里我们就完成了全套流程,现在我们就可以进入游戏测试了,看看我们的精炼炉是否可以正常工作