本篇教程的视频

本篇教程的源代码

GitHub地址:TutorialMod-BuildingBlocks-1.20.1

本篇教程目标

  • 理解原版各种建材类方块的注册
  • 学会添加各种建材类方块

查看源代码

这里的建材类方块,指的是楼梯台阶活板门等这样的建材类方块

我们先来看看原版中的这些方块是怎么注册的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
public static final Block OAK_STAIRS = register("oak_stairs", new StairsBlock(OAK_PLANKS.getDefaultState(), AbstractBlock.Settings.copy(OAK_PLANKS)));

public static final Block OAK_SLAB = register(
"oak_slab",
new SlabBlock(
AbstractBlock.Settings.create().mapColor(MapColor.OAK_TAN).instrument(Instrument.BASS).strength(2.0F, 3.0F).sounds(BlockSoundGroup.WOOD).burnable()
)
);

public static final Block OAK_BUTTON = register("oak_button", createWoodenButtonBlock(BlockSetType.OAK));

public static final Block OAK_PRESSURE_PLATE = register(
"oak_pressure_plate",
new PressurePlateBlock(
PressurePlateBlock.ActivationRule.EVERYTHING,
AbstractBlock.Settings.create()
.mapColor(OAK_PLANKS.getDefaultMapColor())
.solid()
.instrument(Instrument.BASS)
.noCollision()
.strength(0.5F)
.burnable()
.pistonBehavior(PistonBehavior.DESTROY),
BlockSetType.OAK
)
);

public static final Block OAK_FENCE = register(
"oak_fence",
new FenceBlock(
AbstractBlock.Settings.create()
.mapColor(OAK_PLANKS.getDefaultMapColor())
.solid()
.instrument(Instrument.BASS)
.strength(2.0F, 3.0F)
.sounds(BlockSoundGroup.WOOD)
.burnable()
)
);

public static final Block OAK_FENCE_GATE = register(
"oak_fence_gate",
new FenceGateBlock(
AbstractBlock.Settings.create().mapColor(OAK_PLANKS.getDefaultMapColor()).solid().instrument(Instrument.BASS).strength(2.0F, 3.0F).burnable(), WoodType.OAK
)
);

public static final Block COBBLESTONE_WALL = register("cobblestone_wall", new WallBlock(AbstractBlock.Settings.copy(COBBLESTONE).solid()));

public static final Block OAK_DOOR = register(
"oak_door",
new DoorBlock(
AbstractBlock.Settings.create()
.mapColor(OAK_PLANKS.getDefaultMapColor())
.instrument(Instrument.BASS)
.strength(3.0F)
.nonOpaque()
.burnable()
.pistonBehavior(PistonBehavior.DESTROY),
BlockSetType.OAK
)
);

public static final Block OAK_TRAPDOOR = register(
"oak_trapdoor",
new TrapdoorBlock(
AbstractBlock.Settings.create().mapColor(MapColor.OAK_TAN).instrument(Instrument.BASS).strength(3.0F).nonOpaque().allowsSpawning(Blocks::never).burnable(),
BlockSetType.OAK
)
);

这里的话,除了按钮方块,其他的方块都是直接用register方法来注册的,然后实例化对应的方块类

按钮方块其实也一样,在它的那个方法里,实例化的也是按钮对应的方块类

这里面具体的参数我们在之后再解释,这里就不解释了

方块族BlockFamilies

但是还没完,如果你查找其中一个字段引用的地方,你会发现它们在一个BlockFamilies类中被调用了

1
2
3
4
5
6
7
8
9
10
11
12
13
public static final BlockFamily OAK = register(Blocks.OAK_PLANKS)
.button(Blocks.OAK_BUTTON)
.fence(Blocks.OAK_FENCE)
.fenceGate(Blocks.OAK_FENCE_GATE)
.pressurePlate(Blocks.OAK_PRESSURE_PLATE)
.sign(Blocks.OAK_SIGN, Blocks.OAK_WALL_SIGN)
.slab(Blocks.OAK_SLAB)
.stairs(Blocks.OAK_STAIRS)
.door(Blocks.OAK_DOOR)
.trapdoor(Blocks.OAK_TRAPDOOR)
.group("wooden")
.unlockCriterionName("has_planks")
.build();

比如这里的OAK,我们可以看到它调用了上面提到的大部分建材类方块

