本篇教程的视频

本篇教程的源代码

GitHub地址:

查看源代码

本期教程我们来写一个使用自己方块的床类方块

当然,我们先看看原版的床是怎么写的,这里我们来看BedBlock

我们这一次写这个案例是直接继承BedBlock的,但它里面的有些东西我们还是得了解一下

1
2
public static final EnumProperty<BedPart> PART = BlockStateProperties.BED_PART;
public static final BooleanProperty OCCUPIED = BlockStateProperties.OCCUPIED;

两个方块的属性,一个是分,一个是这个床是否被占领

后面是一堆碰撞箱,这里就略过

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
@Override
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
if (level.isClientSide) {
return InteractionResult.CONSUME;
} else {
if (state.getValue(PART) != BedPart.HEAD) {
pos = pos.relative(state.getValue(FACING));
state = level.getBlockState(pos);
if (!state.is(this)) {
return InteractionResult.CONSUME;
}
}

if (!canSetSpawn(level)) {
level.removeBlock(pos, false);
BlockPos blockpos = pos.relative(state.getValue(FACING).getOpposite());
if (level.getBlockState(blockpos).is(this)) {
level.removeBlock(blockpos, false);
}

Vec3 vec3 = pos.getCenter();
level.explode(null, level.damageSources().badRespawnPointExplosion(vec3), null, vec3, 5.0F, true, Level.ExplosionInteraction.BLOCK);
return InteractionResult.SUCCESS;
} else if (state.getValue(OCCUPIED)) {
if (!this.kickVillagerOutOfBed(level, pos)) {
player.displayClientMessage(Component.translatable("block.minecraft.bed.occupied"), true);
}

return InteractionResult.SUCCESS;
} else {
player.startSleepInBed(pos).ifLeft(p_49477_ -> {
if (p_49477_.getMessage() != null) {
player.displayClientMessage(p_49477_.getMessage(), true);
}
});
return InteractionResult.SUCCESS;
}
}
}

这个是床的交互,也就是当玩家右键床的时候执行的逻辑

首先判断是否是床头,如果不是床头,就获取床头的位置,
然后判断床头是否存在,如果不存在,就返回

然后通过canSetSpawn判断床是否可以在当前维度下正常运行,如果不在运行,就移除床,并爆炸

1
2
3
public static boolean canSetSpawn(Level level) {
return level.dimensionType().bedWorks();
}

下界末地,床都是会爆炸的

然后判断床是否被占领,如果床是被村民占领的,就唤醒村民,但如果床不是被村民占领的,
比如你在多人游戏中,床被另外一个玩家占领,那么你右键床的时候,就会输出提示信息

1
2
3
4
5
6
7
8
9
private boolean kickVillagerOutOfBed(Level level, BlockPos pos) {
List<Villager> list = level.getEntitiesOfClass(Villager.class, new AABB(pos), LivingEntity::isSleeping);
if (list.isEmpty()) {
return false;
} else {
list.get(0).stopSleeping();
return true;
}
}

最后,如果床没有被占领,那么玩家就可以尝试睡眠

当然还会进行判断,当不符合条件时,也会提醒玩家,比如离床太远周围有怪物在游荡,你不能休息等等

至于重置玩家重生点的方法是在ServerPlayerEntitytrySleep方法中,有单独的方法来判定是否重置玩家重生点

1
2
3
4
@Override
public void fallOn(Level level, BlockState state, BlockPos pos, Entity entity, float fallDistance) {
super.fallOn(level, state, pos, entity, fallDistance * 0.5F);
}

这个方法是当实体坠落在床上时,会被弹起一段距离(黏液块中也有类似的方法)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Override
public void updateEntityAfterFallOn(BlockGetter level, Entity entity) {
if (entity.isSuppressingBounce()) {
super.updateEntityAfterFallOn(level, entity);
} else {
this.bounceUp(entity);
}
}

