本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 学会用 lightLevel 让方块发光
  • 学会用 LIT 属性做「开关灯」
  • 搞清楚哪些属性要写进方块状态文件、哪些不用
  • 复习一遍右键交互(useWithoutItem)

介绍

前面几期的方块,改的都是「形」,朝向、连接,肉眼能看出变化

这一期改的是「光」

发光方块分两种

一种是一直亮着的,火把、荧石、南瓜灯,放下去就亮,没有开关

另一种是能开关的,红石灯是最典型的例子:有信号亮,没信号灭

两种做法其实是一套,后者只是多了一个属性

这期我们做一个能右键开关的壁灯

查看源代码

lightLevel

先看火把,Blocks 里的注册

1
2
3
4
5
public static final Block TORCH = register(
"torch",
p -> new TorchBlock(ParticleTypes.FLAME, p),
BlockBehaviour.Properties.of().noCollision().instabreak().lightLevel(state -> 14).sound(SoundType.WOOD).pushReaction(PushReaction.DESTROY)
);

火把能发光就是实现了lightLevel这个方法,参数是一个 ToIntFunction<BlockState>

火把给它一个常数14,无论什么状态,光照等级都是 14

光照等级的范围是0到 15,15 是荧石、海晶灯那一档,火把是14

能开关的

我们可以再来看看红石灯

1
2
3
4
5
6
7
8
9
10
public static final Block REDSTONE_LAMP = register(
"redstone_lamp",
RedstoneLampBlock::new,
BlockBehaviour.Properties.of()
.mapColor(MapColor.TERRACOTTA_ORANGE)
.lightLevel(litBlockEmission(15))
.strength(0.3F)
.sound(SoundType.GLASS)
.isValidSpawn(Blocks::always)
);

注意这里传的不是常数,而是 litBlockEmission(15)

我们再来看看这个方法

1
2
3
public static ToIntFunction<BlockState> litBlockEmission(final int lightEmission) {
return state -> state.getValue(BlockStateProperties.LIT) ? lightEmission : 0;
}

显然,LIT为true时就返回 15,否则返回 0

所以「开关灯」的本质就是:方块状态里有个布尔值,光照等级跟着它走

LIT 属性

1
public static final BooleanProperty LIT = BlockStateProperties.LIT;

BooleanProperty 就是我们上一篇用的 EnumProperty 的特例(因为它也是继承EnumProperty的),只有 true 和 false 两个值

用到它的原版方块不少:熔炉、烟熏炉、高炉、红石灯、蜡烛,都是这一个属性

动手写

LampBlock

1
2
3
4
5
6
7
8
public class LampBlock extends Block {
public static final BooleanProperty LIT = BlockStateProperties.LIT;

public LampBlock(Properties properties) {
super(properties.lightLevel(state -> state.getValue(LIT) ? 15 : 0));
this.registerDefaultState(this.stateDefinition.any().setValue(LIT, true));
}
}

Properties是可以链式调用的,我们在构造函数里补一条 lightLevel,效果和写在注册语句里完全一样,你也可以在注册时写这个方法

后面的方块状态初始值,我们默认给 true,也就是放下来就是亮的

属性声明

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

老规矩,加了属性不声明,启动就崩

碰撞箱

这个壁灯模型只有 4 像素高,挂在墙上,碰撞箱也得跟着缩

1
2
3
4
5
6
public static final VoxelShape SHAPE = Block.box(0, 6, 12, 16, 10, 16);

@Override
protected VoxelShape getShape(BlockState state, BlockGetter level, BlockPos pos, CollisionContext context) {
return SHAPE;
}

当然这里我们就省略了FACING,大家可以按照需要补上

实现右键开关

1
2
3
4
5
6
7
@Override
protected InteractionResult useWithoutItem(BlockState state, Level level, BlockPos pos, Player player, BlockHitResult hitResult) {
if (!level.isClientSide()) {
level.setBlock(pos, state.setValue(LIT, !state.getValue(LIT)), 3);
}
return InteractionResult.SUCCESS;
}

