本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 用 RotatedPillarBlock 做原木,搞懂 axis 这个属性
  • 分清原木和木头的区别
  • 学会 Fabric 的两套注册:削皮、可燃
  • 学会原木的模型生成,包括那份容易漏的「横放」模型

介绍

后面的教程要做树,还要做树的世界生成

但树得先有木头,所以这一期先把木头这套东西铺好

我们一共做五种

原木、木头、去皮原木、去皮木头、木板

树叶这次先不做,那东西要配树苗一起讲,留到树那一期

原木和木头有什么区别

原木(log)是竖着长的,它的顶面和底面是年轮,侧面是树皮

木头(wood)是把原木合成当建材用,它六个面都是树皮

所以原木需要两张贴图(侧面一张、顶面一张),木头只需要一张

查看源代码

RotatedPillarBlock

原木、木头、柱子这类方块,用的都是同一个类

1
2
public class RotatedPillarBlock extends Block {
public static final EnumProperty<Axis> AXIS = BlockStateProperties.AXIS;

AXIS 就是「轴向」,只有三个值:X、Y、Z

放下去默认是 Y,也就是竖着立起来;你横着放,游戏会自动把它设成 X 或者 Z

这个类还把 rotate 和 mirror 写好了,结构方块转房子的时候轴向会跟着转,我们不用管

原木的方块状态

原版的橡木原木长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"variants": {
"axis=x": {
"model": "minecraft:block/oak_log_horizontal",
"x": 90,
"y": 90
},
"axis=y": {
"model": "minecraft:block/oak_log"
},
"axis=z": {
"model": "minecraft:block/oak_log_horizontal",
"x": 90
}
}
}

注意 axis=x 和 axis=z 用的是另一份模型 oak_log_horizontal,不是把竖着的模型转 90 度

因为原木侧面的树皮纹理是有方向的,直接转 90 度,纹理就躺倒了

所以原版给它专门做了一份「横放」的模型,纹理按横放重新排一遍

而木头(wood)就没这个问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"variants": {
"axis=x": {
"model": "minecraft:block/oak_wood",
"x": 90,
"y": 90
},
"axis=y": {
"model": "minecraft:block/oak_wood"
},
"axis=z": {
"model": "minecraft:block/oak_wood",
"x": 90
}
}
}

削皮和可燃,原版是写死的

用斧头右键原木能削皮,这个映射表在 AxeItem 里,是个静态 Map,而且类加载的时候就建好了

方块会不会烧起来,原版是把数据塞在 FireBlock 里的一堆 setFlammable 调用,那个方法是 private 的

也就是说,这两样我们自己的方块默认都没有,得自己接

NeoForge 的做法是在方块类里重写 isFlammable、getToolModifiedState 这一堆方法

Fabric 的做法不一样,它给了两个注册表,直接在初始化的时候「登记」一下就行

FlammableBlockRegistry 管可燃,StrippableBlockRegistry 管削皮

动手写

注册五个方块

1
2
3
4
5
6
7
8
9
10
public static final Block ICE_ETHER_LOG = register("ice_ether_log", RotatedPillarBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_LOG), true);
public static final Block ICE_ETHER_WOOD = register("ice_ether_wood", RotatedPillarBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_WOOD), true);
public static final Block STRIPPED_ICE_ETHER_LOG = register("stripped_ice_ether_log", RotatedPillarBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.STRIPPED_OAK_LOG), true);
public static final Block STRIPPED_ICE_ETHER_WOOD = register("stripped_ice_ether_wood", RotatedPillarBlock::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.STRIPPED_OAK_WOOD), true);
public static final Block ICE_ETHER_PLANKS = register("ice_ether_planks", Block::new,
BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_PLANKS), true);

四个木头全用 RotatedPillarBlock,属性直接抄对应的橡木

木板是普通方块,抄橡木木板

都记得加物品栏

1
2
3
4
5
output.accept(ModItems.ICE_ETHER_LOG);
output.accept(ModItems.ICE_ETHER_WOOD);
output.accept(ModItems.STRIPPED_ICE_ETHER_LOG);
output.accept(ModItems.STRIPPED_ICE_ETHER_WOOD);
output.accept(ModItems.ICE_ETHER_PLANKS);