private void bounceUp(Entity entity) {
Vec3 vec3 = entity.getDeltaMovement();
if (vec3.y < 0.0) {
double d0 = entity instanceof LivingEntity ? 1.0 : 0.8;
entity.setDeltaMovement(vec3.x, -vec3.y * 0.66F * d0, vec3.z);
}
}

这两个方法让你可以在床上蹦跶,会弹起来

1
2
3
4
5
6
7
8
9
10
11
public static Optional<Vec3> findStandUpPosition(EntityType<?> pEntityType, CollisionGetter pCollisionGetter, BlockPos pPos, Direction pDirection, float pYRot) {
...
}

private static Optional<Vec3> findBunkBedStandUpPosition(EntityType<?> pEntityType, CollisionGetter pCollisionGetter, BlockPos pPos, Direction pStateFacing, Direction pEntityFacing) {
...
}

private static Optional<Vec3> findStandUpPositionAtOffset(EntityType<?> pEntityType, CollisionGetter pCollisionGetter, BlockPos pPos, int[][] pOffsets, boolean pSimulate) {
...
}

这些方法显然易见,是寻找玩家起床后,用来重置玩家的位置

其他的方法的话,我们就不在这里讲了,另外的是关于这个床方块放置的方法,毕竟这个床也是分了两个部分的,
与门类似

未来我们的教程也会涉及放下一个方块后生成另外部分方块的教程,到那个时候我们再具体来讲好了

创建自定义床类

这里我们还是要继承BedBlock来写我们的床类方块,因为实际上trySleep方法是会判断这个方块是否是BedBlock的实例

所以除非你使用Mixin,不然就只能继承BedBlock来写床类方块

当然,这个Mixin也不是很难写

1
2
3
4
5
public class ModBedBlock extends BedBlock {
public ModBedBlock(DyeColor color, Properties properties) {
super(color, properties);
}
}

这里我们创建ModBedBlock,并创建构造函数

然后我们写个碰撞箱

1
2
3
4
5
6
public static final VoxelShape SHAPE = Block.box(0, 0, 0, 16, 8, 16);

@Override
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return SHAPE;
}

这个大小是一个半砖的大小,与原版床的高度一样,当然你要更加细致就参考原版的床来好了

另外,我们再重写getRenderType

1
2
3
4
5
6
7
8
@Override
protected RenderShape getRenderShape(BlockState state) {
if (state.getValue(PART) == BedPart.HEAD) {
return RenderShape.MODEL;
} else {
return RenderShape.INVISIBLE;
}
}

因为原版的床是两个部分,但我们的床是拿Blockbench制作的一个完整的模型,所以得隐藏其中一个部分

当然,我这种实际上属于偷懒的做法,你也可以参考原版床的放置逻辑

注册方块

注册

1
2
3
public static final RegistryObject<Block> BED = 
registerBlocks("bed",
() -> new ModBedBlock(DyeColor.BLACK, BlockBehaviour.Properties.of().strength(2.0f, 2.0f).noOcclusion()));

那么接下来我们就注册这个床,这里要填一个DyeColor,随便写一个就可以了

物品栏

不要忘记物品栏

1
output.accept(ModBlocks.BED);

注册方块实体

因为1.21.1 改了一些底层逻辑,所以如果直接用之前 1.20.1 的方法添加床会出问题

这里我们还需要注册方块实体

数据文件

战利品列表

1
add(ModBlocks.BED.get(), createSinglePropConditionTable(ModBlocks.BED.get(), BedBlock.PART, BedPart.HEAD));

战利品列表的写法有些特殊,因为床是两个方块,而我们只希望它只会有一个掉落物,所以这里的战利品列表要给它加上一个条件

只有方块类型是BedPart.HEAD,才会有掉落物

语言文件

1
add(ModBlocks.BED.get(), "Bed");

模型文件

1
customHorizontalBlock(ModBlocks.BED);

床的话,它是带有FACING参数的,所以这里我们使用customHorizontalBlock(之前的自定义方法)来生成方块状态文件

另外的方块模型我们依旧拿Blockbench制作

测试

做完一切之后我们就可以进入游戏进行测试了