本篇教程的视频

(待发布)

本篇教程的源代码

(待发布)

本篇教程目标

编写一个灌装机

介绍

这期教程我们来写一个罐装机,特别的,它是一个多输入单输出的方块实体

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

选取这个方块也是为了之后可以直接拓展到流体工业上,毕竟灌装机可以罐装各种容器对吧

这里的案例我们还是使用GeckoLib来添加我们的方块实体

当然,因为这部分教程和前面一期的精炼炉有很多相似的地方,这里我们就要加速了,有些部分就不详细讲解了

这个教程是为了巩固前一篇教程的,顺便多加一个输入,整体思路是一样的

方块类

这个灌装机也是一个多方块结构,不过底座是4*6大小的

这里的解决方案也是和前面的精炼炉一样的,不过我们需要一个主方块和23个侧面方块,方块实体上也是如此

当然,这里我只是简单粗暴将其他的23个方块都设置为侧面方块,理论上只要外面一圈是就可以了,大家可以自己优化,不过对性能我估计没有太大影响,因为侧面方块实体的逻辑也是委托给中央方块实体的

主方块类

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

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

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

侧面方块

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

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

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

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

注册方块

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

1
2
3
4
public static final Block FILLING_UNIT = registerBlocksWithoutItem("filling_unit",
new FillingUnitBlock(AbstractBlock.Settings.create().strength(0.5f).nonOpaque()));
public static final Block FILLING_UNIT_SIDE = register("filling_unit_side",
new FillingUnitSideBlock(AbstractBlock.Settings.create().strength(0.5f)));

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

方块实体类

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

FillingUnitBlockEntity

我们先创建FillingUnitBlockEntity类,它继承自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 FillingUnitBlockEntity extends BlockEntity implements ExtendedScreenHandlerFactory, GeoBlockEntity {
public FillingUnitBlockEntity(BlockEntityType<?> type, BlockPos pos, BlockState state) {
super(type, pos, state);
}

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

}

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

@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;
}
}

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

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

FillingUnitSideBlockEntity

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

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

方块实体注册

1
2
3
4
public static final BlockEntityType<FillingUnitBlockEntity> FILLING_UNIT = create("filling_unit",
BlockEntityType.Builder.create(FillingUnitBlockEntity::new, ModBlocks.FILLING_UNIT));
public static final BlockEntityType<FillingUnitSideBlockEntity> FILLING_UNIT_SIDE = create("filling_unit_side",
BlockEntityType.Builder.create(FillingUnitSideBlockEntity::new, ModBlocks.FILLING_UNIT_SIDE));

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

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

// FillingUnitSideBlockEntity
public FillingUnitSideBlockEntity(BlockPos pos, BlockState state) {
super(ModBlockEntities.FILLING_UNIT_SIDE, pos, state);
}

GeokoLib相关逻辑

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

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

@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
controllers.add(new AnimationController<>(this, "controller", 0,
state -> state.setAndContinue(RawAnimation.begin().thenLoop("working"))));
}

@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, FillingUnitBlockEntity be) {

}

public Inventory getItems() {
return null;
}

初步完善 FillingUnitSideBlockEntity

添加字段

这里我们添加一个字段

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 FillingUnitBlockEntity getParentBlock() {
if (parentPos == null || world == null) return null;
BlockEntity entity = world.getBlockEntity(parentPos);
if (entity instanceof FillingUnitBlockEntity 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);
}

FillingUnitBlock

getAdjacentPositions

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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(facing).offset(right),
pos.offset(facing).offset(left, 2), pos.offset(facing).offset(right, 2), pos.offset(facing).offset(right, 3),
pos.offset(right), pos.offset(left),
pos.offset(right, 2), pos.offset(right, 3), pos.offset(left, 2),
pos.offset(back), pos.offset(back, 2),
pos.offset(back).offset(backLeft), pos.offset(back).offset(backRight),
pos.offset(back).offset(backLeft, 2), pos.offset(back).offset(backRight, 2), pos.offset(back).offset(backRight, 3),
pos.offset(back, 2).offset(backLeft), pos.offset(back, 2).offset(backRight),
pos.offset(back, 2).offset(backLeft, 2), pos.offset(back, 2).offset(backRight, 2), pos.offset(back, 2).offset(backRight, 3)
};
}

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

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[] sidePositions = getAdjacentPositions(state, pos);