削皮

我们要到模组主类里面去添加以下内容

1
2
StrippableBlockRegistry.registerCopyState(ModBlocks.ICE_ETHER_LOG, ModBlocks.STRIPPED_ICE_ETHER_LOG);
StrippableBlockRegistry.registerCopyState(ModBlocks.ICE_ETHER_WOOD, ModBlocks.STRIPPED_ICE_ETHER_WOOD);

第一个参数是原来的方块,第二个是削完变成的方块

可燃

同样在主类里添加

1
2
3
4
5
6
FlammableBlockRegistry flammable = FlammableBlockRegistry.getDefaultInstance();
flammable.add(ModBlocks.ICE_ETHER_LOG, 5, 5);
flammable.add(ModBlocks.ICE_ETHER_WOOD, 5, 5);
flammable.add(ModBlocks.STRIPPED_ICE_ETHER_LOG, 5, 5);
flammable.add(ModBlocks.STRIPPED_ICE_ETHER_WOOD, 5, 5);
flammable.add(ModBlocks.ICE_ETHER_PLANKS, 5, 20);

后两个数字是「点燃几率」和「烧毁几率」

原版橡木原木是 5, 5,橡木木板是 5, 20

这些数值都可以参考FireBlock里面原版的数值

数据生成

原木和木头:一条链写完

BlockModelGenerators里有个专门给木头准备的woodProvider,原版那十几种木头全靠它生成

我们直接用它

1
2
3
4
5
6
blockModelGenerators.woodProvider(ModBlocks.ICE_ETHER_LOG)
.logWithHorizontal(ModBlocks.ICE_ETHER_LOG)
.wood(ModBlocks.ICE_ETHER_WOOD);
blockModelGenerators.woodProvider(ModBlocks.STRIPPED_ICE_ETHER_LOG)
.logWithHorizontal(ModBlocks.STRIPPED_ICE_ETHER_LOG)
.wood(ModBlocks.STRIPPED_ICE_ETHER_WOOD);

一行链式,原木和木头都有了,方块状态、模型、物品模型全齐

返回的对象能接着链式调,三个方法对应三种原木

我们用logWithHorizontal,因为树皮纹理有方向,硬转 90 度会躺倒

.wood(block) 生成的是木头,它把映射里的END槽换成SIDE的内容,于是六面都是树皮

木头六面一样,所以它内部用的是直接旋转那套,不整 _horizontal

木板

1
blockModelGenerators.createTrivialCube(ModBlocks.ICE_ETHER_PLANKS);

六面同一张图的标准方块,一行搞定

标签

方块标签,三个

1
2
3
4
5
6
7
8
9
10
11
12
valueLookupBuilder(BlockTags.LOGS)
.add(ModBlocks.ICE_ETHER_LOG)
.add(ModBlocks.ICE_ETHER_WOOD)
.add(ModBlocks.STRIPPED_ICE_ETHER_LOG)
.add(ModBlocks.STRIPPED_ICE_ETHER_WOOD);
valueLookupBuilder(BlockTags.LOGS_THAT_BURN)
.add(ModBlocks.ICE_ETHER_LOG)
.add(ModBlocks.ICE_ETHER_WOOD)
.add(ModBlocks.STRIPPED_ICE_ETHER_LOG)
.add(ModBlocks.STRIPPED_ICE_ETHER_WOOD);
valueLookupBuilder(BlockTags.PLANKS)
.add(ModBlocks.ICE_ETHER_PLANKS);

物品标签,同样三个

1
2
3
4
5
6
7
8
9
10
11
12
valueLookupBuilder(ItemTags.LOGS)
.add(ModBlocks.ICE_ETHER_LOG.asItem())
.add(ModBlocks.ICE_ETHER_WOOD.asItem())
.add(ModBlocks.STRIPPED_ICE_ETHER_LOG.asItem())
.add(ModBlocks.STRIPPED_ICE_ETHER_WOOD.asItem());
valueLookupBuilder(ItemTags.LOGS_THAT_BURN.asItem())
.add(ModBlocks.ICE_ETHER_LOG.asItem())
.add(ModBlocks.ICE_ETHER_WOOD.asItem())
.add(ModBlocks.STRIPPED_ICE_ETHER_LOG.asItem())
.add(ModBlocks.STRIPPED_ICE_ETHER_WOOD.asItem());
valueLookupBuilder(ItemTags.PLANKS)
.add(ModBlocks.ICE_ETHER_PLANKS.asItem());

