利用方块状态实现自定义材质的方块

简介

在 Minecraft 中,放置在世界里的方块有个非常重要的性质,称为方块状态,有以下特征和作用

  • 一个方块可能有 1 个或者多个状态,方块状态是由方块的属性 Property 决定的,属性可能有 0 个或者多个,某些方块比如石头、泥土它们是没有任何属性的,所以方块状态只有一种,具有属性的方块往往方块状态存在多个。

  • 决定客户端显示的方块材质和方块的功能。

其中,决定客户端显示的方块材质这个特性就是我们要利用的东西,很好理解,比如原版种植在田地里的作物,比如小麦方块,就有着好几种材质,在不同的生长阶段显示的材质就不同。

方块状态

进入游戏,按 F3 打开信息显示,用十字光标选中某一个方块,就能在屏幕的右下角查看到这个方块目前的状态

音符盒的方块状态

比如音符盒方块,就有着三种属性,分别为 instrument,note,power,以下为音符盒方块状态表,数据来自 wiki

名称

默认值

接受值

描述

instrument

harp

harp basedrum snare hat bass flute bell guitar chime xylophone iron_xylophone cow_bell didgeridoo bit banjo pling

乐器

note

0

0—24

音高

powered

false

true false

是否激活

一个方块的不同状态可以显示不同的模型材质,原版有很多方块不同的状态显示相同的材质,这些状态是可以拿来利用的,就拿音符盒来说,instrument 有 16 种,note 有 25 种,power 有 2 种,可以组成 800 种状态,也就是说音符盒方块可以实现最多 16x25x2=800 种材质的方块。

自定义方块材质

想要自定义方块材质,就需要了解资源包的文件结构,这里提供了一个 demo 给大家参考。

其中包含了一个自定义的材质模型以及运用,材质包的制作技术不是本文的重点。

通过了资源包文件可以了解到其中有个 blockstate 文件夹用来存放不同类型方块的方块状态材质。

打开 note_block.json 发现如下代码。

{
  "variants": {
    "": {
      "model": "minecraft:block/note_block"
    }
  }
}

可以发现音符盒所有的状态都使用了 note_block 这个材质,我们可以参考其他的方块比如 tripwire 它的代码是这样的

