本篇教程的视频
本篇教程的源代码
GitHub地址:
本篇教程目标
- 复习
updateShape 的用法,把判定方向从「左右」换成「上下」
- 学会让碰撞箱跟着
TYPE 变
- 学会只用一层
with 生成方块状态
介绍
沙发那期我们做了个能左右拼的家具,这一期做个能上下接的柱子
它俩的思路一模一样,都是「自定义一个枚举属性,然后在 updateShape 里看邻居」
区别只有一处:沙发看的是左右,柱子看的是上下
所以源码这部分我就不重复了,直接上手写
柱子的四个形态
single:孤零零一根,底下有底座、上面有柱头
bottom:最底下那一段,只有底座
middle:中间那些,只有柱身
top:最顶上那一段,只有柱头
四种形态对应四份模型,建模的时候注意柱身和底座柱头的尺寸要对得上,不然接起来会错位
动手写
ModPillarBlock
1 2 3 4 5 6 7 8
| public class ModPillarBlock extends Block { public static final EnumProperty<Type> TYPE = EnumProperty.create("type", Type.class);
public ModPillarBlock(Properties properties) { super(properties); this.registerDefaultState(this.stateDefinition.any().setValue(TYPE, Type.SINGLE)); } }
|
EnumProperty.create 两个参数:属性名和枚举类
这里需要修正一下之前的说法,这里的type和沙发那个重名了也没关系
方块属性是挂在各自的方块状态定义里的,不同方块用同名的属性互不影响,游戏找属性是「先找到方块,再按名字找」
NeoForge 那期的教程把它改成了 type2,其实不用
TYPE 枚举
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public enum Type implements StringRepresentable { SINGLE("single"), BOTTOM("bottom"), MIDDLE("middle"), TOP("top");
private final String name;
Type(String name) { this.name = name; }
@Override public String getSerializedName() { return this.name; } }
|
和沙发那个 Type 长得一模一样,只是值换成了上中下
声明属性
1 2 3 4
| @Override protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) { builder.add(TYPE); }
|
老规矩,不写这一步启动就崩
判断上下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @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) { boolean above = level.getBlockState(pos.above()).is(this); boolean below = level.getBlockState(pos.below()).is(this); if (above && below) { return state.setValue(TYPE, Type.MIDDLE); } else if (above) { return state.setValue(TYPE, Type.BOTTOM); } else if (below) { return state.setValue(TYPE, Type.TOP); } return state.setValue(TYPE, Type.SINGLE); }
|
四个判断,和沙发那个一样,只是把「左右有没有沙发」换成了「上下有没有柱子」
写的时候注意别把above和below用反了
放置时也算一遍
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
| private static final VoxelShape SHAFT = Block.box(4, 0, 4, 12, 16, 12); private static final VoxelShape BASE = Block.box(2, 0, 2, 14, 2, 14); private static final VoxelShape CAP = Block.box(2, 14, 2, 14, 16, 14);
public static final VoxelShape SHAPE_SINGLE = Shapes.or(SHAFT, BASE, CAP); public static final VoxelShape SHAPE_BOTTOM = Shapes.or(SHAFT, BASE); public static final VoxelShape SHAPE_TOP = Shapes.or(SHAFT, CAP); public static final VoxelShape SHAPE_MIDDLE = SHAFT;
|
Shapes.or 就是把几个形状并起来,上一篇沙发的碰撞箱只用一个 Block.box,这里因为有两块外扩的,得拼一下
然后按 TYPE 返回
1 2 3 4 5 6 7 8 9
| @Override protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) { return switch (state.getValue(TYPE)) { case BOTTOM -> SHAPE_BOTTOM; case MIDDLE -> SHAPE_MIDDLE; case TOP -> SHAPE_TOP; default -> SHAPE_SINGLE; }; }
|
沙发那期的 switch 切的是 FACING,这次切的是 TYPE
也就是说,方块状态的任何一个属性都可以拿来决定碰撞箱,取决于你哪个属性会影响外形
注册
1 2
| public static final Block PILLAR = register("pillar", ModPillarBlock::new, BlockBehaviour.Properties.of().strength(2.0f, 2.0f).noOcclusion(), true);
|
照例加物品栏、语言、战利品
1 2 3 4 5 6
| output.accept(ModBlocks.PILLAR);
translationBuilder.add(ModBlocks.PILLAR, "Pillar"); translationBuilder.add(ModBlocks.PILLAR, "柱子");
dropSelf(ModBlocks.PILLAR);
|
数据生成
方块状态
柱子只有 TYPE 一个属性,没有朝向,所以只需要一层 with
1 2 3 4 5 6 7 8 9 10 11
| private void generatePillarBlock(BlockModelGenerators blockModelGenerators, Block block) { blockModelGenerators.blockStateOutput.accept( MultiVariantGenerator.dispatch(block) .with(PropertyDispatch.initial(ModPillarBlock.TYPE) .select(ModPillarBlock.Type.SINGLE, BlockModelGenerators.plainVariant(modLoc("block/pillar"))) .select(ModPillarBlock.Type.BOTTOM, BlockModelGenerators.plainVariant(modLoc("block/pillar_bottom"))) .select(ModPillarBlock.Type.MIDDLE, BlockModelGenerators.plainVariant(modLoc("block/pillar_middle"))) .select(ModPillarBlock.Type.TOP, BlockModelGenerators.plainVariant(modLoc("block/pillar_top")))) ); blockModelGenerators.registerSimpleItemModel(block, modLoc("block/pillar")); }
|
测试
跑完数据生成我们就可以启动游戏进行测试了
常见问题(大肥鱼总结版)
- 启动崩,报属性没注册:
createBlockStateDefinition 里漏了 TYPE
- 柱子永远是
single:updateShape 没写,或者写成了 level.getBlockState(pos.above()) 但忘了 .is(this)
- 上下用反了:上面那根判断成
TOP 了,检查一下 above / below 两个布尔值对应的分支
- 新放的柱子不接上去,敲一下才变:
getStateForPlacement 里没算 TYPE
- 拆掉中间一根,上面的柱子还是
middle:updateShape 里没有返回值,或者写成了只在客户端改
- 数据生成报
Values of property type already defined:TYPE 在两个 with 里都写了
- 属性名和沙发重名会不会冲突:不会,不同方块的方块状态是各自定义的
- 底座那一圈能穿过去:碰撞箱只写了柱身,
Shapes.or 里漏了 BASE / CAP
- 物品栏里显示的是半截柱子:物品模型指向了
pillar_bottom 之类的,改成 pillar