本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-Chest-1.20.1

本篇教程目标

  • 理解原版箱子的编写、注册
  • 学会仿照原版的箱子编写储物类方块实体

查看源代码

本期教程是我们暑假期之前的唯一一个方块实体教程,这个想起来还算简单,而后的方块实体教程我们就得实现完整的方块实体编写

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方块实体,idsimple_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制作的,然后这里我们写一个最简单的方块状态文件

测试

那么现在我们就可以进入游戏进行测试了