本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 认识 multipart 这种方块状态,搞清它和 variants 的分工
  • 自己写一个四向连接的栅栏方块类
  • 学会用 createFence 一行生成 multipart 方块状态
  • 知道原版栅栏为什么靠标签连、而我们这期为什么不用

介绍

这是方块状态小系列的最后一期

回头看一下我们走过的路

最早是朝向,一个 facing 属性,四个值对应四次旋转

然后是沙发,左右能不能连,type 分四种

再是柱子,同样的思路换成上下

这一期做栅栏,四个方向都能连

前三个我们用的都是 variants,也就是把状态一条条列出来

栅栏不行,这就是这期的重点

为什么栅栏不能用 variants

栅栏有四个方向属性 north、east、south、west,都是布尔值

四个布尔一组合是 16 种

更麻烦的是:每种状态的模型不是「一个模型」,而是「竖杆 + 若干横杆」拼起来的

variants 一条只能填一个模型,你没法在一条里写「竖杆 + 北边横杆 + 东边横杆」

真要硬写,就得给 16 种组合各准备一份完整模型,那是十几份模型文件

所以栅栏用的是另一种写法:multipart

它的思路是「往同一个方块上叠模型」:竖杆永远画,北边能连就再叠一个北边横杆,东边能连就再叠一个东边横杆

一句话概括这两兄弟的分工

  • variants:状态少、每种状态一份独立模型
  • multipart:状态多、模型是「基础部分 + 若干可选部分」的叠加

这期的取舍

原版栅栏的 FenceBlock 功能很全:连接判定、碰撞箱、拴绳、含水,全都有

但它连接的对象是「所有栅栏」,靠的是标签

我们这期的模型只有一个「半截横杆」,接不上原版栅栏的造型,也不需要拴绳和含水

所以这期照前面几期的路子,自己写一个方块类,只跟自己连

查看源代码

四个属性

FenceBlock 继承的是 CrossCollisionBlock,四个方向属性在那里

1
2
3
4
public static final BooleanProperty NORTH = PipeBlock.NORTH;
public static final BooleanProperty EAST = PipeBlock.EAST;
public static final BooleanProperty SOUTH = PipeBlock.SOUTH;
public static final BooleanProperty WEST = PipeBlock.WEST;

它们和前面几期用的那批是同一套,直接引用就行

构造函数里默认全给 false,另外还有一个 WATERLOGGED,含水特性

看原版的方块状态文件

原版橡木栅栏的 blockstates/oak_fence.json

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
{
"multipart": [
{
"apply": {
"model": "minecraft:block/oak_fence_post"
}
},
{
"apply": {
"model": "minecraft:block/oak_fence_side",
"uvlock": true
},
"when": {
"north": "true"
}
},
{
"apply": {
"model": "minecraft:block/oak_fence_side",
"uvlock": true,
"y": 90
},
"when": {
"east": "true"
}
}
]
}

(后面还有 south 和 west 两条,我省了)

when,意思是不管什么状态都画,这就是中间那根竖杆

when.north是true 的时候,叠一个横杆模型

后面同理,只是多了一句 "y": 90,因为横杆模型只做了北边那一份,东边是转出来的

原版怎么判断连接

1
2
3
4
5
6
7
8
9
10
11
@Override
public boolean connectsTo(final BlockState state, final boolean isSideSolid, final Direction direction) {
Block block = state.getBlock();
boolean sameFence = this.isSameFence(state);
boolean fenceGate = block instanceof FenceGateBlock && FenceGateBlock.connectsToDirection(state, direction);
return !isExceptionForConnection(state) && isSideSolid || sameFence || fenceGate;
}

private boolean isSameFence(final BlockState state) {
return state.is(BlockTags.FENCES) && state.is(BlockTags.WOODEN_FENCES) == this.defaultBlockState().is(BlockTags.WOODEN_FENCES);
}

原版的判断有三条:对面是完整的方块面、对面是同类栅栏、对面是朝向合适的栅栏门

其中「同类栅栏」靠的是 #minecraft:fences 这个标签

也就是说,只要你把自己的栅栏塞进这个标签,原版栅栏就会主动来连你

不过这期我们自己是判断,因为我们自定义的模型不太能和原版的栅栏很好得连接,所以就跳过了

动手写

ModFenceBlock

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ModFenceBlock extends Block {
public static final BooleanProperty NORTH = PipeBlock.NORTH;
public static final BooleanProperty EAST = PipeBlock.EAST;
public static final BooleanProperty SOUTH = PipeBlock.SOUTH;
public static final BooleanProperty WEST = PipeBlock.WEST;

public ModFenceBlock(Properties properties) {
super(properties);
this.registerDefaultState(this.stateDefinition.any()
.setValue(NORTH, false)
.setValue(EAST, false)
.setValue(SOUTH, false)
.setValue(WEST, false));
}
}

四个属性默认全是 false,也就是一根光杆

声明属性

1
2
3
4
@Override
protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
builder.add(NORTH, EAST, SOUTH, WEST);
}