注意方块和物品的标签是两套,名字一样但路径不同,两个都得写

LOGS_THAT_BURN 这个名字看着不起眼,其实是个「分类」标签

#minecraft:logs 是总标签,底下挂着 #logs_that_burn 和绯红菌柄那两套

会烧的木头进 logs_that_burn,绯红菌柄不进,这样配方里要「木头」的时候,用总标签就能把所有木头一网打尽,需要「会烧的木头」时单独查子标签

燃料判定则是看 #minecraft:logs 这个物品标签

所以我们把木头加进 ItemTags.LOGS,它就能当燃料烧;木板进 ItemTags.PLANKS,也是一样

这里再强调一次:方块标签和物品标签是两套,BlockTags.LOGS 和 ItemTags.LOGS 名字一样但完全是两个文件

战利品

1
2
3
4
5
dropSelf(ModBlocks.ICE_ETHER_LOG);
dropSelf(ModBlocks.ICE_ETHER_WOOD);
dropSelf(ModBlocks.STRIPPED_ICE_ETHER_LOG);
dropSelf(ModBlocks.STRIPPED_ICE_ETHER_WOOD);
dropSelf(ModBlocks.ICE_ETHER_PLANKS);

语言文件

1
2
3
4
5
6
7
8
9
10
11
translationBuilder.add(ModBlocks.ICE_ETHER_LOG, "Ice Ether Log");
translationBuilder.add(ModBlocks.ICE_ETHER_WOOD, "Ice Ether Wood");
translationBuilder.add(ModBlocks.STRIPPED_ICE_ETHER_LOG, "Stripped Ice Ether Log");
translationBuilder.add(ModBlocks.STRIPPED_ICE_ETHER_WOOD, "Stripped Ice Ether Wood");
translationBuilder.add(ModBlocks.ICE_ETHER_PLANKS, "Ice Ether Planks");

translationBuilder.add(ModBlocks.ICE_ETHER_LOG, "冰以太原木");
translationBuilder.add(ModBlocks.ICE_ETHER_WOOD, "冰以太木头");
translationBuilder.add(ModBlocks.STRIPPED_ICE_ETHER_LOG, "去皮冰以太原木");
translationBuilder.add(ModBlocks.STRIPPED_ICE_ETHER_WOOD, "去皮冰以太木头");
translationBuilder.add(ModBlocks.ICE_ETHER_PLANKS, "冰以太木板");

测试

跑好数据生成之后,我们就可以启动游戏进行测试了

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

  • 原木侧面是紫黑方块:模型里侧面贴图变成了 xxx_side,用的是 TextureMapping.column,原木得用 logColumn(woodProvider 已经帮你选好了)
  • 横放原木的树皮纹理躺倒了:用了 .log(),换成 .logWithHorizontal(),它会多生成一份 _horizontal 模型
  • 木头的顶面和侧面不一样:没走 .wood(),自己拼映射时忘了把 END 也填成侧面那张贴图
  • 削皮之后轴向乱了:用了 StrippableBlockRegistry.register,改成 registerCopyState
  • 斧头削不了皮:StrippableBlockRegistry 那两行没写,或者写在方块注册之前了
  • 木头烧不起来:FlammableBlockRegistry 没登记,或者数字写反了(第一个是点燃几率,第二个是烧毁几率)
  • 原木不能当燃料:物品标签 ItemTags.LOGS 漏了(不是 LOGS_THAT_BURN,燃料查的是 LOGS)
  • 方块标签写了但配方不认:只写了方块标签,物品标签也得写一份
  • 物品栏里没有木头:注册方块那行最后一个参数给的 false
  • 数据生成报 Duplicate key:同一个方块注册了两次模型,检查是不是既有 registerSimpleItemModel 又有自动生成