本篇教程的视频 (待发布)
本篇教程的源代码 (待发布) 本篇教程目标 编写一个精炼炉
介绍 前面的教程我们已经编写了一个矿机,那么这期教程我们就来写一个精炼炉
当然,特别的,它是一个单输入单输出的方块实体
同样的,我们也要为其配置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,并实现ExtendedScreenHandlerFactory和GeoBlockEntity,然后实现相关方法
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 public RefiningUnitBlockEntity (BlockPos pos, BlockState state) { super (ModBlockEntities.REFINING_UNIT, pos, state); } 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方法,返回RefiningUnitBlockEntity的tick,当然现在还没写
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 ; } }
添加字段 这里我们添加三个字段,分别是id、input和output,与之前的配方差不多
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类,比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" ); }
输入输出槽 接下来我们定义输入槽和输出槽,它依旧使用Fabric的Transfer 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 ; }
这个方法是用来获取加工箭头的长度,主要是根据progress和maxProgress计算出当前进度条的长度
注册屏幕 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我们也可以完善了
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的显示内容
好了,到这里我们就完成了全套流程,现在我们就可以进入游戏测试了,看看我们的精炼炉是否可以正常工作