那么这个类用来干什么呢?我们再进一步找这个类调用的地方

1
2
3
BlockFamilies.getFamilies()
.filter(BlockFamily::shouldGenerateModels)
.forEach(family -> this.registerCubeAllModelTexturePool(family.getBaseBlock()).family(family));

我们可以在BlockStateModelGenerator类里找到这一串语句,这是其中一个

而这个类是干什么的?那自然就是数据生成用的

待会我们写到数据生成的时候,就会用到它

那么我们现在回过头来看看这个方块族,这个东西解释一下就是由一个主方块引出的各种建材类方块

其中除了活板门还有告示牌,其余的方块都是与主方块共用同一个材质的(注:告示牌是方块实体,其材质是展开图

比如上面写到的OAK里面,它的主方块是OAK_PLANKS,即橡木木板,而除了橡木门活板门以及告示牌,其他方块的材质都是橡木木板的材质

所以这里就会冒出来一个方块族,同样的,这里的unlockCriterionName也是与解锁配方的进度相关的

不过在实际编写的时候,是可以不写的

注册方块

我们到ModBlocks类里,添加我们的方块

楼梯

1
2
public static final Block ICE_ETHER_STAIRS = register("ice_ether_stairs",
new StairsBlock(ICE_ETHER_BLOCK.getDefaultState(), AbstractBlock.Settings.copy(ICE_ETHER_BLOCK)));

StairsBlock的构造方法需要两个参数

第一个参数是基础方块,这里的基础方块我们就直接拿我们前面写的ICE_ETHER_BLOCK

第二个参数是方块设置

台阶

1
2
public static final Block ICE_ETHER_SLAB = register("ice_ether_slab",
new SlabBlock(AbstractBlock.Settings.copy(ICE_ETHER_BLOCK)));

SlabBlock的构造方法只需要一个参数,就是方块设置,这里直接复制ICE_ETHER_BLOCK的设置即可

按钮

1
2
public static final Block ICE_ETHER_BUTTON = register("ice_ether_button",
new ButtonBlock(AbstractBlock.Settings.copy(ICE_ETHER_BLOCK), BlockSetType.STONE, 60, false));

ButtonBlock的构造方法需要四个参数

第一个参数是方块设置

第二个参数是方块类型

第三个参数是按钮从按下弹起的时间,单位tick

第四个是是否为木制,其决定了是否能被三叉戟这样的投掷物激活

压力板

1
2
public static final Block ICE_ETHER_PRESSURE_PLATE = register("ice_ether_pressure_plate",
new PressurePlateBlock(PressurePlateBlock.ActivationRule.EVERYTHING, AbstractBlock.Settings.copy(ICE_ETHER_BLOCK), BlockSetType.STONE));

PressurePlateBlock的构造方法需要三个参数

第一个参数是激活规则,这里我们用EVERYTHING,表示任何实体都会激活,另外一个类型是MOB,则仅限生物实体才能激活

第二个参数是方块设置

第三个参数是方块类型

栅栏

1
2
public static final Block ICE_ETHER_FENCE = register("ice_ether_fence",
new FenceBlock(AbstractBlock.Settings.copy(ICE_ETHER_BLOCK)));

FenceBlock的构造方法只需要一个参数,就是方块设置

栅栏门

1
2
public static final Block ICE_ETHER_FENCE_GATE = register("ice_ether_fence_gate",
new FenceGateBlock(AbstractBlock.Settings.copy(ICE_ETHER_BLOCK), WoodType.OAK));

FenceGateBlock的构造方法需要两个参数

第一个参数是方块设置

第二个参数是木头类型,原版所有的栅栏门都是木制的,所以它对应类的构造函数中也有这么一个参数,这里就拿个原版的木头类型过来就好了

1
2
public static final Block ICE_ETHER_WALL = register("ice_ether_wall",
new WallBlock(AbstractBlock.Settings.copy(ICE_ETHER_BLOCK)));

WallBlock的构造方法只需要一个参数,就是方块设置

1
2
public static final Block ICE_ETHER_DOOR = register("ice_ether_door",
new DoorBlock(AbstractBlock.Settings.copy(ICE_ETHER_BLOCK), BlockSetType.IRON));

DoorBlock的构造方法需要两个参数

第一个参数是方块设置

第二个参数是方块类型,注意,如果这里设置为IRON,那么就是和原版的铁门一样,只能用红石信号来打开,而不能直接打开

活板门

1
2
public static final Block ICE_ETHER_TRAPDOOR = register("ice_ether_trapdoor",
new TrapdoorBlock(AbstractBlock.Settings.copy(ICE_ETHER_BLOCK).nonOpaque(), BlockSetType.STONE));

TrapdoorBlock的构造方法需要两个参数
第一个参数是方块设置,这里加上了一个nonOpaque(),表示这个方块是非实心的,如果说你要使用带完全或者半透明的材质,这个方法得加上

第二个参数是方块类型,这个与上面的门一样

添加到物品栏

不要忘了物品栏

1
2
3
4
5
6
7
8
9
entries.add(ModBlocks.ICE_ETHER_STAIRS);
entries.add(ModBlocks.ICE_ETHER_SLAB);
entries.add(ModBlocks.ICE_ETHER_BUTTON);
entries.add(ModBlocks.ICE_ETHER_PRESSURE_PLATE);
entries.add(ModBlocks.ICE_ETHER_FENCE);
entries.add(ModBlocks.ICE_ETHER_FENCE_GATE);
entries.add(ModBlocks.ICE_ETHER_WALL);
entries.add(ModBlocks.ICE_ETHER_DOOR);
entries.add(ModBlocks.ICE_ETHER_TRAPDOOR);

数据生成

那么注册就是这一堆,接下来就要写它的各种数据文件

方块模型

方块模型呢,如果你要像之前写的registerSimpleCubeAll那样类似地写,你会发现楼梯台阶这种是没有对应方法的

这个时候,我们就要用到上面看源代码时提到的那个BlockFamilies

ModBlockFamilies

这里我们来创建一个ModBlockFamilies类,用来存放我们自己的方块族

1
2
3
public class ModBlockFamilies {

}

然后里面从原版的BlockFamilies类中搬一些要用到的东西过来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private static final Map<Block, BlockFamily> BASE_BLOCKS_TO_FAMILIES = Maps.<Block, BlockFamily>newHashMap();

public static BlockFamily.Builder register(Block baseBlock) {
BlockFamily.Builder builder = new BlockFamily.Builder(baseBlock);
BlockFamily blockFamily = (BlockFamily)BASE_BLOCKS_TO_FAMILIES.put(baseBlock, builder.build());
if (blockFamily != null) {
throw new IllegalStateException("Duplicate family definition for " + Registries.BLOCK.getId(baseBlock));
} else {
return builder;
}
}
public static Stream<BlockFamily> getFamilies() {
return BASE_BLOCKS_TO_FAMILIES.values().stream();
}

我们从原版的类搬三块语句进来,这里的话,没有我们要改的东西,所以就直接搬过来即可

最后一个方法是最后数据生成时调用的

然后我们来写一个方块族

1
2
3
4
5
6
7
8
9
10
11
public static final BlockFamily ICE_ETHER = register(ModBlocks.ICE_ETHER_BLOCK)
.stairs(ModBlocks.ICE_ETHER_STAIRS)
.slab(ModBlocks.ICE_ETHER_SLAB)
.button(ModBlocks.ICE_ETHER_BUTTON)
.pressurePlate(ModBlocks.ICE_ETHER_PRESSURE_PLATE)
.fence(ModBlocks.ICE_ETHER_FENCE)
.fenceGate(ModBlocks.ICE_ETHER_FENCE_GATE)
.wall(ModBlocks.ICE_ETHER_WALL)
.door(ModBlocks.ICE_ETHER_DOOR)
.trapdoor(ModBlocks.ICE_ETHER_TRAPDOOR)
.build();

这里我们用到了上面搬过来的register方法,然后依次添加我们注册的方块

数据生成调用

那么写好我们的方块族之后,我们就可以在数据生成中调用这个类了

generateBlockStateModels方法中写上

1
2
3
4
5
6
ModBlockFamilies.getFamilies()
.filter(BlockFamily::shouldGenerateModels)
.forEach(family ->
blockStateModelGenerator.
registerCubeAllModelTexturePool(family.getBaseBlock())
.family(family));

这个其实也是照着BlockStateModelGenerator中的语句写的,只是将this替换为方法中的形参blockStateModelGenerator

这个呢,其实就是来遍历方块族这个类中所以需要生成模型的方块,来生成它们的模型

registerCubeAllModelTexturePool即为生成一个共用的材质池,生成的模型会共用同一个材质

注意,我们还要将原来的ICE ETHER BLOCK相应的方块模型生成语句给去掉,不然会导致生成重复的模型

因为在方块族中已经包括了ICE ETHER BLOCK这个方块了

战利品列表

1
2
3
4
5
6
7
8
9
addDrop(ModBlocks.ICE_ETHER_STAIRS);
addDrop(ModBlocks.ICE_ETHER_SLAB, slabDrops(ModBlocks.ICE_ETHER_SLAB));
addDrop(ModBlocks.ICE_ETHER_BUTTON);
addDrop(ModBlocks.ICE_ETHER_PRESSURE_PLATE);
addDrop(ModBlocks.ICE_ETHER_FENCE);
addDrop(ModBlocks.ICE_ETHER_FENCE_GATE);
addDrop(ModBlocks.ICE_ETHER_DOOR, doorDrops(ModBlocks.ICE_ETHER_DOOR));
addDrop(ModBlocks.ICE_ETHER_TRAPDOOR);
addDrop(ModBlocks.ICE_ETHER_WALL);

战利品列表的数据生成就台阶特殊一点

台阶两个组合之后变成一个完整的方块,但是破坏时,依旧会掉落两个台阶

而门有半门特性,它分上下两个部分,但是破坏时,不论破坏哪个部分,都只会掉一个且完整的门

其他的方块则就像一般的方块来写就可以了

语言文件

1
2
3
4
5
6
7
8
9
translationBuilder.add(ModBlocks.ICE_ETHER_STAIRS, "Ice Ether Stairs");
translationBuilder.add(ModBlocks.ICE_ETHER_SLAB, "Ice Ether Slab");
translationBuilder.add(ModBlocks.ICE_ETHER_BUTTON, "Ice Ether Button");
translationBuilder.add(ModBlocks.ICE_ETHER_PRESSURE_PLATE, "Ice Ether Pressure Plate");
translationBuilder.add(ModBlocks.ICE_ETHER_FENCE, "Ice Ether Fence");
translationBuilder.add(ModBlocks.ICE_ETHER_FENCE_GATE, "Ice Ether Fence Gate");
translationBuilder.add(ModBlocks.ICE_ETHER_WALL, "Ice Ether Wall");
translationBuilder.add(ModBlocks.ICE_ETHER_DOOR, "Ice Ether Door");
translationBuilder.add(ModBlocks.ICE_ETHER_TRAPDOOR, "Ice Ether Trapdoor");

测试

那么最后,我们就可以跑一下数据生成,在此之后我们就可以启动我们的游戏进行测试了

出现Bug!

好,那么我们门和活板门是采用了透明的材质,然后呢,现在该透明的地方是一片

因为我们还得为方块配置渲染层,告诉游戏怎么处理这些透明通道

我们到TutorialModClient中的onInitializeClient方法中写上

1
2
3
BlockRenderLayerMap.INSTANCE.putBlock(ModBlocks.ICE_ETHER_DOOR, RenderLayer.getCutout());
BlockRenderLayerMap.INSTANCE.putBlock(ModBlocks.ICE_ETHER_TRAPDOOR, RenderLayer.getCutout());
BlockRenderLayerMap.INSTANCE.putBlock(ModBlocks.ICE_ETHER_BLOCK, RenderLayer.getTranslucent());

渲染这个东西都是在客户端完成的,就像你在服务器(服务端)中,你加什么材质光影,和服务器没有任何关系

它们都是在本地客户端渲染的

这里我们使用BlockRenderLayerMap.INSTANCE.putBlock来设置方块的渲染层

第一个参数是方块,第二个是渲染层类型设置

getCutout为全透明材质使用,getTranslucent为半透明材质使用

ICE ETHER BLOCK的材质其实并不是完全不透明的,所以我们这里也可以设置一下

另外,不论完全还是半透明的方块,只要有一个面是完全与其他方块贴合的,就要在注册方块时加上非实心方法,即nonOpaque

因为MC为了节约渲染开销,方块相接触的面是不会被渲染的,如果不设置方块非实心,则会导致透视

好,那么现在我们就可以重新启动游戏去运行了