四个都要加进去

形状

之前的教程没给栅栏写碰撞箱,用默认的满方块形状,结果就是「环境光遮蔽」出问题:栅栏周围一圈看着发黑,像是插在一个看不见的方块里

所以这里我们就来设置一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final Function<BlockState, VoxelShape> shapes;

private Function<BlockState, VoxelShape> makeShapes() {
VoxelShape post = Block.column(2.0, 0.0, 16.0);
Map<Direction, VoxelShape> arms = Shapes.rotateHorizontal(Block.boxZ(2.0, 0.0, 16.0, 0.0, 8.0));
return this.getShapeForEachState(state -> {
VoxelShape shape = post;
for (Map.Entry<Direction, BooleanProperty> entry : PROPERTY_BY_DIRECTION.entrySet()) {
if (state.getValue(entry.getValue())) {
shape = Shapes.or(shape, arms.get(entry.getKey()));
}
}
return shape;
});
}

不过这里的写法有点意思,与我们之前直接指定不太一样,这里是「先算出 16 种状态的形状,缓存起来,运行时直接查表」

PROPERTY_BY_DIRECTION 是我们自己建的一张对照表

1
2
3
4
5
6
public static final Map<Direction, BooleanProperty> PROPERTY_BY_DIRECTION = Map.of(
Direction.NORTH, NORTH,
Direction.EAST, EAST,
Direction.SOUTH, SOUTH,
Direction.WEST, WEST
);

有了它,后面判断连接、旋转都能写成一个循环,不用四个方向抄四遍

最后把形状接上

1
2
3
4
5
6
7
8
9
@Override
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return this.shapes.apply(state);
}

@Override
protected VoxelShape getCollisionShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return this.shapes.apply(state);
}

判断连接

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@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);
}

private BlockState getRelatedBlockState(BlockState state, LevelReader level, BlockPos pos) {
return state
.setValue(NORTH, level.getBlockState(pos.north()).is(this))
.setValue(EAST, level.getBlockState(pos.east()).is(this))
.setValue(SOUTH, level.getBlockState(pos.south()).is(this))
.setValue(WEST, level.getBlockState(pos.west()).is(this));
}

和柱子那期一个套路,只不过要判断四次

pos.north()、pos.east() 这些是 BlockPos 自带的便捷方法,比手算偏移好读

is(this) 就是「对面也是我们这个栅栏」

如果哪天你想让它也能连别的方块,把 is(this) 换成 tag 判断就行

放置时也算一遍

1
2
3
4
@Override
public BlockState getStateForPlacement(BlockPlaceContext context) {
return this.getRelatedBlockState(this.defaultBlockState(), context.getLevel(), context.getClickedPos());
}

旋转和镜像

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Override
protected BlockState rotate(BlockState state, Rotation rotation) {
BlockState rotated = state;
for (Map.Entry<Direction, BooleanProperty> entry : PROPERTY_BY_DIRECTION.entrySet()) {
BooleanProperty source = PROPERTY_BY_DIRECTION.get(rotation.rotate(entry.getKey()));
rotated = rotated.setValue(entry.getValue(), state.getValue(source));
}
return rotated;
}

@Override
protected BlockState mirror(BlockState state, Mirror mirror) {
return switch (mirror) {
case LEFT_RIGHT -> state.setValue(NORTH, state.getValue(SOUTH)).setValue(SOUTH, state.getValue(NORTH));
case FRONT_BACK -> state.setValue(EAST, state.getValue(WEST)).setValue(WEST, state.getValue(EAST));
default -> state;
};
}

这两个是给结构方块用的,不写的话你用结构方块转房子,栅栏的横杆方向不会跟着转

注册

1
2
public static final Block FENCE = register("fence", ModFenceBlock::new,
BlockBehaviour.Properties.of().strength(2.0f, 3.0f).noOcclusion(), true);

noOcclusion 照旧

物品栏、语言、战利品

1
2
3
4
5
6
output.accept(ModBlocks.FENCE);

translationBuilder.add(ModBlocks.FENCE, "Fence");
translationBuilder.add(ModBlocks.FENCE, "栅栏");

dropSelf(ModBlocks.FENCE);

数据生成

三份模型

栅栏要三份模型

block/fence_post:中间那根竖杆,光秃秃的一根

block/fence_side:横杆,注意只做北边那一半

block/fence:完整的一根(竖杆 + 两边横杆),只给物品栏用

第二份是最容易做错的

横杆必须南北向放,而且要放在方块靠北的那一侧

1

做成这样,游戏里往东、南、西转的时候,才转得出正确的结果

你要是把它做成一整条横穿或者放偏了,连起来会错位

借 createFence 看看它怎么写的

原版数据生成里有个现成的 createFence,三个参数:方块、竖杆模型、横杆模型

它内部是这样的

