本篇教程的视频

本篇教程的源代码

Github地址:TutorialMod-Facing-1.21

介绍

这里我们将用两篇教程来讲解方块状态,这一篇讲解方块朝向,下一篇讲解可连接方块的方块状态

简单介绍一下,方块状态是方块独有的一种属性,它可以用来表示方块的不同状态,方块状态的种类相当多,有穷举变种型(variants,名字是我自己定义的),比如以下的acacia_button

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
{
"variants": {
"face=ceiling,facing=east,powered=false": {
"model": "minecraft:block/acacia_button",
"x": 180,
"y": 270
},
"face=ceiling,facing=east,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"x": 180,
"y": 270
},
"face=ceiling,facing=north,powered=false": {
"model": "minecraft:block/acacia_button",
"x": 180,
"y": 180
},
"face=ceiling,facing=north,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"x": 180,
"y": 180
},
"face=ceiling,facing=south,powered=false": {
"model": "minecraft:block/acacia_button",
"x": 180
},
"face=ceiling,facing=south,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"x": 180
},
"face=ceiling,facing=west,powered=false": {
"model": "minecraft:block/acacia_button",
"x": 180,
"y": 90
},
"face=ceiling,facing=west,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"x": 180,
"y": 90
},
"face=floor,facing=east,powered=false": {
"model": "minecraft:block/acacia_button",
"y": 90
},
"face=floor,facing=east,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"y": 90
},
"face=floor,facing=north,powered=false": {
"model": "minecraft:block/acacia_button"
},
"face=floor,facing=north,powered=true": {
"model": "minecraft:block/acacia_button_pressed"
},
"face=floor,facing=south,powered=false": {
"model": "minecraft:block/acacia_button",
"y": 180
},
"face=floor,facing=south,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"y": 180
},
"face=floor,facing=west,powered=false": {
"model": "minecraft:block/acacia_button",
"y": 270
},
"face=floor,facing=west,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"y": 270
},
"face=wall,facing=east,powered=false": {
"model": "minecraft:block/acacia_button",
"uvlock": true,
"x": 90,
"y": 90
},
"face=wall,facing=east,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"uvlock": true,
"x": 90,
"y": 90
},
"face=wall,facing=north,powered=false": {
"model": "minecraft:block/acacia_button",
"uvlock": true,
"x": 90
},
"face=wall,facing=north,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"uvlock": true,
"x": 90
},
"face=wall,facing=south,powered=false": {
"model": "minecraft:block/acacia_button",
"uvlock": true,
"x": 90,
"y": 180
},
"face=wall,facing=south,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"uvlock": true,
"x": 90,
"y": 180
},
"face=wall,facing=west,powered=false": {
"model": "minecraft:block/acacia_button",
"uvlock": true,
"x": 90,
"y": 270
},
"face=wall,facing=west,powered=true": {
"model": "minecraft:block/acacia_button_pressed",
"uvlock": true,
"x": 90,
"y": 270
}
}
}

这里的facefacingpowered都是方块的属性

显然易见,variants类型的方块状态是根据方块的属性来穷举出所有状态的方块模型,当然它的缺点也很明显,就是当属性过多时,会导致方块状态文件过于庞大,不利于维护

另外,也有组合型(multipart),比如acacia_fence

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
{
"multipart": [
{
"apply": {
"model": "minecraft:block/acacia_fence_post"
}
},
{
"apply": {
"model": "minecraft:block/acacia_fence_side",
"uvlock": true
},
"when": {
"north": "true"
}
},
{
"apply": {
"model": "minecraft:block/acacia_fence_side",
"uvlock": true,
"y": 90
},
"when": {
"east": "true"
}
},
{
"apply": {
"model": "minecraft:block/acacia_fence_side",
"uvlock": true,
"y": 180
},
"when": {
"south": "true"
}
},
{
"apply": {
"model": "minecraft:block/acacia_fence_side",
"uvlock": true,
"y": 270
},
"when": {
"west": "true"
}
}
]
}

这里的multipart类型的方块状态是根据方块的属性来选择应用哪个方块模型,比如这里的栅栏,根据四个面是否可连接来决定应用哪个方块模型(如果说要用穷举法,那得写多长的文件啊)

不过,其实很多方块的方块状态还是用variants类型的,而里面的属性也是相当多的,这里我们就不展开讲解了,有兴趣的可以去看看原版的方块状态文件

我们这里要讲解的参数是facing,上面的acacia_button的其中一个属性就是facing,它表示按钮的朝向,不同的朝向对应不同的模型

原版的楼梯等都有facing属性,我们可以去看看它们的源代码,看看它们的这个属性是怎么定义,如何使用的

查看源代码

这里我们以楼梯为例,看看它的facing属性是怎么定义的

StairsBlock类

1
public static final DirectionProperty FACING = HorizontalFacingBlock.FACING;

StairsBlock这个类中,我们可以看到它定义了一个DirectionProperty类型的属性FACING,这个属性是HorizontalFacingBlock.FACING,我们再看看HorizontalFacingBlock这个类

