本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 理解「可连接方块」到底靠什么实现
  • 学会自己定义一个方块状态属性(枚举类型)
  • 学会重写 updateShape,让方块感知邻居
  • 学会用数据生成输出 属性 × 朝向 的全部变体

介绍

上一篇我们做了个挂墙上的时钟,它只有朝向,四个方向各转一下就行

这一期我们来进阶一点,制作一个沙发,它不仅有朝向,还可以进行连接

你把两个沙发挨着放,它们会自动拼成一个双人沙发,再放一个就是三人沙发

这期的实现思路

原版的栅栏有四个方向都能连,沙发不用那么夸张

这一期我们只做左右两个方向:一个沙发的左边或者右边有同样朝向的沙发,它就把自己换成「左半边」或者「右半边」的模型

所以沙发的方块状态有两个

一个是 facing,就是我们上一篇学的朝向

另一个是 type,表示它是单人、左半边、右半边还是中间那一截

type 是原版没有的,得我们自己定义,这就是这期的重点

动手写

创建方块类

1
2
3
4
5
public class SofaBlock extends Block {
public SofaBlock(Properties properties) {
super(properties);
}
}

定义 Type 枚举

在 SofaBlock 里写一个嵌套枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public enum Type implements StringRepresentable {
SINGLE("single"),
LEFT("left"),
RIGHT("right"),
MIDDLE("middle");

private final String name;

Type(String name) {
this.name = name;
}

@Override
public String getSerializedName() {
return this.name;
}
}

四个值分别是单个、左、右、中

这四个值决定了我们要准备四份模型:sofa、sofa_left、sofa_right、sofa_middle

建模的部分我跳过,你可以拿我仓库里面的模型

声明两个属性

1
2
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;
public static final EnumProperty<Type> TYPE = EnumProperty.create("type", Type.class);

TYPE 这个名字无所谓,但里面的字符串 type 就是方块状态文件里的键名,别乱改

默认状态和属性声明

1
2
3
4
5
6
7
8
9
10
11
public SofaBlock(Properties properties) {
super(properties);
this.registerDefaultState(this.stateDefinition.any()
.setValue(FACING, Direction.NORTH)
.setValue(TYPE, Type.SINGLE));
}

@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING, TYPE);
}

两个属性都要加进去,少一个就崩

四个常规方法

1
2
3
4
5
6
7
8
9
@Override
protected BlockState rotate(BlockState state, Rotation rotation) {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
}

@Override
protected BlockState mirror(BlockState state, Mirror mirror) {
return state.rotate(mirror.getRotation(state.getValue(FACING)));
}

这两个和上一篇一模一样

放置的时候也是先定朝向

1
2
3
4
5
6
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
BlockState state = this.defaultBlockState()
.setValue(FACING, context.getHorizontalDirection().getOpposite());
return this.getRelatedBlockState(state, context.getLevel(), context.getClickedPos(), state.getValue(FACING));
}

注意这里比上一篇多了一行

上一篇只定朝向就够了,而沙发放下去的时候,旁边可能已经有沙发了,它需要计算自己是左半边还是右半边或者中间

getRelatedBlockState这个方法是我们自定义的,后面再展开

updateShape

1
2
3
4
5
6
@Override
protected BlockState updateShape(BlockState state, LevelReader level, ScheduledTickAccess ticks, BlockPos pos,
Direction directionToNeighbour, BlockPos neighbourPos, BlockState neighbourState,
RandomSource random) {
return this.getRelatedBlockState(state, level, pos, state.getValue(FACING));
}

updateShape是用来更新方块的,当周围方块发生变化时,游戏会调用这个方法

判断左右

1
2
3
4
5
6
7
8
9
10
11
12
private BlockState getRelatedBlockState(BlockState state, LevelReader level, BlockPos pos, Direction direction) {
boolean left = this.isRelatedInDirection(level, pos, direction, true);
boolean right = this.isRelatedInDirection(level, pos, direction, false);
if (left && right) {
return state.setValue(TYPE, Type.MIDDLE);
} else if (left) {
return state.setValue(TYPE, Type.RIGHT);
} else if (right) {
return state.setValue(TYPE, Type.LEFT);
}
return state.setValue(TYPE, Type.SINGLE);
}

那么这个方法就是用来返回不同的方块状态,判断左右是否同样是沙发方块,决定其样式

左边和右边到底指哪边

1
2
3
4
private boolean isRelatedInDirection(LevelReader level, BlockPos pos, Direction direction, boolean counterClockwise) {
Direction rotate = counterClockwise ? direction.getCounterClockWise() : direction.getClockWise();
return this.isRelatedBlock(level, pos, rotate, direction);
}

getCounterClockWise 和 getClockWise 是 Direction 自带的方法,分别返回逆时针和顺时针转 90 度之后的方向

direction 是方块本身的朝向,逆时针转 90 度得到的就是「我左手边」那个方位,那么如果方块现在朝北的话,它的左边就是西,右边就是东

所以 counterClockwise = true 查左边,false 查右边

检查那个方块是不是沙发

1
2
3
4
5
6
7
private boolean isRelatedBlock(LevelReader level, BlockPos pos, Direction rotate, Direction direction) {
BlockState state = level.getBlockState(pos.relative(rotate));
if (state.getBlock() == this) {
return state.getValue(FACING) == direction;
}
return false;
}

这里实际上有两个条件:

  • 是否是方块本身,毕竟只有沙发之间才能连接
  • 是否朝向一致,两个朝向不同的沙发不能连在一起(当然,如果是那种无靠背的沙发倒是可以考虑连接,还有转角类型的)

碰撞箱

沙发是 14 像素高、12 像素深的长条,同样要按朝向分

