本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 搞懂 facing 这个方块状态到底是什么
  • 学会给方块加 HORIZONTAL_FACING 属性
  • 学会 getStateForPlacement、rotate、mirror、createBlockStateDefinition 这四个方法
  • 学会用 Blockbench 导出四个方向的碰撞箱
  • 学会用数据生成一次输出四个朝向的方块状态

介绍

上一篇我们添加了一个Blockbench的模型,但它是「死」的:放下去永远朝同一个方向

这一期我们做一个真正有朝向的方块,一个挂墙上的橙色时钟

从这期开始进入方块状态小系列,这篇讲朝向,下一篇讲可连接方块

什么叫朝向

游戏里除了六个面材质一样的方块,大部分都是有朝向的

比如熔炉,它开口的那一面,我们姑且叫特征面

你把熔炉放下去的时候,不管你面朝哪边(上下除外),特征面永远朝着你

楼梯、讲台、床、门,都是这个逻辑

方块朝向在底层就是一个叫 facing 的方块状态

方块状态是什么

方块状态是方块独有的一层属性,用来描述同一个方块的不同形态

我们在代码里给方块声明了哪些属性,游戏就会把所有组合穷举出来

而方块状态文件就是一张「状态 → 模型」的对照表,游戏照着它来决定这个方块该用什么模型、转多少度

两种写法

穷举型,variants,一种状态一条。原版的熔炉就是这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"variants": {
"facing=east,lit=false": {
"model": "minecraft:block/furnace",
"y": 90
},
"facing=north,lit=false": {
"model": "minecraft:block/furnace"
},
"facing=south,lit=false": {
"model": "minecraft:block/furnace",
"y": 180
},
"facing=west,lit=false": {
"model": "minecraft:block/furnace",
"y": 270
}
}
}

lit=true省略了,本质上是一样的

组合型,multipart,按条件往方块上叠模型,栅栏就是这种

穷举型的缺点很明显,属性一多就爆炸,原版按钮 face、facing、powered 三个属性一组合就是 24 条

我们这期只有 facing 一个,四条就够,用 variants 最省事

查看源代码

FACING 属性

先看熔炉,AbstractFurnaceBlock 里

1
public static final EnumProperty<Direction> FACING = HorizontalDirectionalBlock.FACING;

它引用的是 HorizontalDirectionalBlock.FACING,而那边又是

1
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;

绕了两层,本质还是 BlockStateProperties 里的那个

HORIZONTAL_FACING 的定义

1
public static final EnumProperty<Direction> HORIZONTAL_FACING = EnumProperty.create("facing", Direction.class, Direction.Plane.HORIZONTAL);

三个参数看一遍

facing 是方块状态文件里那个键名,必须和它一致,不然游戏找不到这个属性

Direction.class 说明这个属性存的是方向枚举

Direction.Plane.HORIZONTAL 限定只取水平四个方向:NORTH、EAST、SOUTH、WEST

同一层里还有个 FACING,那个多带了 UP 和 DOWN,原版只有木桶在用

构造函数里初始化

1
2
3
4
protected AbstractFurnaceBlock(final BlockBehaviour.Properties properties) {
super(properties);
this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH).setValue(LIT, false));
}

registerDefaultState 把默认朝向定成北,LIT 那个我们以后讲发光再说

四个方法

放置时决定朝向

1
2
3
4
@Override
public BlockState getStateForPlacement(final BlockPlaceContext context) {
return this.defaultBlockState().setValue(FACING, context.getHorizontalDirection().getOpposite());
}

getHorizontalDirection() 是玩家面朝的方向,getOpposite() 取反,所以正面才朝着玩家

旋转和镜像

1
2
3
4
5
6
7
8
9
@Override
protected BlockState rotate(final BlockState state, final Rotation rotation) {
return state.setValue(FACING, rotation.rotate(state.getValue(FACING)));
}

@Override
protected BlockState mirror(final BlockState state, final Mirror mirror) {
return state.rotate(mirror.getRotation(state.getValue(FACING)));
}

这两个不是给玩家用的,是给结构方块、/rotate 命令用的

不写的话,你用结构方块把房子转 90 度,别的方块都跟着转,我们的方块还杵在原地

最后把属性注册进状态表

1
2
3
4
@Override
protected void createBlockStateDefinition(final StateDefinition.Builder<Block, BlockState> builder) {
builder.add(FACING, LIT);
}

这个漏了直接崩,报的是属性没注册

动手写

方块类

新建 OrangeClock,继承 Block

1
2
3
4
5
6
7
8
public class OrangeClock extends Block {
public static final EnumProperty<Direction> FACING = BlockStateProperties.HORIZONTAL_FACING;

public OrangeClock(Properties properties) {
super(properties);
this.registerDefaultState(this.stateDefinition.any().setValue(FACING, Direction.NORTH));
}
}

原版的 HorizontalDirectionalBlock 其实也可以用,但它是抽象类,子类还得写一个 codec,为了省事我们直接继承 Block,自己把属性加上

碰撞箱

