本篇教程的视频
本篇教程的源代码
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:完整的一根(竖杆 + 两边横杆),只给物品栏用
第二份是最容易做错的
横杆必须南北向放,而且要放在方块靠北的那一侧

做成这样,游戏里往东、南、西转的时候,才转得出正确的结果
你要是把它做成一整条横穿或者放偏了,连起来会错位
借 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 里