本篇教程的视频
本篇教程的源代码
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 没按朝向切