1
2
public static final VoxelShape SHAPE_NS = Block.box(0, 0, 2, 16, 14, 14);
public static final VoxelShape SHAPE_EW = Block.box(2, 0, 0, 14, 14, 16);

这次只用两个形状就够了

同样重写getShape,按朝向返回对应的碰撞箱

1
2
3
4
5
6
7
@Override
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return switch (state.getValue(FACING)) {
case EAST, WEST -> SHAPE_EW;
default -> SHAPE_NS;
};
}

switch 里可以用逗号把两个情况并到一行,比写四个分支省事

注册

1
2
public static final Block SOFA = register("sofa", SofaBlock::new,
BlockBehaviour.Properties.of().strength(3.0f, 3.0f).noOcclusion(), true);

加物品栏

1
output.accept(ModBlocks.SOFA);

语言文件

1
2
translationBuilder.add(ModBlocks.SOFA, "Sofa");
translationBuilder.add(ModBlocks.SOFA, "沙发");

战利品表也别忘了

1
dropSelf(ModBlocks.SOFA);

数据生成

这次的数据生成更复杂了,因为原版并没有 type 这个属性,所以我们得自己写,而且我们还得加上 facing,因为沙发是有朝向的

所以这里的方法要叠两层:先按 TYPE 选模型,再按 FACING 套旋转,最后别忘了物品的生成

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void generateSofaBlock(BlockModelGenerators blockModelGenerators, Block block) {
blockModelGenerators.blockStateOutput.accept(
MultiVariantGenerator.dispatch(block)
.with(PropertyDispatch.initial(SofaBlock.TYPE)
.select(SofaBlock.Type.SINGLE, BlockModelGenerators.plainVariant(modLoc("block/sofa")))
.select(SofaBlock.Type.LEFT, BlockModelGenerators.plainVariant(modLoc("block/sofa_left")))
.select(SofaBlock.Type.RIGHT, BlockModelGenerators.plainVariant(modLoc("block/sofa_right")))
.select(SofaBlock.Type.MIDDLE, BlockModelGenerators.plainVariant(modLoc("block/sofa_middle"))))
.with(PropertyDispatch.modify(BlockStateProperties.HORIZONTAL_FACING)
.select(Direction.NORTH, BlockModelGenerators.NOP)
.select(Direction.EAST, BlockModelGenerators.Y_ROT_90)
.select(Direction.SOUTH, BlockModelGenerators.Y_ROT_180)
.select(Direction.WEST, BlockModelGenerators.Y_ROT_270))
);
blockModelGenerators.registerSimpleItemModel(block, modLoc("block/sofa"));
}

private Identifier modLoc(String path) {
return Identifier.fromNamespaceAndPath(TutorialMod.MOD_ID, path);
}

第一个 with 用的是 PropertyDispatch.initial(TYPE),四个枚举值各选一个模型

第二个 with 用的是 PropertyDispatch.modify(HORIZONTAL_FACING),给上面每一个结果再套旋转

生成结果

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
54
55
56
57
58
59
60
61
62
63
64
{
"variants": {
"facing=east,type=left": {
"model": "tutorial:block/sofa_left",
"y": 90
},
"facing=east,type=middle": {
"model": "tutorial:block/sofa_middle",
"y": 90
},
"facing=east,type=right": {
"model": "tutorial:block/sofa_right",
"y": 90
},
"facing=east,type=single": {
"model": "tutorial:block/sofa",
"y": 90
},
"facing=north,type=left": {
"model": "tutorial:block/sofa_left"
},
"facing=north,type=middle": {
"model": "tutorial:block/sofa_middle"
},
"facing=north,type=right": {
"model": "tutorial:block/sofa_right"
},
"facing=north,type=single": {
"model": "tutorial:block/sofa"
},
"facing=south,type=left": {
"model": "tutorial:block/sofa_left",
"y": 180
},
"facing=south,type=middle": {
"model": "tutorial:block/sofa_middle",
"y": 180
},
"facing=south,type=right": {
"model": "tutorial:block/sofa_right",
"y": 180
},
"facing=south,type=single": {
"model": "tutorial:block/sofa",
"y": 180
},
"facing=west,type=left": {
"model": "tutorial:block/sofa_left",
"y": 270
},
"facing=west,type=middle": {
"model": "tutorial:block/sofa_middle",
"y": 270
},
"facing=west,type=right": {
"model": "tutorial:block/sofa_right",
"y": 270
},
"facing=west,type=single": {
"model": "tutorial:block/sofa",
"y": 270
}
}
}

测试

跑完数据生成,进游戏进行测试

常见问题(大肥鱼总结版)

  • 启动崩,报 Property type is not defined:createBlockStateDefinition 里漏了 TYPE
  • 数据生成报 Values of property ... already defined:同一个属性在两个 with 里都写了
  • 沙发永远连不起来:updateShape 没写,或者签名写错(26.1 是 8 个参数,抄老的会编译不过)
  • 新放的沙发是单人的,敲一下旁边的才连上:getStateForPlacement 里没算 type
  • 左边右边反了:getCounterClockWise 和 getClockWise 用反了
  • 朝向不同的沙发也连起来了:isRelatedBlock 里少了朝向判断
  • 拆掉中间那个,剩下的还是半截模型:updateShape 只在 getRelatedBlockState 里算了自己,检查一下是不是把返回值丢掉了
  • 模型显示成单人沙发,但 F3 里 type=left:方块状态文件里 type 的模型选错了,检查 select 那一行
  • 物品栏里是紫黑块:registerSimpleItemModel 漏了
  • 沙发能穿过去或者挡住半个身位:getShape 没按朝向切