本篇教程的视频
本篇教程的源代码 介绍 方块实体
(BlockEntity
或TileEntity
)确切来讲既不是方块,也不是实体,它这个概念就和ItemStack
一样
不过,其实你可以注意到在游戏里面,方块实体是被划分在Entity里面的,参见玩速通时候的饼状图
普通方块
它是不能
存储数据的,假设说你想在一个方块里放一个物品,普通的方块是无法存储这个物品相关信息的,自然我们也就无法在这个方块里放物品了
而方块实体
就是可以实现这个功能,它与对应的方块绑定,存储相关数据、执行tick相关的任务(方块实体自带randomTick
,普通方块还得自己设置),还有动态渲染(比如附魔台的书)等等
ItemStack同理,它是存储物品相关数据的
那么从本篇教程开始,我们就来讲方块实体
教程将讲两个案例,其中一个是本篇的仿制原版箱子
的方块实体,另一个是我们自定义的方块实体
在下一个案例中,我们将完整编写一个带有GUI
的、可以处理
方块或者物品的方块实体
在那之后,我们还将为方块实体
添加配方类型
,用json
文件来定义方块实体的配方,以及后续的REI
适配,显示方块实体的配方
原版的方块实体也由很多,比如工作台
、附魔台
、熔炼
、高炉
等等,这些都是方块实体,我们可以参考原版的方块实体来自定义你的方块实体
当然,工业模组里面也有大量的方块实体,也值得我们去学习
我们编写方块实体的主要思路大概长这样(这是我自己在开发过程中总结的):
编写自定义方块实体,注册方块实体
编写自定义的方块,并注册方块
编写方块实体的屏幕程序,这个是GUI相关的东西
编写方块实体的屏幕处理程序,注册屏幕处理程序
当然,这四步是带GUI
的方块实体,但是在实际开发过程中,我们这几步其实是杂合的,并不是完全按照顺序来写的,东写一点,西写一点,后面一篇教程尤其
查看源代码 那么我们这里先来看看原版的箱子方块实体,这是一类可以储物的方块实体
ChestBlock方块 1 2 3 4 5 6 7 8 9 public class ChestBlock extends AbstractChestBlock <ChestBlockEntity> implements Waterloggable { ... public ChestBlock (AbstractBlock.Settings settings, Supplier<BlockEntityType<? extends ChestBlockEntity>> supplier) { super (settings, supplier); ... ); } ... }
这个是箱子方块,它继承了AbstractChestBlock
,并且实现了Waterloggable
接口
AbstractChestBlock
泛型是ChestBlockEntity
,这个是箱子方块对应的方块实体
Waterloggable
是含水方块的接口,方块的含水特性就是这个接口来实现的
这里面的方法其实不用看什么,除非你希望完全实现原版箱子的各种逻辑
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 25 26 27 28 29 30 31 32 33 34 35 36 37 public class ChestBlockEntity extends LootableContainerBlockEntity implements LidOpenable { ... private DefaultedList<ItemStack> inventory = DefaultedList.ofSize(27 , ItemStack.EMPTY); ... protected ChestBlockEntity (BlockEntityType<?> blockEntityType, BlockPos blockPos, BlockState blockState) { super (blockEntityType, blockPos, blockState); } public ChestBlockEntity (BlockPos pos, BlockState state) { this (BlockEntityType.CHEST, pos, state); } @Override public int size () { return 27 ; } @Override protected Text getContainerName () { return Text.translatable("container.chest" ); } ... @Override protected DefaultedList<ItemStack> getHeldStacks () { return this .inventory; } ... @Override protected ScreenHandler createScreenHandler (int syncId, PlayerInventory playerInventory) { return GenericContainerScreenHandler.createGeneric9x3(syncId, playerInventory, this ); } ... }
而这个方块实体类我们是要好好看看的,主要的也是看这里的几个方法
它的构造函数有两个,前一个是super
的构造函数,后一个是调用前一个的,后面的那一个是为了方便方块创建方块实体的
inventory
是一个DefaultedList
,这个是箱子的物品栏,大小是27
,这个是箱子的默认的大小,可以结合这里没有显示的方法,看看在什么情况下会改变这个大小
size
方法是返回箱子的默认大小
getContainerName
方法是返回箱子的名字,这个是用来显示在GUI上的
getHeldStacks
方法是返回箱子的物品栏,这个是用来操作物品栏的
createScreenHandler
方法是返回箱子的屏幕处理程序,这个是用来打开GUI的,这里的GenericContainerScreenHandler
是一个通用的物品栏屏幕处理程序,我们写的时候也可以直接用这个
BlockEntityType 1 public static final BlockEntityType<ChestBlockEntity> CHEST = create("chest" , BlockEntityType.Builder.create(ChestBlockEntity::new , Blocks.CHEST));
我们再进一步寻找方块实体注册的地方,在BlockEntityType
里面,我们可以看到这个CHEST
,这个是箱子的方块实体
1 2 3 4 5 6 7 8 private static <T extends BlockEntity > BlockEntityType<T> create (String id, BlockEntityType.Builder<T> builder) { if (builder.blocks.isEmpty()) { LOGGER.warn("Block entity type {} requires at least one valid block to be defined!" , id); } Type<?> type = Util.getChoiceType(TypeReferences.BLOCK_ENTITY, id); return Registry.register(Registries.BLOCK_ENTITY_TYPE, id, builder.build(type)); }
这个create
方法是用来创建方块实体的,它的参数是id
和builder
,同样的,这个方法改一改就可以被我们使用了
注册方块实体 BoxBlockEntity 这里我们创建BoxBlockEntity
,这个是我们仿制原版箱子的方块实体
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 class BoxBlockEntity extends LootableContainerBlockEntity { public BoxBlockEntity (BlockEntityType<?> blockEntityType, BlockPos blockPos, BlockState blockState) { super (blockEntityType, blockPos, blockState); } @Override protected Text getContainerName () { return null ; } @Override protected DefaultedList<ItemStack> getHeldStacks () { return null ; } @Override protected void setHeldStacks (DefaultedList<ItemStack> inventory) { } @Override protected ScreenHandler createScreenHandler (int syncId, PlayerInventory playerInventory) { return null ; } @Override public int size () { return 0 ; } }
这里要重写的方法就是我们上面介绍的那几个,简单重写一下即可
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 public class BoxBlockEntity extends LootableContainerBlockEntity { private DefaultedList<ItemStack> inventory = DefaultedList.ofSize(27 , ItemStack.EMPTY); public BoxBlockEntity (BlockEntityType<?> blockEntityType, BlockPos blockPos, BlockState blockState) { super (blockEntityType, blockPos, blockState); } @Override protected Text getContainerName () { return Text.translatable("container.box" ); } @Override protected DefaultedList<ItemStack> getHeldStacks () { return this .inventory; } @Override protected void setHeldStacks (DefaultedList<ItemStack> inventory) { this .inventory = inventory; } @Override protected ScreenHandler createScreenHandler (int syncId, PlayerInventory playerInventory) { return GenericContainerScreenHandler.createGeneric9x3(syncId, playerInventory, this ); } @Override public int size () { return 27 ; } }
这里创建的屏幕和屏幕处理程序我们就直接用原版的,这样我们就不用自己去写了
其实原版除了9X3
的屏幕处理,还有9X1
、9X2
、9X4
、9X5
、9X6
这几个
但是,除了9X3
和9X6
的,其他的如果直接调用会有bug,你可以自己试试,解决方法其实也简单,不过,就交给大家自己探索了
(补充内容)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 @Override protected void readNbt (NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { super .readNbt(nbt, registryLookup); this .inventory = DefaultedList.ofSize(this .size(), ItemStack.EMPTY); if (!this .readLootTable(nbt)) { Inventories.readNbt(nbt, this .inventory, registryLookup); } } @Override protected void writeNbt (NbtCompound nbt, RegistryWrapper.WrapperLookup registryLookup) { super .writeNbt(nbt, registryLookup); if (!this .writeLootTable(nbt)) { Inventories.writeNbt(nbt, this .inventory, registryLookup); } }
这两个方法是用来读取和写入NBT的,这个是用来保存箱子的物品栏的,也就是在退出世界时,箱子里的物品不会丢失
(咋没人说这个问题嘞,视频教程忘记说了,我自己写方舟模组才发现的)
创建ModBlockEntities 接下来我们创建ModBlockEntities
,这个是我们的方块实体注册类
1 2 3 4 5 6 7 8 public class ModBlockEntities { 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, Identifier.of(TutorialMod.MOD_ID, id), builder.build(type)); } }
这里的注册方法和原版的一样,只是命名空间是我们的,同时中间的日志去掉了
随后我们注册我们的方块实体
1 public static final BlockEntityType<BoxBlockEntity> BOX = create("box" , BlockEntityType.Builder.create(BoxBlockEntity::new , ModBlocks.BOX));
当然,这里的ModBlocks.BOX
会报错,这个方块我们还没有创建
最后也不要忘了初始化注册方法
1 2 3 public static void registerBlockEntities () {}
主类调用
1 ModBlockEntities.registerBlockEntities();
再写一个构造函数 我们再写一个BoxBlockEntity
的构造函数,前面也提到过了,为了我们方块实体和方块的绑定
1 2 3 public BoxBlockEntity (BlockPos pos, BlockState state) { this (ModBlockEntities.BOX, pos, state); }
创建BoxBlock 接下来我们创建BoxBlock
,这个是我们的方块,继承AbstractChestBlock
,泛型是我们的方块实体BoxBlockEntity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 public class BoxBlock extends AbstractChestBlock <BoxBlockEntity> { public BoxBlock (Settings settings, Supplier<BlockEntityType<? extends BoxBlockEntity>> blockEntityTypeSupplier) { super (settings, blockEntityTypeSupplier); } @Override protected MapCodec<? extends AbstractChestBlock <BoxBlockEntity>> getCodec() { return null ; } @Override public DoubleBlockProperties.PropertySource<? extends ChestBlockEntity > getBlockEntitySource(BlockState state, World world, BlockPos pos, boolean ignoreBlocked) { return null ; } @Nullable @Override public BlockEntity createBlockEntity (BlockPos pos, BlockState state) { return null ; } }
创建super
的构造函数,重写一些方法
这里我们注意到相比较以前的版本,这里多了一个getCodec
方法,这个是获取编解码器。编解码器用于方块实体数据的序列化和反序列化,本质是用于网络传输的
在1.21
中,几乎每一个方块实体都有这种编解码器,这里我们就简单写一个即可,因为我们没有什么特殊的数据
1 public static final MapCodec<BoxBlock> CODEC = createCodec(settings -> new BoxBlock (settings, () -> ModBlockEntities.BOX));
随后返回这个编解码器
1 2 3 4 @Override protected MapCodec<? extends AbstractChestBlock <BoxBlockEntity>> getCodec() { return CODEC; }
再接下来的getBlockEntitySource
我们不用管,这个是双方块的一些东西(因为两个箱子可以组成一个大箱子),我们这里不涉及
createBlockEntity
方法我们要返回我们的方块实体
1 2 3 4 5 @Nullable @Override public BlockEntity createBlockEntity (BlockPos pos, BlockState state) { return new BoxBlockEntity (pos, state); }
这里用BoxBlockEntity
我们重新写的构造函数,这样,当我们放置方块的时候,方块实体就会被创建
那么除此之外,还有几个方法我们要重写
1 2 3 4 @Override protected BlockRenderType getRenderType (BlockState state) { return BlockRenderType.MODEL; }
一个是这里的渲染,我们让它渲染为MODEL
,也就是模型长什么样它就渲染成什么样
渲染是必须得写的,否则你一放置方块,游戏就会崩溃(别问我怎么知道的,毕竟已经好几次没写了)
1 2 3 4 5 6 7 8 9 10 11 @Override protected ActionResult onUse (BlockState state, World world, BlockPos pos, PlayerEntity player, 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
方法,这个是方块被右键点击的时候的方法,这里是为了打开我们的GUI,而后我们就可以存放物品了
注册方块 我们来注册我们的方块,按照原版的写法即可
1 public static final Block BOX = register("box" , new BoxBlock (AbstractBlock.Settings.copy(Blocks.CHEST), () -> ModBlockEntities.BOX));
写好之后,ModBlockEntities
里面也就不会报错了
数据文件 语言文件 1 2 translationBuilder.add(ModBlocks.BOX, "Box" ); translationBuilder.add("container.box" , "Box" );
一个是方块的名字,一个是显示在GUI上的名字
模型文件 这一次我们还是那BlockBench
做了一个简单的模型,所以我们只需生成一个简单的方块状态文件即可
1 blockStateModelGenerator.registerSimpleState(ModBlocks.BOX);
当然,不要忘了我们的材质文件,全部放好以后,我们就可以启动我们的游戏了