for (BlockPos p : sidePositions) {
world.setBlockState(p, ModBlocks.FILLING_UNIT_SIDE.getDefaultState().with(FACING, state.get(FACING)));
BlockEntity blockEntity = world.getBlockEntity(p);
if (blockEntity instanceof FillingUnitSideBlockEntity sideBlockEntity) {
sideBlockEntity.setParentPos(pos);
}
}
}
}

这个是放置逻辑

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
@Override
public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) {
if (!world.isClient()) {
BlockEntity blockEntity = world.getBlockEntity(pos);
if (blockEntity instanceof FillingUnitBlockEntity 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.FILLING_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 FillingUnitBlockEntity(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.FILLING_UNIT, FillingUnitBlockEntity::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 = (FillingUnitBlockEntity) world.getBlockEntity(pos);
if (screenHandlerFactory != null) {
player.openHandledScreen(screenHandlerFactory);
return ActionResult.SUCCESS;
}
}
return ActionResult.CONSUME;
}

FillingUnitSideBlock

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 FillingUnitSideBlockEntity(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 FillingUnitSideBlockEntity entity1) {
FillingUnitBlockEntity 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 FillingUnitSideBlockEntity sideBlockEntity) {
FillingUnitBlockEntity 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.FILLING_UNIT_ITEM);
}

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

当然目前还没注册物品

物品类

接下来我们创建FillingUnitItem类,它继承自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 FillingUnitItem extends BlockItem implements GeoItem {
public FillingUnitItem(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
public static final Item FILLING_UNIT_ITEM = registerSameBlockItem("filling_unit",
new FillingUnitItem(ModBlocks.FILLING_UNIT, new Item.Settings().rarity(Rarity.RARE)), ModBlocks.FILLING_UNIT_SIDE);

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

1
entries.add(ModItems.FILLING_UNIT_ITEM);

Model类

接下来我们写GeokoLib的模型类

FillingUnitModel

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

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

@Override
public Identifier getAnimationResource(FillingUnitBlockEntity animatable) {
return new Identifier(TutorialModRe.MOD_ID, "animations/filling_unit.animation.json");
}
}

FillingUnitItemModel

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

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

@Override
public Identifier getAnimationResource(FillingUnitItem animatable) {
return new Identifier(TutorialModRe.MOD_ID, "animations/filling_unit.animation.json");
}
}

Renderer类

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

FillingUnitRenderer

1
2
3
4
5
public class FillingUnitRenderer extends GeoBlockRenderer<FillingUnitBlockEntity> {
public FillingUnitRenderer(BlockEntityRendererFactory.Context context) {
super(new FillingUnitModel());
}
}

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

1
BlockEntityRendererFactories.register(ModBlockEntities.FILLING_UNIT, FillingUnitRenderer::new);

FillingUnitItemRenderer

1
2
3
4
5
public class FillingUnitItemRenderer extends GeoItemRenderer<FillingUnitItem> {
public FillingUnitItemRenderer() {
super(new FillingUnitItemModel());
}
}

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

完善 FillingUnitItem

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

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

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

@Override
public void registerControllers(AnimatableManager.ControllerRegistrar controllers) {
controllers.add(new AnimationController<>(this, "controller", 0,
state -> state.setAndContinue(RawAnimation.begin().thenLoop("idle"))));
}

@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 FillingUnitItemRenderer renderer = new FillingUnitItemRenderer();
@Override
public BuiltinModelItemRenderer getCustomRenderer() {
return renderer;
}
});
}

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

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
@Override
public ActionResult place(ItemPlacementContext context) {
World world = context.getWorld();
if (!world.isClient()) {

BlockPos[] pos1 = getBlockPos(context);

for (BlockPos p : pos1) {
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);
}

private static BlockPos @NotNull [] getBlockPos(ItemPlacementContext context) {
Direction facing = context.getHorizontalPlayerFacing().getOpposite();
Direction left = facing.rotateYCounterclockwise();
Direction right = facing.rotateYClockwise();
Direction back = facing.getOpposite();
Direction backLeft = back.rotateYClockwise();
Direction backRight = back.rotateYCounterclockwise();

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

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

数据文件

语言文件

1
translationBuilder.add(ModItems.FILLING_UNIT_ITEM, "Filling Unit");

模型文件

1
blockStateModelGenerator.registerNorthDefaultHorizontalRotation(ModBlocks.FILLING_UNIT);

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

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

自定义配方类

FillingUnitRecipe

我们创建FillingUnitRecipe类,它实现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 FillingUnitRecipe 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 DefaultedList<InputEntry> input;
private final ItemStack output;

public FillingUnitRecipe(Identifier id, DefaultedList<InputEntry> input, ItemStack output) {
this.id = id;
this.input = input;
this.output = output;
}

特别的,因为这是多输入配方,所以我们这里的输入是一个DefaultedList<InputEntry>类型的列表,里面存储了多个输入物品

重写方法

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
@Override
public boolean matches(SimpleInventory inventory, World world) {
if (world.isClient()) return false;

List<ItemStack> inputs = new ArrayList<>();
for (int i = 0; i < inventory.size(); i++) {
inputs.add(inventory.getStack(i));
}

for (InputEntry inputEntry : input) {
boolean matched = false;
for (ItemStack stack : inputs) {
if (inputEntry.ingredient().test(stack)) {
matched = true;
break;
}
}
if (!matched) return false;
}
return true;
}

@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;
}

这里的matches因为是多输入,所以我们要遍历所有的输入物品,判断是否都能匹配上,如果有一个没有匹配上,那么就返回false,否则返回true

当然,输入的顺序不重要,只要都能匹配上就行

Type 和 Serializer

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

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

public static class Type implements RecipeType<FillingUnitRecipe> {
public static final Type INSTANCE = new Type();
public static final String ID = "filling_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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
public DefaultedList<InputEntry> getInput() {
return input;
}

@Override
public DefaultedList<Ingredient> getIngredients() {
DefaultedList<Ingredient> ingredients = DefaultedList.of();
for (InputEntry entry : input) {
ingredients.add(entry.ingredient());
}
return ingredients;
}

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

public static class Serializer implements RecipeSerializer<FillingUnitRecipe> {

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

@Override
public FillingUnitRecipe read(Identifier id, JsonObject json) {
ItemStack output = ShapedRecipe.outputFromJson(JsonHelper.getObject(json, "output"));
JsonArray ingredients = JsonHelper.getArray(json, "input");
DefaultedList<InputEntry> inputs = DefaultedList.ofSize(ingredients.size(), InputEntry.EMPTY);
for (int i = 0; i < inputs.size(); i++) {
JsonObject obj = ingredients.get(i).getAsJsonObject();
Ingredient ingredient = Ingredient.fromJson(obj);
int count = obj.has("count") ? obj.get("count").getAsInt() : 1;
inputs.set(i, new InputEntry(ingredient, count));
}

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

@Override
public FillingUnitRecipe read(Identifier id, PacketByteBuf buf) {
DefaultedList<InputEntry> inputs = DefaultedList.ofSize(buf.readInt(), InputEntry.EMPTY);
for (int i = 0; i < inputs.size(); i++) {
Ingredient ingredient = Ingredient.fromPacket(buf);
int count = buf.readInt();
inputs.set(i, new InputEntry(ingredient, count));
}
ItemStack output = buf.readItemStack();
return new FillingUnitRecipe(id, inputs, output);
}

@Override
public void write(PacketByteBuf buf, FillingUnitRecipe recipe) {
buf.writeInt(recipe.getIngredients().size());
for (InputEntry entry: recipe.input) {
entry.ingredient().write(buf);
buf.writeInt(entry.count());
}
buf.writeItemStack(recipe.output);
}
}

因为是多输入物品,所以我们额外需要一个getIngredients方法来获取所有的输入物品

注册配方

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

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

配方构造器

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

FillingUnitRecipeBuilder

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

1
2
3
public class FillingUnitRecipeBuilder {

}

添加字段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private final List<ItemCountInput> inputs;
private final ItemConvertible output;
private final int outputCount;

private FillingUnitRecipeBuilder(List<ItemCountInput> inputs, ItemConvertible output, int outputCount) {
this.inputs = inputs;
this.output = output;
this.outputCount = outputCount;
}

public static FillingUnitRecipeBuilder create(List<ItemCountInput> inputs, ItemConvertible output) {
return new FillingUnitRecipeBuilder(inputs, output, 1);
}

public static FillingUnitRecipeBuilder create(List<ItemCountInput> inputs, ItemConvertible output, int outputCount) {
return new FillingUnitRecipeBuilder(inputs, 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
30
31
32
33
34
35
36
37
38
39
40
41
public void offerTo(Consumer<RecipeJsonProvider> exporter, Identifier id) {
exporter.accept(new RecipeJsonProvider() {
@Override
public void serialize(JsonObject json) {
json.addProperty("type", TutorialModRe.MOD_ID + ":filling_unit");
JsonArray inputsArray = new JsonArray();
for (ItemCountInput input : inputs) {
JsonObject inputJson = new JsonObject();
inputJson.addProperty("item", Registries.ITEM.getId(input.itemConvertible().asItem()).toString());
inputJson.addProperty("count", input.count());
inputsArray.add(inputJson);
}
json.add("input", inputsArray);

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 FillingUnitRecipe.Serializer.INSTANCE;
}

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

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

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

数据生成

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

1
2
3
4
5
List<ItemCountInput> HONEY_BOTTLE = List.of(
new ItemCountInput(Items.HONEY_BLOCK, 1),
new ItemCountInput(Items.GLASS_BOTTLE, 4));
FillingUnitRecipeBuilder.create(HONEY_BOTTLE, Items.HONEY_BOTTLE, 4)
.offerTo(exporter, new Identifier(TutorialModRe.MOD_ID, "honey_bottle_filling"));

这里我们就可以用4个空瓶和1个蜂蜜块灌装4个蜂蜜瓶

FillingUnitBlockEntity 逻辑完善

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

添加字段

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 = 200;

public final PropertyDelegate propertyDelegate;

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

@Override
public int get(int index) {
return switch (index) {
case 0 -> FillingUnitBlockEntity.this.progress;
case 1 -> FillingUnitBlockEntity.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.filling_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
32
33
34
35
36
37
38
39
40
protected final SimpleInventory inputInv = new SimpleInventory(2) {
@Override
public void markDirty() {
super.markDirty();
FillingUnitBlockEntity.this.markDirty();
}

@Override
public boolean isValid(int slot, ItemStack stack) {
if (stack == null || stack.isEmpty()) return false;

for (int i = 0; i < this.size(); i++) {
if (i == slot) continue;
ItemStack s = this.getStack(i);
if (!s.isEmpty() && ItemStack.canCombine(s, stack)) {
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();
FillingUnitBlockEntity.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.FILLING_UNIT);
ItemStorage.SIDED.registerForBlockEntity(
(sideBe, side) -> {
FillingUnitBlockEntity parent = sideBe.getParentBlock();
if (parent == null) return null;
return parent.getStorage(sideBe.getCachedState(), side);
}, ModBlockEntities.FILLING_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
64
65
66
67
68
69
70
71
72
73
74
private Optional<FillingUnitRecipe> getMatchRecipe(World world) {
SimpleInventory inv = new SimpleInventory(inputInv.size());
for (int i = 0; i < inputInv.size(); i++) {
inv.setStack(i, inputInv.getStack(i));
}
return world.getRecipeManager()
.getFirstMatch(FillingUnitRecipe.Type.INSTANCE, inv, world);
}

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

DefaultedList<InputEntry> recipeInputs = r.getInput();
boolean[] used = new boolean[inputInv.size()];
for (InputEntry entry: recipeInputs) {
for (int i = 0; i < used.length; i++) {
ItemStack stack = inputInv.getStack(i);
if (!used[i] && entry.ingredient().test(stack)) {
inputInv.removeStack(i, entry.count());
used[i] = true;
break;
}
}
}
});
}

private boolean hasCorrectRecipe(World world) {
Optional<FillingUnitRecipe> match = getMatchRecipe(world);
if (match.isPresent()) {
DefaultedList<InputEntry> recipeInputs = match.get().getInput();
boolean[] used = new boolean[recipeInputs.size()];
for (InputEntry entry: recipeInputs) {
boolean matched = false;
for (int i = 0; i < used.length; i++) {
ItemStack stack = inputInv.getStack(i);
if (!used[i] && entry.ingredient().test(stack) && stack.getCount() >= entry.count()) {
matched = true;
used[i] = true;
break;
}
}
if (!matched) return false;
}
ItemStack result = match.get().getOutput(world.getRegistryManager());
return canOutputAccept(result);
}
return false;
}

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

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();
}

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

  • 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
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.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

FillingUnitScreenHandler

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class FillingUnitScreenHandler extends ScreenHandler {
protected FillingUnitScreenHandler(@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 FillingUnitBlockEntity entity;

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

重写构造函数

而后我们重写构造函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public FillingUnitScreenHandler(int syncId, PlayerInventory playerInventory, PacketByteBuf packetByteBuf) {
this(syncId, playerInventory, Objects.requireNonNull(getClientEntity(playerInventory, packetByteBuf)),
new ArrayPropertyDelegate(2));
}
public FillingUnitScreenHandler(int syncId, PlayerInventory playerInventory, FillingUnitBlockEntity 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 FillingUnitBlockEntity getClientEntity(PlayerInventory playerInventory, PacketByteBuf buf) {
BlockPos pos = buf.readBlockPos();
BlockEntity be = playerInventory.player.getWorld().getBlockEntity(pos);
return be instanceof FillingUnitBlockEntity 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
31
public FillingUnitScreenHandler(int syncId, PlayerInventory playerInventory, FillingUnitBlockEntity blockEntity, PropertyDelegate propertyDelegate) {
...
this.addSlot(new Slot(inputInv, 0, 47, 22));
this.addSlot(new Slot(inputInv, 1, 47, 49));
this.addSlot(new Slot(outputInv, 0, 113, 35) {
@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<FillingUnitScreenHandler> FILLING_UNIT_SCREEN =
Registry.register(Registries.SCREEN_HANDLER, new Identifier(TutorialModRe.MOD_ID, "filling_unit_screen"),
new ExtendedScreenHandlerType<>(FillingUnitScreenHandler::new));

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

1
super(ModScreens.FILLING_UNIT_SCREEN, syncId);

方块实体 createMenu 方法

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

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

FillingUnitScreen

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

1
2
3
4
5
6
7
8
9
10
public class FillingUnitScreen extends HandledScreen<FillingUnitScreenHandler> {
public FillingUnitScreen(FillingUnitScreenHandler 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 FillingUnitBlockEntity entity;

public FillingUnitScreen(FillingUnitScreenHandler 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 + 75, y + 40, 176, 0, handler.getScaledProgress(), 8);
}
}

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

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.FILLING_UNIT_SCREEN, FillingUnitScreen::new);

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

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

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

REI 适配

FillingUnitCategory

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FillingUnitCategory implements DisplayCategory<FillingUnitDisplay> {
@Override
public CategoryIdentifier<? extends FillingUnitDisplay> 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<FillingUnitDisplay> FILLING_UNIT =
CategoryIdentifier.of(TutorialModRe.MOD_ID, "filling_unit");
@Override
public CategoryIdentifier<? extends FillingUnitDisplay> getCategoryIdentifier() {
return FILLING_UNIT;
}

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

getTitle

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

getIcon

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

setupDisplay

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Override
public List<Widget> setupDisplay(FillingUnitDisplay display, Rectangle bounds) {
List<Widget> widgets = new ArrayList<>();

Point start = new Point(bounds.getCenterX() - 60, bounds.getCenterY() - 8);
widgets.add(Widgets.createRecipeBase(bounds));

List<EntryIngredient> inputs = display.getInputEntries();
for (int i = 0; i < inputs.size(); i++) {
widgets.add(Widgets.createSlot(new Point(start.x + i * 20, start.y))
.entries(inputs.get(i))
.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的显示界面,主要是添加输入槽、输出槽和箭头

FillingUnitDisplay

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FillingUnitDisplay 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
15
private final List<EntryIngredient> inputs;
private final List<EntryIngredient> outputs;
public FillingUnitDisplay(FillingUnitRecipe recipe) {
this.inputs = recipe.getInput().stream().map(entry -> {
List<ItemStack> stacks = Arrays.asList(entry.ingredient().getMatchingStacks());
return EntryIngredients.ofItemStacks(
stacks.stream().map(stack -> {
ItemStack copy = stack.copy();
copy.setCount(entry.count());
return copy;
}).toList());
}).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 FillingUnitCategory.FILLING_UNIT;
}

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

注册 REI 内容

registerCategories

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

registry.addWorkstations(FillingUnitCategory.FILLING_UNIT, EntryStacks.of(ModBlocks.FILLING_UNIT));
}

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

registerDisplays

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

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

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