本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-Bed-1.20.1

本篇教程目标

  • 理解原版的床类
  • 会自己编写床类方块

查看源代码

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

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

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

1
2
public static final EnumProperty<BedPart> PART = Properties.BED_PART;
public static final BooleanProperty OCCUPIED = Properties.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
public ActionResult onUse(BlockState state, World world, BlockPos pos, PlayerEntity player, Hand hand, BlockHitResult hit) {
if (world.isClient) {
return ActionResult.CONSUME;
} else {
if (state.get(PART) != BedPart.HEAD) {
pos = pos.offset(state.get(FACING));
state = world.getBlockState(pos);
if (!state.isOf(this)) {
return ActionResult.CONSUME;
}
}

if (!isBedWorking(world)) {
world.removeBlock(pos, false);
BlockPos blockPos = pos.offset(((Direction)state.get(FACING)).getOpposite());
if (world.getBlockState(blockPos).isOf(this)) {
world.removeBlock(blockPos, false);
}

Vec3d vec3d = pos.toCenterPos();
world.createExplosion(null, world.getDamageSources().badRespawnPoint(vec3d), null, vec3d, 5.0F, true, World.ExplosionSourceType.BLOCK);
return ActionResult.SUCCESS;
} else if ((Boolean)state.get(OCCUPIED)) {
if (!this.wakeVillager(world, pos)) {
player.sendMessage(Text.translatable("block.minecraft.bed.occupied"), true);
}

return ActionResult.SUCCESS;
} else {
player.trySleep(pos).ifLeft(reason -> {
if (reason.getMessage() != null) {
player.sendMessage(reason.getMessage(), true);
}
});
return ActionResult.SUCCESS;
}
}
}

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

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

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

1
2
3
public static boolean isBedWorking(World world) {
return world.getDimension().bedWorks();
}

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

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

1
2
3
4
5
6
7
8
9
private boolean wakeVillager(World world, BlockPos pos) {
List<VillagerEntity> list = world.getEntitiesByClass(VillagerEntity.class, new Box(pos), LivingEntity::isSleeping);
if (list.isEmpty()) {
return false;
} else {
((VillagerEntity)list.get(0)).wakeUp();
return true;
}
}

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

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

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

1
2
3
4
@Override
public void onLandedUpon(World world, BlockState state, BlockPos pos, Entity entity, float fallDistance) {
super.onLandedUpon(world, 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 onEntityLand(BlockView world, Entity entity) {
if (entity.bypassesLandingEffects()) {
super.onEntityLand(world, entity);
} else {
this.bounceEntity(entity);
}
}

private void bounceEntity(Entity entity) {
Vec3d vec3d = entity.getVelocity();
if (vec3d.y < 0.0) {
double d = entity instanceof LivingEntity ? 1.0 : 0.8;
entity.setVelocity(vec3d.x, -vec3d.y * 0.66F * d, vec3d.z);
}
}

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

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
49
50
51
52
53
public static Optional<Vec3d> findWakeUpPosition(EntityType<?> type, CollisionView world, BlockPos pos, Direction bedDirection, float spawnAngle) {
Direction direction = bedDirection.rotateYClockwise();
Direction direction2 = direction.pointsTo(spawnAngle) ? direction.getOpposite() : direction;
if (isBedBelow(world, pos)) {
return findWakeUpPosition(type, world, pos, bedDirection, direction2);
} else {
int[][] is = getAroundAndOnBedOffsets(bedDirection, direction2);
Optional<Vec3d> optional = findWakeUpPosition(type, world, pos, is, true);
return optional.isPresent() ? optional : findWakeUpPosition(type, world, pos, is, false);
}
}

private static Optional<Vec3d> findWakeUpPosition(EntityType<?> type, CollisionView world, BlockPos pos, Direction bedDirection, Direction respawnDirection) {
int[][] is = getAroundBedOffsets(bedDirection, respawnDirection);
Optional<Vec3d> optional = findWakeUpPosition(type, world, pos, is, true);
if (optional.isPresent()) {
return optional;
} else {
BlockPos blockPos = pos.down();
Optional<Vec3d> optional2 = findWakeUpPosition(type, world, blockPos, is, true);
if (optional2.isPresent()) {
return optional2;
} else {
int[][] js = getOnBedOffsets(bedDirection);
Optional<Vec3d> optional3 = findWakeUpPosition(type, world, pos, js, true);
if (optional3.isPresent()) {
return optional3;
} else {
Optional<Vec3d> optional4 = findWakeUpPosition(type, world, pos, is, false);
if (optional4.isPresent()) {
return optional4;
} else {
Optional<Vec3d> optional5 = findWakeUpPosition(type, world, blockPos, is, false);
return optional5.isPresent() ? optional5 : findWakeUpPosition(type, world, pos, js, false);
}
}
}
}
}

private static Optional<Vec3d> findWakeUpPosition(EntityType<?> type, CollisionView world, BlockPos pos, int[][] possibleOffsets, boolean ignoreInvalidPos) {
BlockPos.Mutable mutable = new BlockPos.Mutable();

for (int[] is : possibleOffsets) {
mutable.set(pos.getX() + is[0], pos.getY(), pos.getZ() + is[1]);
Vec3d vec3d = Dismounting.findRespawnPos(type, world, mutable, ignoreInvalidPos);
if (vec3d != null) {
return Optional.of(vec3d);
}
}

return Optional.empty();
}

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

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

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

创建自定义床类

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

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

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

1
2
3
4
5
6
7
public class ModBedBlock extends BedBlock {

public ModBedBlock(DyeColor color, Settings settings) {
super(color, settings);
}

}

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

然后我们写个碰撞箱

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

@Override
public VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
return SHAPE;
}

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

另外,我们再重写getRenderType

1
2
3
4
5
6
7
8
@Override
public BlockRenderType getRenderType(BlockState state) {
if (state.get(PART) == BedPart.HEAD) {
return BlockRenderType.MODEL;
} else {
return BlockRenderType.INVISIBLE;
}
}

因为原版的床是两个部分,但我们的床是拿Blockbench制作的一个完整的模型,使用得隐藏其中一个部分,
不然两个区域就重叠了

注册方块

注册

1
2
public static final Block BED = register("bed",
new ModBedBlock(DyeColor.BLACK, AbstractBlock.Settings.create().strength(2.0f, 6.0f).nonOpaque()));

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

物品栏

不要忘记物品栏

1
entries.add(ModBlocks.BED);

数据文件

语言文件

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

模型文件

1
blockStateModelGenerator.registerNorthDefaultHorizontalRotation(ModBlocks.BED);

床的话,它是带有FACING参数的,所以这里我们使用registerNorthDefaultHorizontalRotation来生成方块状态文件

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

测试

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