这一步比上一篇麻烦,因为朝四个方向,碰撞箱也得有四份

装插件

Blockbench 本身导不出 VoxelShape,得装个插件

在 文件 -> 插件 里搜 Mod Utils,装上

1

装好之后,文件 -> 导出 里就会多出一项 Export VoxelShape(1.14+ Modded Minecraft)

整理体块

这个插件有个前提:所有体块必须放在一个叫 VoxelShapes 的组里,名字一个字母都不能错,大小写也要对

2

整理好之后点导出,会弹一个窗口让你选映射

3

这里要选你工程用的那套:我们这个 26.1 模板是 Mojang 官方映射,就选 Mojang;写 Yarn 的工程选 Yarn(上图是 1.21.1 那期的截图,所以显示的是 Yarn)

斜着的体块会跑掉

导出北面这份之后,还得有东西南三份

正常想法是把整个模型转 90 度再导一次,对吧?

在26.1之前的版本我会说不行

因为在这之前,Java版的方块模型只支持单轴旋转,你把一个带有斜着体块的模型整体旋转,那些斜着的体块就会跑偏,导出来的碰撞箱跟模型对不上

4

当然,现在的版本你大可以直接旋转然后导出碰撞箱,但是我还是要讲讲曾经的方法

也就是别管原来的体块了,直接拉一个能盖住整个时钟的大体块,把其他体块全删掉(或者挪走),拿它去导出四个方向的碰撞箱

5

旋转的时候记得把枢轴点调到方块正中心,也就是把 X 和 Z 都改成 8,Y 不用管

这种方法简单粗暴,也算是一种优化的手段,如果你制作了一些复杂的模型,实际上也只需要拉一个能覆盖整个模型的体块当碰撞箱就行了,因为本来模型里面犄角旮旯的地方玩家也进不去

四个碰撞箱

1
2
3
4
public static final VoxelShape SHAPE_N = Block.box(3, 3, 15, 13, 13, 16);
public static final VoxelShape SHAPE_E = Block.box(0, 3, 3, 1, 13, 13);
public static final VoxelShape SHAPE_S = Block.box(3, 3, 0, 13, 13, 1);
public static final VoxelShape SHAPE_W = Block.box(15, 3, 3, 16, 13, 13);

按朝向返回碰撞箱

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(FACING)) {
case EAST -> SHAPE_E;
case SOUTH -> SHAPE_S;
case WEST -> SHAPE_W;
default -> SHAPE_N;
};
}

Direction 一共六个值,UP 和 DOWN 在这个方块上不可能出现,所以用 default 兜住北

注册方块

1
2
public static final Block ORANGE_CLOCK = register("orange_clock", OrangeClock::new,
BlockBehaviour.Properties.of().strength(1.0f, 3.0f).noOcclusion(), true);

加进物品栏

1
output.accept(ModBlocks.ORANGE_CLOCK);

数据生成

语言和战利品

1
2
translationBuilder.add(ModBlocks.ORANGE_CLOCK, "Orange Clock");
translationBuilder.add(ModBlocks.ORANGE_CLOCK, "橙色时钟");

上面两条分别是英文和中文那个文件里的,别写一个文件里去

新方块记得补战利品表,不然挖了不掉

1
dropSelf(ModBlocks.ORANGE_CLOCK);

方块状态

上一篇那个 createNonTemplateModelBlock 现在不能用了,因为我们多了一个facing的属性,所以这里我们自己来写一个方法

1
2
3
4
5
private void generateHorizontalFacingBlock(BlockModelGenerators blockModelGenerators, Block block) {
Identifier model = ModelLocationUtils.getModelLocation(block);
blockModelGenerators.createNonTemplateHorizontalBlock(ModBlocks.ORANGE_CLOCK);
blockModelGenerators.registerSimpleItemModel(block, model);
}

这里我们直接调用 createNonTemplateHorizontalBlock,它会帮我们生成四个朝向的方块状态文件,另外同样需要调用registerSimpleItemModel来生成对应的物品模型

之后就是把我们的方块加进去

1
generateHorizontalFacingBlock(blockModelGenerators, ModBlocks.ORANGE_CLOCK);

测试

跑完数据生成,进游戏进行测试

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

  • 启动就崩,报属性没注册:createBlockStateDefinition 里漏了 builder.add(FACING)
  • 放下来永远是同一个方向:没写 getStateForPlacement
  • 模型不显示:方块状态还是那条空变体,数据生成没换成带 facing 的版本
  • 四个方向都不转:.with(...) 那段没写,或者 select 少了一个方向
  • 钟面朝反了:模型建的时候钟面朝南了,把 NOP 和 Y_ROT_180 换个位置
  • 转 90 度后碰撞箱错位:导出碰撞箱时没把枢轴点调到 X、Z 都是 8
  • 碰撞箱跟模型差一截:模型里有斜着的体块,整体旋转导出的结果不准,老老实实用一个覆盖体块代替
  • 结构方块转了朝向不变:rotate / mirror 没写
  • 物品栏里的模型是紫黑块:registerSimpleItemModel 漏了