{
  "variants": {
    "attached=false,east=false,north=false,south=false,west=false": {
      "model": "minecraft:block/tripwire_ns"
    },
    "attached=false,east=false,north=false,south=false,west=true": {
      "model": "minecraft:block/tripwire_n",
      "y": 270
    },
    "attached=false,east=false,north=false,south=true,west=false": {
      "model": "minecraft:block/tripwire_n",
      "y": 180
    }
    // ....
  }

发现了 tripwire 的每种状态都有一个材质,我们也可以对音符盒进行这样的修改,代码如下

{
    "variants": {
        "instrument=harp,note=0,powered=false": {
            "model": "custom/block/snow_brick"
        }
    }
    //....
}

以上的模型来自于 demo,加载材质后发现方块状态为 harp 0 false 音符盒的材质发生了改变

通过音符盒实现的雪砖方块

于是我们就可以发现,原版有很多类似的方块可以用来魔改,比如有 age 属性的甘蔗,发光浆果藤,缠怨藤,垂泪藤,海带以及其他属性的树叶,树苗等等。

但是这里就有个问题了,方块状态除了显示材质以外,还有个很重要的功能,就是决定了方块的功能,比如音符盒的 instrument 决定了乐器,note 决定了音调以及 power 表示是否被红石充能,如果这些状态被拿来做自定义方块,那么这些方块具有的原版功能岂不是没有用了?

看似是这样,但是其实是有方法可以做到一部分状态用来显示原版材质和功能,另一部分的状态用来实现自定义方块,怎么样做到呢,让我们来思考。

思考

在 mc 代码的 Block 类中,有个这样的静态属性

public static final IdMapper<BlockState> BLOCK_STATE_REGISTRY = new IdMapper<>();

这个属性记录了每种方块状态及其对应的 Integer 型 id,有个很重要的作用就是与客户端通信使用的,似乎就这一个功能

在服务端,通过 Block.BLOCK_STATE_REGISTRY.getId(blockState) 方法可以得到这个方块的id然后发送给客户端。

客户端通过 Block.BLOCK_STATE_REGISTRY.byId(id) 方法把id转换为 blockstate,反之亦然。

毕竟通过一个 integer 的数据来标识方块状态,进行网络传输是非常高效的。

经过研究发现这个 IdMapper 可以使多种 blockstate 具有相同的 id,这个特性非常有用。

想要做到一部分方块状态用来执行原版功能,其余的状态用来做自定义方块,就需要把真实的方块状态隐藏起来,让客户端看到的是一个的方块状态。

这类方块必须有个特点,就是必须有多个状态显示相同的材质,哪怕其中有的属性会改变方块显示的材质,但只要存在那种属性,不管怎么改变都不会影响方块显示的材质,就能拿来魔改。

就比如音符盒,它的三种属性(instrument,note,power)的改变都不会引起方块材质的改变,有800种状态却显示的是相同的材质,我们可以拿其中的1种状态用来显示原版的材质,并且执行原版的功能,那么剩下的799种就是可以拿来使用的状态。

想要做到这一点就需要给音符盒方块添加三个额外属性,称为假属性,这三个属性的数据类型和真实属性的数据类型对应,只不过命名不同,而且这个命名还很讲究,比如 ainstrument, bnote, cpower。

为什么要这样命名呢,因为属性最后是通过枚举来得到不同的方块状态,属性的顺序会决定blockstate在 IdMapper 中的id顺序,而属性的顺序是由存到 hash 表当中的属性名决定的,我们需要满足两个条件

  • 所有假属性必须排列在真属性之前

  • 假属性之间的顺序必须和真属性之间的顺序相同

之所以要满足这些条件,是为了方便后续魔改方块 id 注册机制。

这样音符盒方块就有了6种属性,按名称的hash顺序分别为 ainstrument、bnote、cpower、instrument、note、power。

然后就需要对音符盒方块状态的注册机制进行修改了,我们需要用额外添加的假属性产生的状态对之前存在的状态进行替换,

比如 instrument、note、power 分别为 HARP、0、false 这种状态的方块,在 IdMapper 中的 id 为 282(版本1.18.1),HARP、0、true 则为 289,我们需要把添加假属性后的方块,状态 ainstrument=HARP、bnote=0、cpower=false 所对应的800种状态的 id 注册为 282,ainstrument=HARP、bnote=0、cpower=true,所对应的注册为 289 以此类推,这样就可以达到让客户端看到的是的方块状态,而真实的状态,用于实现功能的状态,被隐藏起来了。

魔改方块属性

看起来似乎很难实现,但我们可以通过魔改服务端代码来实现这个效果,魔改服务端的技术有很多,比如 Paper 项目组的 paperweight,以及 SpigotMC 的 BuildTools,服务端魔改技术本文不作重点。

这里就拿上文的音符盒来举例,我们找到音符盒的 mc 源代码,这里只例举了核心代码。

public class NoteBlock extends Block {
​
    public static final EnumProperty<NoteBlockInstrument> INSTRUMENT = EnumProperty.create("instrument", NoteBlockInstrument.class);
    public static final BooleanProperty POWERED = BooleanProperty.create("powered");
    public static final IntegerProperty NOTE = NOTE = IntegerProperty.create("note", 0, 24);
    
    public NoteBlock(BlockBehaviour.Properties settings) {
        super(settings);
        this.registerDefaultState(this.stateDefinition.any()
                .setValue(INSTRUMENT, NoteBlockInstrument.HARP)
                .setValue(NOTE, 0)
                .setValue(POWERED, false));
    }
    
    @Override
    protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
        builder.add(INSTRUMENT, POWERED, NOTE);
    }
}

可以给它添加 3 个额外的属性,叫做 ainstrument,bnote,cpower。