1
2
3
4
5
6
7
8
public static BlockModelDefinitionGenerator createFence(final Block block, final MultiVariant post, final MultiVariant side) {
return MultiPartGenerator.multiPart(block)
.with(post)
.with(condition().term(BlockStateProperties.NORTH, true), side.with(UV_LOCK))
.with(condition().term(BlockStateProperties.EAST, true), side.with(Y_ROT_90).with(UV_LOCK))
.with(condition().term(BlockStateProperties.SOUTH, true), side.with(Y_ROT_180).with(UV_LOCK))
.with(condition().term(BlockStateProperties.WEST, true), side.with(Y_ROT_270).with(UV_LOCK));
}

MultiPartGenerator.multiPart 就是 multipart 的构造器

.with(post) 加一个无条件的基础部分

.with(condition().term(属性, true), 模型) 就是加一条「当某个属性为 true 时叠这个模型」

四个方向各一条,转 90、180、270

这段代码看懂了,multipart 就会写了

但它我们只能「借来看」,不能直接调,原因就是每一条后面都挂了 UV_LOCK

为什么不能用 createFence

UV_LOCK 对应 json 里的 "uvlock": true,作用是「模型旋转的时候,把贴图的方向掰回去,不让纹理跟着转」

原版栅栏需要这个,因为它的横杆贴图是一张竖木纹,转 90 度之后木纹就横过来了,看着别扭

我们的情况相反

模型是自己搭的,贴图是照着模型画的,UV 的位置、方向全都安排好了

一旦锁上 UV,游戏在旋转模型的时候会强行把纹理方向掰回原样,结果就是贴图和模型对不上:接缝处的花纹错位、描边跑到反面去

所以自定义模型要把这一项去掉

好在 createFence 只是把里面这段拼装过程包了一下,我们完全可以照抄它,只是不带 UV_LOCK

自己拼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private void generateFenceBlock(BlockModelGenerators blockModelGenerators, Block block) {
MultiVariant post = BlockModelGenerators.plainVariant(modLoc("block/fence_post"));
MultiVariant side = BlockModelGenerators.plainVariant(modLoc("block/fence_side"));

blockModelGenerators.blockStateOutput.accept(
MultiPartGenerator.multiPart(block)
.with(post)
.with(BlockModelGenerators.condition().term(BlockStateProperties.NORTH, true), side)
.with(BlockModelGenerators.condition().term(BlockStateProperties.EAST, true), side.with(BlockModelGenerators.Y_ROT_90))
.with(BlockModelGenerators.condition().term(BlockStateProperties.SOUTH, true), side.with(BlockModelGenerators.Y_ROT_180))
.with(BlockModelGenerators.condition().term(BlockStateProperties.WEST, true), side.with(BlockModelGenerators.Y_ROT_270))
);
blockModelGenerators.registerSimpleItemModel(block, modLoc("block/fence"));
}

和原版那段一比,就是把每个 .with(UV_LOCK) 删了

plainVariant 把模型路径包成变体,Y_ROT_90 这些是原版给的旋转器,condition() 是公开的静态方法,都能直接点出来

物品模型那行指向完整的那份 block/fence

MultiVariant、MultiPartGenerator 这两个类记得导包,都在 net.minecraft.client.data.models 下面

生成的方块状态

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

系列收尾

和原版橡木栅栏那段比,唯一的区别就是每条后面没有 uvlock

到这里,方块状态这套东西基本讲完了

  • facing:一个属性,管朝向
  • 自定义枚举:type 那种,管形态
  • bool 组合:栅栏这种多方向连接
  • multipart:属性多、模型可叠加的时候用

判断标准其实就一句话:你的方块在不同状态下,是「换模型」还是「叠模型」

换模型用 variants,叠模型用 multipart

以后你会碰到同时需要两者的方块,那就得看原版怎么写的了,比如红石粉、楼梯这些

测试

跑完数据生成我们就可以启动游戏进行测试了

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

  • 两根栅栏放一起连不起来:updateShape 没写,或者写成了 pos.north() 但漏了 .is(this)
  • 新放的栅栏不长横杆,敲一下才出来:getStateForPlacement 里没算
  • 横杆贴图和模型对不上、接缝花纹错位:用了 createFence,它给每条都挂了 UV_LOCK,自定义模型得照上面那样自己拼,把 UV_LOCK 去掉
  • 连起来位置错开:横杆模型没放在北边那一半,而是放在了正中间
  • 栅栏周围一圈发黑:形状还是默认的满方块,getShape 里没换成拼出来的形状
  • 物品栏里是一根光杆:物品模型指向了 fence_post,应该指向完整的 fence
  • 数据生成报属性不属于这个方块:createFence 要求方块有 NORTH、EAST、SOUTH、WEST 四个属性,而且得是 BlockStateProperties 里那几个
  • 结构方块转动后横杆方向不变:rotate / mirror 没写
  • 想让它跟原版栅栏连:把 is(this) 换成标签判断,同时得把方块加进 #minecraft:fences,但模型接缝得自己处理
  • 拴不上动物、水里不流水:这是我们故意去掉的,原版那套在 FenceBlock 里