本篇教程的视频
本篇教程的源代码
本篇教程目标
- 理解原版箱子的编写、注册
- 学会仿照原版的箱子编写储物类方块实体
查看源代码
本期教程是我们暑假期之前的唯一一个方块实体教程,这个想起来还算简单,而后的方块实体教程我们就得实现完整的方块实体编写
BE概念
这里我先简单提一下方块实体BlockEntity
这个概念和它具体的一个编写流程
BlockEntity
这个概念与ItemStack
类似,因为方块本身是无法存储数据
的,更别说处理一写复杂的逻辑了
而方块实体这个东西就是用来帮助方块存储数据和处理一些逻辑,同样ItemStack
也是用来存储物品数据的,
像物品的耐久度、数量等都是由物品堆栈存储的
原版的工作台
、附魔台
等等都是方块实体,它们能够处理一般方块不能实现的逻辑,比如加工物品,给物品附魔等等
所以方块实体就是和方块绑在一起的一种实体
,当然,因为是实体,或许对性能有所影响,但这方面我还没有具体研究过
编写流程
编写流程的话,一个基本的、带GUI
的方块实体是这样的:
- 方块类(定义方块
基本属性
,比如方块状态、碰撞箱)
- 方块注册类(
注册
方块)
- 方块实体类(定义方块实体的
处理逻辑
,比如加工某些物品)
- 方块实体注册类(
注册
方块实体)
- 方块实体渲染类(可以不写,但在方块类中要指定
渲染类型
,也可实现一些有意思的渲染)
- 屏幕类(指定
GUI
文件、背景等)
- 屏幕处理程序类(配合方块实体类,指定槽位,渲染加工过程的箭头等)
- 屏幕注册类(
注册
屏幕和屏幕处理程序类)
- 客户端类(
客户端
屏幕注册)
- 配方类型类(
json
文件编解码类,让方块实体类通过读取json文件来实现判断)
- 配方类型注册类(
注册
配方类型)
大体上就这些类要写,当然,不带GUI
的方块实体会稍微简单一点,你就不用写屏幕这些东西了
所以写一个方块实体其实并不是一件简单的事情,我的建议是原版
的那些方块实体你看看明白,
再来尝试写自己的方块实体
另外的那些工业类模组的方块实体也可以研究研究
在暑假的教程中,我们将一步步来讲解方块实体的编写
查看箱子方块类
当然,本期教程我们就简单一点,就让方块能存储东西就好了,其他的加工逻辑我们暂时不考虑
GUI
这一块也不用我们来写,直接拿原版的用就好了
这里我们先找ChestBlock
,也就是箱子的方块类
1
| public class ChestBlock extends AbstractChestBlock<ChestBlockEntity> implements Waterloggable
|
它是继承了AbstractChestBlock的,它的泛型是箱子的方块实体
AbstractChestBlock这个类其实没有什么好讲的,所以我们就还是看ChestBlock
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
| private static final DoubleBlockProperties.PropertyRetriever<ChestBlockEntity, Optional<NamedScreenHandlerFactory>> NAME_RETRIEVER = new DoubleBlockProperties.PropertyRetriever<ChestBlockEntity, Optional<NamedScreenHandlerFactory>>() { public Optional<NamedScreenHandlerFactory> getFromBoth(ChestBlockEntity chestBlockEntity, ChestBlockEntity chestBlockEntity2) { final Inventory inventory = new DoubleInventory(chestBlockEntity, chestBlockEntity2); return Optional.of(new NamedScreenHandlerFactory() { @Nullable @Override public ScreenHandler createMenu(int i, PlayerInventory playerInventory, PlayerEntity playerEntity) { if (chestBlockEntity.checkUnlocked(playerEntity) && chestBlockEntity2.checkUnlocked(playerEntity)) { chestBlockEntity.checkLootInteraction(playerInventory.player); chestBlockEntity2.checkLootInteraction(playerInventory.player); return GenericContainerScreenHandler.createGeneric9x6(i, playerInventory, inventory); } else { return null; } }
@Override public Text getDisplayName() { if (chestBlockEntity.hasCustomName()) { return chestBlockEntity.getDisplayName(); } else { return (Text)(chestBlockEntity2.hasCustomName() ? chestBlockEntity2.getDisplayName() : Text.translatable("container.chestDouble")); } } }); }
public Optional<NamedScreenHandlerFactory> getFrom(ChestBlockEntity chestBlockEntity) { return Optional.of(chestBlockEntity); }
public Optional<NamedScreenHandlerFactory> getFallback() { return Optional.empty(); } };
|
这个方法你看它的返回值类型就知道了,它的返回值是DoubleBlockProperties
两个箱子可以组合为一个大箱子,所以它这里会有这么一个方法,你打开任意一块都是打开整个箱子,
而不是打开其中一个
其中的GenericContainerScreenHandler.createGeneric9x6
,也就是创建54
个格子的大箱子GUI
,这也是原版格子数量最多的GUI
1 2 3 4 5 6 7 8
| public static DoubleBlockProperties.Type getDoubleBlockType(BlockState state) { ChestType chestType = state.get(CHEST_TYPE); if (chestType == ChestType.SINGLE) { return DoubleBlockProperties.Type.SINGLE; } else { return chestType == ChestType.RIGHT ? DoubleBlockProperties.Type.FIRST : DoubleBlockProperties.Type.SECOND; } }
|
同样下面还有一个方法配合这个双方块属性
当然,本期教程我们并不实现双方块属性,所以这里就了解一下就好了
1 2 3 4
| @Override public BlockRenderType getRenderType(BlockState state) { return BlockRenderType.ENTITYBLOCK_ANIMATED; }
|
这个就是方块类中的指定渲染类型的方法
因为箱子是有动画的,所以这里指定为ENTITYBLOCK_ANIMATED
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
| @Override public BlockState getStateForNeighborUpdate( BlockState state, Direction direction, BlockState neighborState, WorldAccess world, BlockPos pos, BlockPos neighborPos ) { if ((Boolean)state.get(WATERLOGGED)) { world.scheduleFluidTick(pos, Fluids.WATER, Fluids.WATER.getTickRate(world)); }
if (neighborState.isOf(this) && direction.getAxis().isHorizontal()) { ChestType chestType = neighborState.get(CHEST_TYPE); if (state.get(CHEST_TYPE) == ChestType.SINGLE && chestType != ChestType.SINGLE && state.get(FACING) == neighborState.get(FACING) && getFacing(neighborState) == direction.getOpposite()) { return state.with(CHEST_TYPE, chestType.getOpposite()); } } else if (getFacing(state) == direction) { return state.with(CHEST_TYPE, ChestType.SINGLE); }
return super.getStateForNeighborUpdate(state, direction, neighborState, world, pos, neighborPos); }
@Override public BlockState getPlacementState(ItemPlacementContext ctx) { ChestType chestType = ChestType.SINGLE; Direction direction = ctx.getHorizontalPlayerFacing().getOpposite(); FluidState fluidState = ctx.getWorld().getFluidState(ctx.getBlockPos()); boolean bl = ctx.shouldCancelInteraction(); Direction direction2 = ctx.getSide(); if (direction2.getAxis().isHorizontal() && bl) { Direction direction3 = this.getNeighborChestDirection(ctx, direction2.getOpposite()); if (direction3 != null && direction3.getAxis() != direction2.getAxis()) { direction = direction3; chestType = direction3.rotateYCounterclockwise() == direction2.getOpposite() ? ChestType.RIGHT : ChestType.LEFT; } }
if (chestType == ChestType.SINGLE && !bl) { if (direction == this.getNeighborChestDirection(ctx, direction.rotateYClockwise())) { chestType = ChestType.LEFT; } else if (direction == this.getNeighborChestDirection(ctx, direction.rotateYCounterclockwise())) { chestType = ChestType.RIGHT; } }
return this.getDefaultState().with(FACING, direction).with(CHEST_TYPE, chestType).with(WATERLOGGED, Boolean.valueOf(fluidState.getFluid() == Fluids.WATER)); }
|
这些方法应该不陌生了,用来更新方块
箱子的这些方法,也被我用在了明日方舟家具模组
中,未来也将作为教程放送
1 2 3 4 5 6 7 8 9 10 11 12
| @Override public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) { if (!state.isOf(newState.getBlock())) { BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity instanceof Inventory) { ItemScatterer.spawn(world, pos, (Inventory)blockEntity); world.updateComparators(pos, this); }
super.onStateReplaced(state, world, pos, newState, moved); } }
|
这个方法就是方块被破坏时调用的方法
当箱子里有东西的时候,会将里面的东西掉落出来,调用ItemScatterer.spawn
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @Override public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) { if (world.isClient) { return ActionResult.SUCCESS; } else { NamedScreenHandlerFactory namedScreenHandlerFactory = this.createScreenHandlerFactory(state, world, pos); if (namedScreenHandlerFactory != null) { player.openHandledScreen(namedScreenHandlerFactory); player.incrementStat(this.getOpenStat()); PiglinBrain.onGuardedBlockInteracted(player, true); }
return ActionResult.CONSUME; } }
|
右键箱子时,会打开箱子的GUI
,也就是调用player.openHandledScreen(namedScreenHandlerFactory)
方法
player.incrementStat
这个方法用来增加玩家的开箱子的次数,显示在你的统计数据里
PiglinBrain.onGuardedBlockInteracted
方法,用来判断这个箱子是否是猪灵
守卫的,因为我们在猪灵面前打开箱子的话,
猪灵就会来攻击我们
1 2 3 4
| @Override public BlockEntity createBlockEntity(BlockPos pos, BlockState state) { return new ChestBlockEntity(pos, state); }
|
这个是创建方块实体,返回ChestBlockEntity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| public static boolean isChestBlocked(WorldAccess world, BlockPos pos) { return hasBlockOnTop(world, pos) || hasCatOnTop(world, pos); }
private static boolean hasBlockOnTop(BlockView world, BlockPos pos) { BlockPos blockPos = pos.up(); return world.getBlockState(blockPos).isSolidBlock(world, blockPos); }
private static boolean hasCatOnTop(WorldAccess world, BlockPos pos) { List<CatEntity> list = world.getNonSpectatingEntities( CatEntity.class, new Box((double)pos.getX(), (double)(pos.getY() + 1), (double)pos.getZ(), (double)(pos.getX() + 1), (double)(pos.getY() + 2), (double)(pos.getZ() + 1)) ); if (!list.isEmpty()) { for (CatEntity catEntity : list) { if (catEntity.isInSittingPose()) { return true; } } }
return false; }
|
这两个方法用来判断箱子上面是否有方块或者猫
如果箱子上面的方块是实心
的或者猫
坐在上面时,箱子是无法打开的
OK,那么这是箱子方块类中的一部分源代码,其他的在我们教程中暂时还用不上
箱子方块实体类
接下来我们来看看箱子方块实体类
1
| public class ChestBlockEntity extends LootableContainerBlockEntity implements LidOpenable
|
它是继承了LootableContainerBlockEntity,也就是可以掉落物品的容器方块实体,同时实现了LidOpenable接口,这个是盖子可以打开的接口
1
| private DefaultedList<ItemStack> inventory = DefaultedList.ofSize(27, ItemStack.EMPTY);
|
这个是创建一个物品栏,这里是创建一个27格大小的物品栏,也就是普通箱子的大小
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
| private final ViewerCountManager stateManager = new ViewerCountManager() { @Override protected void onContainerOpen(World world, BlockPos pos, BlockState state) { ChestBlockEntity.playSound(world, pos, state, SoundEvents.BLOCK_CHEST_OPEN); }
@Override protected void onContainerClose(World world, BlockPos pos, BlockState state) { ChestBlockEntity.playSound(world, pos, state, SoundEvents.BLOCK_CHEST_CLOSE); }
@Override protected void onViewerCountUpdate(World world, BlockPos pos, BlockState state, int oldViewerCount, int newViewerCount) { ChestBlockEntity.this.onViewerCountUpdate(world, pos, state, oldViewerCount, newViewerCount); }
@Override protected boolean isPlayerViewing(PlayerEntity player) { if (!(player.currentScreenHandler instanceof GenericContainerScreenHandler)) { return false; } else { Inventory inventory = ((GenericContainerScreenHandler)player.currentScreenHandler).getInventory(); return inventory == ChestBlockEntity.this || inventory instanceof DoubleInventory && ((DoubleInventory)inventory).isPart(ChestBlockEntity.this); } } };
|
这个是打开箱子的玩家计数管理器,这个我在教程中没说要不要写
后来我考虑了一下,应该是要写的,不然在多人游戏中,可能会出问题
1 2 3 4 5 6 7 8 9
| @Override public int size() { return 27; }
@Override protected Text getContainerName() { return Text.translatable("container.chest"); }
|
这两个方法就是获取物品栏的大小和获取箱子名字的
箱子名字可以用语言文件
来翻译
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Override public void readNbt(NbtCompound nbt) { super.readNbt(nbt); this.inventory = DefaultedList.ofSize(this.size(), ItemStack.EMPTY); if (!this.deserializeLootTable(nbt)) { Inventories.readNbt(nbt, this.inventory); } }
@Override protected void writeNbt(NbtCompound nbt) { super.writeNbt(nbt); if (!this.serializeLootTable(nbt)) { Inventories.writeNbt(nbt, this.inventory); } }
|
这两个方法就是读取
和写入
NBT数据,这两个方法尤其重要,用来保存存储在方块中的数据
这两个方法不写的话,你存在箱子里的东西就消失了
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Override public void onOpen(PlayerEntity player) { if (!this.removed && !player.isSpectator()) { this.stateManager.openContainer(player, this.getWorld(), this.getPos(), this.getCachedState()); } }
@Override public void onClose(PlayerEntity player) { if (!this.removed && !player.isSpectator()) { this.stateManager.closeContainer(player, this.getWorld(), this.getPos(), this.getCachedState()); } }
|
结合上面的计数管理器,用来决定箱子的开关
1 2 3 4 5 6 7 8 9
| @Override protected DefaultedList<ItemStack> getInvStackList() { return this.inventory; }
@Override protected void setInvStackList(DefaultedList<ItemStack> list) { this.inventory = list; }
|
这两个方法用来获取和设置物品栏
1 2 3 4
| @Override protected ScreenHandler createScreenHandler(int syncId, PlayerInventory playerInventory) { return GenericContainerScreenHandler.createGeneric9x3(syncId, playerInventory, this); }
|
这个方法用来创建屏幕,也就是打开箱子后显示的界面,这里创建的是9x3
的箱子界面
那么源代码要看的东西基本上就是这些了,我们可以来写自己的储物类方块
编写方块
方块实体类
首先我们创建一个SimpleCabinetBlockEntity方块实体类,继承LootableContainerBlockEntity
1 2 3 4 5 6
| public class SimpleCabinetBE extends LootableContainerBlockEntity {
protected SimpleCabinetBE(BlockEntityType<?> blockEntityType, BlockPos blockPos, BlockState blockState) { super(blockEntityType, blockPos, blockState); } }
|
因为后面的方块类中会用到方块实体类,所以我们先创建方块实体类
这里和原版的箱子方块实体一样,我们先来创建物品栏
1 2 3 4 5
| private DefaultedList<ItemStack> inv = createInventory();
private DefaultedList<ItemStack> createInventory() { return DefaultedList.ofSize(27, ItemStack.EMPTY); }
|
当然,这里我是直接拿了一个方法来创建物品栏,原来明日方舟家具模组中,这个是一个抽象类,这样写的话,
可以让继承的方块实体类可以改变实际物品栏的大小,重写这个createInventory
就好了
另外,我们再重写一些方法
1 2 3 4
| @Override protected DefaultedList<ItemStack> getInvStackList() { return this.inv; }
|
这个方法用来获取物品栏
1 2 3 4
| @Override protected void setInvStackList(DefaultedList<ItemStack> list) { this.inv = list; }
|
这个方法用来设置物品栏
1 2 3 4
| @Override protected Text getContainerName() { return Text.translatable("container.simple_cabinet"); }
|
这个方法用来获取箱子名字,之后用语言文件
翻译它
1 2 3 4
| @Override protected ScreenHandler createScreenHandler(int syncId, PlayerInventory playerInventory) { return GenericContainerScreenHandler.createGeneric9x3(syncId, playerInventory, this); }
|
这个方法用来创建屏幕,这里创建的是9x3
的箱子界面
1 2 3 4
| @Override public int size() { return 27; }
|
这个方法用来获取物品栏的大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Override public void readNbt(NbtCompound nbt) { super.readNbt(nbt); this.inv = DefaultedList.ofSize(this.size(), ItemStack.EMPTY); if (!this.deserializeLootTable(nbt)) { Inventories.readNbt(nbt, this.inv); } }
@Override protected void writeNbt(NbtCompound nbt) { super.writeNbt(nbt); if (!this.serializeLootTable(nbt)) { Inventories.writeNbt(nbt, this.inv); } }
|
最后还有读取
和写入
NBT的方法,用于保存数据
另外,我们还要写一个构造函数,这个构造函数是给方块类创建方块实体时用的
1 2 3
| public SimpleCabinetBE(BlockPos blockPos, BlockState blockState) { this(ModBlockEntities.SIMPLE_CABINET, blockPos, blockState); }
|
当然,这里的ModBlockEntities.SIMPLE_CABINET
是注册方块实体,我们之后会写
方块类
接下来创建一个SimpleCabinet
方块类,继承AbstractChestBlock<SimpleCabinetBE>
1 2 3 4 5 6
| public class SimpleCabinet extends AbstractChestBlock<SimpleCabinetBE> {
public SimpleCabinet(Settings settings, Supplier<BlockEntityType<? extends SimpleCabinetBE>> blockEntityTypeSupplier) { super(settings, blockEntityTypeSupplier); } }
|
这里还要求我们重写一些方法
1 2 3 4
| @Override public DoubleBlockProperties.PropertySource<? extends ChestBlockEntity> getBlockEntitySource(BlockState state, World world, BlockPos pos, boolean ignoreBlocked) { return null; }
|
这个DoubleBlockProperties
我们就返回null
,教程中我们并不使用这个双方块这个属性
1 2 3 4
| @Override public @Nullable BlockEntity createBlockEntity(BlockPos pos, BlockState state) { return new SimpleCabinetBE(pos, state); }
|
这个方法用来创建方块实体,我们返回我们创建的SimpleCabinetBE
1 2 3 4
| @Override public BlockRenderType getRenderType(BlockState state) { return BlockRenderType.MODEL; }
|
这个方法用来获取渲染类型,我们返回BlockRenderType.MODEL
,这样方块就会渲染模型,
然后方块实体的渲染器类就不用写了
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 factory = this.createScreenHandlerFactory(state, world, pos); if (factory != null) { player.openHandledScreen(factory); return ActionResult.SUCCESS; } } return ActionResult.CONSUME; }
|
重写onUse
方法,让玩家打开物品栏
写法上与原版一样
1 2 3 4 5 6 7 8 9 10 11
| @Override public void onStateReplaced(BlockState state, World world, BlockPos pos, BlockState newState, boolean moved) { if (!state.isOf(newState.getBlock())) { BlockEntity blockEntity = world.getBlockEntity(pos); if (blockEntity instanceof Inventory) { ItemScatterer.spawn(world, pos, (Inventory)blockEntity); world.updateComparators(pos, this); } } super.onStateReplaced(state, world, pos, newState, moved); }
|
重写onStateReplaced
方法,当方块被替换时,将方块实体中的物品掉落
那么现在,方块实体我们也就写完了,接下来我们来注册方块实体
注册
注册方块实体
首先,我们创建一个ModBlockEntities
类,用来注册方块实体
1 2 3
| public class ModBlockEntities {
}
|
方块实体的注册也可以看源代码,在BlockEntityType
这个类中
我们先写一个注册方法
1 2 3 4
| private static <T extends BlockEntity> BlockEntityType<T> create(String id, BlockEntityType.Builder<T> builder) { Type<?> type = Util.getChoiceType(TypeReferences.BLOCK_ENTITY, id); return Registry.register(Registries.BLOCK_ENTITY_TYPE, id, builder.build(type)); }
|
这个方法是根据原版的方法来改的,加上我们模组的命名空间
随后我们就可以利用这个方法来注册方块实体了
1 2
| public static final BlockEntityType<SimpleCabinetBE> SIMPLE_CABINET = create("simple_cabinet", BlockEntityType.Builder.create(SimpleCabinetBE::new, ModBlocks.SIMPLE_CABINET));
|
这里我们注册了一个SimpleCabinetBE
方块实体,id
为simple_cabinet
当然ModBlocks.SIMPLE_CABINET
是方块注册,我们后面会写
这个类中还要写一个用于初始化的方法
1 2 3
| public static void registerBlockEntities() {
}
|
并且要在模组主类中调用
1
| ModBlockEntities.registerBlockEntities();
|
注册方块
1 2
| public static final Block SIMPLE_CABINET = register("simple_cabinet", new SimpleCabinet(AbstractBlock.Settings.create().strength(2.0f, 6.0f).nonOpaque(), () -> ModBlockEntities.SIMPLE_CABINET));
|
实例化SimpleCabinet
,并注册,后面写上一个lambda
表达式,返回我们注册的SimpleCabinetBE
方块实体
这样,报错的地方都没有了
好,现在我们的储物类方块就已经写完了
加入物品栏
1
| entries.add(ModBlocks.SIMPLE_CABINET);
|
不要忘了加入物品栏
数据文件
语言文件
1 2 3
| translationBuilder.add(ModBlocks.SIMPLE_CABINET, "Simple Cabinet");
translationBuilder.add("container.simple_cabinet", "Simple Cabinet");
|
翻译的有两个,一个是方块的,一个是方块实体GUI上的
模型文件
1
| blockStateModelGenerator.registerSimpleState(ModBlocks.SIMPLE_CABINET);
|
方块的模型文件我们还是拿Blockbench
制作的,然后这里我们写一个最简单的方块状态文件
测试
那么现在我们就可以进入游戏进行测试了