public class NoteBlock extends Block {
​
    public static final EnumProperty<NoteBlockInstrument> FAKE_INSTRUMENT = EnumProperty.create("ainstrument", NoteBlockInstrument.class);
    public static final BooleanProperty FAKE_POWERED = BooleanProperty.create("cpowered");
    public static final IntegerProperty FAKE_NOTE = NOTE = IntegerProperty.create("bnote", 0, 24);
    public static final EnumProperty<NoteBlockInstrument> INSTRUMENT = EnumProperty.create("instrument", NoteBlockInstrument.class);
    public static final BooleanProperty POWERED = BooleanProperty.create("powered");
    public static final IntegerProperty NOTE = NOTE = IntegerProperty.create("note", 0, 24);
    
    public NoteBlock(BlockBehaviour.Properties settings) {
        super(settings);
        this.registerDefaultState(this.stateDefinition.any()
                .setValue(FAKE_INSTRUMENT, NoteBlockInstrument.HARP)
                .setValue(FAKE_NOTE, 0)
                .setValue(FAKE_POWERED, false));
                .setValue(INSTRUMENT, NoteBlockInstrument.HARP)
                .setValue(NOTE, 0)
                .setValue(POWERED, false));
    }
    
    @Override
    protected void createBlockStateDefinition(StateDefinition.Builder<Block, BlockState> builder) {
        builder.add(FAKE_INSTRUMENT, FAKE_POWERED, FAKE_NOTE, INSTRUMENT, POWERED, NOTE);
    }
}

这样就完成了音符盒方块属性的魔改。

魔改方块状态注册机制

找到方块状态注册的代码,发现是在 Blocks 类下的一个静态代码块

static {
    for(Block block : Registry.BLOCK) {
        for(BlockState blockState : block.getStateDefinition().getPossibleStates()) {
            //注册blockstate并分配一个id
            Block.BLOCK_STATE_REGISTRY.add(blockState);
        }
        block.getLootTable();
    }
}

我们需要对音符盒的注册动点手脚,我们在第一层 for 循环内加入以下代码

if (block.getClass() == NoteBlock.class) {
    //键用来存放转换合并后的属性,值用来存放注册过的id
    Map<Integer, Integer> map = Maps.newHashMap();
    for(BlockState blockState : block.getStateDefinition().getPossibleStates()) {
        //将ainstrument bnote cpowered转换成一个int类型的值,方便map记录
        int ainstrument = blockState.getValue(NoteBlock.FAKE_INSTRUMENT).ordinal();
        int bnote = blockState.getValue(NoteBlock.FAKE_NOTE);
        boolean cpowered = blockState.getValue(NoteBlock.FAKE_POWERED);
        int key = (ainstrument & 63) << 7 | (bnote & 63) << 1 | (cpowered ? 1 : 0);
        
        //如果map中存在这个key则取出id直接注册 否则先注册并记录id
        if (map.containsKey(key)) {
            Block.BLOCK_STATE_REGISTRY.addMapping(blockState, map.get(key));
        } else {
            Block.BLOCK_STATE_REGISTRY.add(blockState);
            map.put(key, Block.BLOCK_STATE_REGISTRY.getId(blockState));
        }
        block.getLootTable();
        continue;
}

这样就能达到假属性产生的状态对之前存在的状态进行替换的效果。进入到游戏当中,发现不管怎么改变 instrument、note、power,在 F3 信息面板中的方块状态都是固定的了,因为真实的属性被隐藏起来了,实际上客户端看到的是我们添加的三种假属性,我们只有改变方块的假属性时,客户端的显示才会改变。

总结

随着 minecraft 的版本更新,未来可能会有更多的方块可以用来实现自定义材质的方块。

本文讲解了如何通过魔改服务端,和自定义材质包来实现自定义材质的方块而又不损失原版的一些方块特性,其中还存在着一些技术细节,但本文不过多讲述了,仅提供一种思路供读者参考。

本文所讲解的一些技术,已经是被证明是可行的,但是有很多可以优化的点,比如音符盒方块经过魔改之后,存在着 800x800=64 万种方块状态,其中可能只有 800+799=1699 种状态是有用的,其余的方块状态则没有太大用处,而且会占用部分的服务器内存,减慢服务端启动速度,这些都是可以通过优化来解决的。