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