本篇教程的视频

本篇教程的源代码

Github地址:TutorialMod-Box-1.21

介绍

方块实体BlockEntityTileEntity)确切来讲既不是方块,也不是实体,它这个概念就和ItemStack一样

不过,其实你可以注意到在游戏里面,方块实体是被划分在Entity里面的,参见玩速通时候的饼状图

普通方块它是不能存储数据的,假设说你想在一个方块里放一个物品,普通的方块是无法存储这个物品相关信息的,自然我们也就无法在这个方块里放物品了

方块实体就是可以实现这个功能,它与对应的方块绑定,存储相关数据、执行tick相关的任务(方块实体自带randomTick,普通方块还得自己设置),还有动态渲染(比如附魔台的书)等等

ItemStack同理,它是存储物品相关数据的

那么从本篇教程开始,我们就来讲方块实体

教程将讲两个案例,其中一个是本篇的仿制原版箱子的方块实体,另一个是我们自定义的方块实体

在下一个案例中,我们将完整编写一个带有GUI的、可以处理方块或者物品的方块实体

在那之后,我们还将为方块实体添加配方类型,用json文件来定义方块实体的配方,以及后续的REI适配,显示方块实体的配方

原版的方块实体也由很多,比如工作台附魔台熔炼高炉等等,这些都是方块实体,我们可以参考原版的方块实体来自定义你的方块实体

当然,工业模组里面也有大量的方块实体,也值得我们去学习

我们编写方块实体的主要思路大概长这样(这是我自己在开发过程中总结的):

  1. 编写自定义方块实体,注册方块实体
  2. 编写自定义的方块,并注册方块
  3. 编写方块实体的屏幕程序,这个是GUI相关的东西
  4. 编写方块实体的屏幕处理程序,注册屏幕处理程序

当然,这四步是带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方法是用来创建方块实体的,它的参数是idbuilder,同样的,这个方法改一改就可以被我们使用了

注册方块实体

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的屏幕处理,还有9X19X29X49X59X6这几个

但是,除了9X39X6的,其他的如果直接调用会有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);

当然,不要忘了我们的材质文件,全部放好以后,我们就可以启动我们的游戏了