逻辑很简单,每右键一次就是把当前LIT值取反,然后 setBlock 写回去

state.setValue(LIT, !state.getValue(LIT)) 是取反的写法,比写 if/else 两个分支短

setBlock 的第三个参数是更新标记

光这一块不用我们管,方块状态一变,游戏会自己重算光照

这里有两点要强调

第一,**判断必须是 !level.isClientSide()**,只在服务端改

第二,返回值给 InteractionResult.SUCCESS,手会挥一下,手感正常

返回 PASS 的话,空手右键等于什么都没发生,有些情况下还会把交互让给别的方块

注册

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

noOcclusion 照旧,因为它不是满方块

光照那个参数我们在方块类里写了,这里就不用重复

顺手加物品栏

1
output.accept(ModBlocks.LAMP_BLOCK);

语言文件

1
2
translationBuilder.add(ModBlocks.LAMP_BLOCK, "Lamp");
translationBuilder.add(ModBlocks.LAMP_BLOCK, "壁灯");

战利品表

1
dropSelf(ModBlocks.LAMP_BLOCK);

数据生成

方块状态

这里有个容易搞混的地方:LIT 有这么个属性,方块状态文件里要不要给两个变体?

我们继续用前几期那个生成静态模型的方法

1
generateCustomBlock(blockModelGenerators, ModBlocks.LAMP_BLOCK);

生成出来是这样

1
2
3
4
5
6
7
{
"variants": {
"": {
"model": "tutorial:block/lamp_block"
}
}
}

那个空的键 "" 表示「任何状态都用这个模型」

为什么可以这么写?因为开灯和关灯,模型是同一个

先想清楚一件事:写进方块状态文件的属性,是会让方块「换模型 / 转角度」的属性

比如上一篇的 FACING,不同的值模型转的角度不一样,所以必须列出来

type 也是,四个值对应四份模型

而 LIT 只影响光照等级,模型一动不动,就不用在方块状态里提它

但是红石灯就必须列两个

1
2
3
4
5
6
7
8
9
10
{
"variants": {
"lit=false": {
"model": "minecraft:block/redstone_lamp"
},
"lit=true": {
"model": "minecraft:block/redstone_lamp_on"
}
}
}

因为它亮的时候是另一张贴图

如果你也想让灯「亮的时候换个模型」,比如灯罩换成发光的贴图,那就得自己拼MultiVariantGenerator,把LIT的两个值分别select到两个模型上,与我们之前写沙发的方法类似

模型和贴图

模型文件放

1
2
assets/tutorial/models/block/lamp_block.json
assets/tutorial/textures/block/lamp_block.png

Blockbench 导出的那份,记得检查命名空间是不是自己的 modid

测试

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

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

  • 方块不发光:lightLevel 没加,或者加在了 BlockBehaviour.Properties.of() 之前被覆盖掉了
  • 放下来不亮,要右键一下才亮:registerDefaultState 里没设 LIT 默认值,或者设成了 false
  • 右键没反应:createBlockStateDefinition 里漏了 LIT,或者判断写成了 level.isClientSide()
  • 客户端亮了服务端没变,走远回来又灭了:setBlock 写在客户端了,判断方向反了
  • 亮度和预期不一样:lightLevel 返回的是 0 到 15,写超过 15 没意义
  • 灯周围有光但方块本身是黑的:贴图问题,和光照无关,检查贴图路径
  • 模型显示成紫黑方块:贴图命名空间不对,或者 registerSimpleItemModel 漏了
  • 启动崩溃,报属性没注册:createBlockStateDefinition 漏了 LIT
  • 想让亮灯换个模型但没生效:方块状态文件还是空的 "",得改成按 LIT 分两个变体
  • 右键灯的时候手上的方块被放下去了:useItemOn 没重写,手上拿东西时会先尝试放置