本篇教程的视频

本篇教程的源代码

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