HorizontalFacingBlock类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class HorizontalFacingBlock extends Block {
public static final DirectionProperty FACING = Properties.HORIZONTAL_FACING;

protected HorizontalFacingBlock(AbstractBlock.Settings settings) {
super(settings);
}

@Override
protected abstract MapCodec<? extends HorizontalFacingBlock> getCodec();

@Override
protected BlockState rotate(BlockState state, BlockRotation rotation) {
return state.with(FACING, rotation.rotate(state.get(FACING)));
}

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

HorizontalFacingBlock这个类中,我们可以看到它定义了一个DirectionProperty类型的属性FACING,这个属性是原版定义好的,我们可以直接使用

Properties类

1
public static final DirectionProperty HORIZONTAL_FACING = DirectionProperty.of("facing", Direction.Type.HORIZONTAL);

我们还可以在追溯至Properties这个类,我们可以看到它定义了一个DirectionProperty.of("facing", Direction.Type.HORIZONTAL)

这个字段注册的id就是facing,也就是对应我们方块状态文件中的facing属性

而在Properties类中还有很多其他的属性,你可以根据需要去查看

实现方块朝向

这里我们就开始来实现方块朝向,我们以前一篇教程的SimpleBlock为例,我们来实现一个朝向的方块

方块类

1
2
3
4
5
6
7
public class SimpleBlock extends Block {

public SimpleBlock(Settings settings) {
super(settings);
}

}

我们先不管其中的碰撞箱,我们先来实现方块朝向

引入FACING属性

1
public static final DirectionProperty FACING = Properties.HORIZONTAL_FACING;

我们在SimpleBlock这个类中定义一个DirectionProperty类型的属性FACING,直接引用原版的Properties.HORIZONTAL_FACING即可

当然,我们可以让我们的方块类继承HorizontalFacingBlock,这样就不用自己定义FACING属性了,同时也不用再重写那些方法了,但是得写个编解码器

重写rotatemirrorgetPlacementState方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected BlockState rotate(BlockState state, BlockRotation rotation) {
return state.with(FACING, rotation.rotate(state.get(FACING)));
}

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

@Nullable
@Override
public BlockState getPlacementState(ItemPlacementContext ctx) {
return this.getDefaultState().with(FACING, ctx.getHorizontalPlayerFacing().getOpposite());
}

这里我们重写了rotatemirrorgetPlacementState方法,这三个方法是用来处理方块朝向的

这里的写法和原版的HorizontalFacingBlock类中的不太一样,我们多写了getPlacementState方法,你可以自己来测试一下有什么不同

重写appendProperties方法

1
2
3
4
@Override
protected void appendProperties(StateManager.Builder<Block, BlockState> builder) {
builder.add(FACING);
}

以后写方块状态的属性时候记得写这个,这是非常重要的点!!!

不然后面会报错、崩溃

这里我们添加了FACING属性,后面我们在游戏中按F3查看方块状态时就会看到facing属性了

碰撞箱

接下来我们结合方块的朝向,利用BlockBench来生成碰撞箱

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
public static final VoxelShape SHAPE_N = Stream.of(
Block.createCuboidShape(0, 0, 0, 16, 2, 16),
Block.createCuboidShape(0, 14, 0, 16, 16, 16),
Block.createCuboidShape(0, 2, 2, 16, 14, 16)
).reduce((v1, v2) -> VoxelShapes.combineAndSimplify(v1, v2, BooleanBiFunction.OR)).get();

public static final VoxelShape SHAPE_W = Stream.of(
Block.createCuboidShape(0, 0, 0, 16, 2, 16),
Block.createCuboidShape(0, 14, 0, 16, 16, 16),
Block.createCuboidShape(2, 2, 0, 16, 14, 16)
).reduce((v1, v2) -> VoxelShapes.combineAndSimplify(v1, v2, BooleanBiFunction.OR)).get();

public static final VoxelShape SHAPE_S = Stream.of(
Block.createCuboidShape(0, 0, 0, 16, 2, 16),
Block.createCuboidShape(0, 14, 0, 16, 16, 16),
Block.createCuboidShape(0, 2, 0, 16, 14, 14)
).reduce((v1, v2) -> VoxelShapes.combineAndSimplify(v1, v2, BooleanBiFunction.OR)).get();

public static final VoxelShape SHAPE_E = Stream.of(
Block.createCuboidShape(0, 0, 0, 16, 2, 16),
Block.createCuboidShape(0, 14, 0, 16, 16, 16),
Block.createCuboidShape(0, 2, 0, 14, 14, 16)
).reduce((v1, v2) -> VoxelShapes.combineAndSimplify(v1, v2, BooleanBiFunction.OR)).get();


@Override
protected VoxelShape getOutlineShape(BlockState state, BlockView world, BlockPos pos, ShapeContext context) {
return switch (state.get(FACING)) {
case WEST -> SHAPE_W;
case SOUTH -> SHAPE_S;
case EAST -> SHAPE_E;
default -> SHAPE_N;
};
}

东西南北,一共四个方向,我们分别定义了四个碰撞箱,然后根据方块的朝向来返回对应的碰撞箱

在使用BlockBench生成碰撞箱时,前面说过,我们要将所有的体块放在一个叫VoxelShapes的组下才能导出,名字不能错,大小写也是

然后不同方向的碰撞箱,可以先选中所有体块,然后将枢轴点设置到xz平面的中心,高度轴y可以任意,然后我们选中所有体块,一次旋转90度(建议逆时针,这样就是按照上面的顺序来的),再导出

最后就可以得到我们上面那一堆了

修改构造函数

1
2
3
4
public SimpleBlock(Settings settings) {
super(settings);
this.setDefaultState(this.getStateManager().getDefaultState().with(FACING, Direction.NORTH));
}

这里我们在构造函数中设置了默认的方块朝向为NORTH

注册方块

前面我们已经注册过了,这里就不再赘述了(当然如果你自己又写了一个的话,记得注册,还有物品栏)

数据文件

语言文件我们就不写了,和前面一样的

模型文件

我们方块的模型文件还是之前的,但是方块状态文件得改

1
blockStateModelGenerator.registerNorthDefaultHorizontalRotation(ModBlocks.SIMPLE_BLOCK);

这里我们用registerNorthDefaultHorizontalRotation方法注册我们的方块,默认面向是NORTH,这样和我们在方块构造函数中设置的默认朝向一致,并且这里会生成带有facing属性的方块状态文件

而后,我们就可以在游戏中测试我们的方块了