本篇教程的视频

本篇教程的源代码

GitHub地址:

本篇教程目标

  • 搞清「树是一种地物」,以及它由哪几块拼成
  • 学会用 TreeConfiguration 描述一棵树
  • 学会用 TreeGrower 让树苗长成树
  • 学会把地物挂进 26.1 的数据生成

介绍

树整个结构在游戏里不是方块,而是一种地物(Feature)

除了树,还有矿脉、湖泊、随机斑块,都是地物

这期我们先来写一棵树,后面再来写树的世界生成

查看源代码

TreeFeatures

原版所有的树都在 TreeFeatures 这个类里,我们看橡树

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private static TreeConfiguration.TreeConfigurationBuilder createStraightBlobTree(
final Block oakLog, final Block oakLeaves, final int baseHeight, final int heightRandA, final int heightRandB, final int blobRadius
) {
return new TreeConfiguration.TreeConfigurationBuilder(
BlockStateProvider.simple(oakLog),
new StraightTrunkPlacer(baseHeight, heightRandA, heightRandB),
BlockStateProvider.simple(oakLeaves),
new BlobFoliagePlacer(ConstantInt.of(blobRadius), ConstantInt.of(0), 3),
new TwoLayersFeatureSize(1, 0, 1)
);
}

private static TreeConfiguration.TreeConfigurationBuilder createOak() {
return createStraightBlobTree(Blocks.OAK_LOG, Blocks.OAK_LEAVES, 4, 2, 0, 2).ignoreVines();
}

这个 TreeConfigurationBuilder 有五个参数:

  • BlockStateProvider.simple(oakLog):树干用什么方块
  • StraightTrunkPlacer(4, 2, 0):树干生成器,「笔直树干」;基础高度、随机高度 A、随机高度 B,最终高度是「基础 + 0 到 A + 0 到 B」这个范围里随机
  • BlockStateProvider.simple(oakLeaves):树叶用什么方块
  • BlobFoliagePlacer(ConstantInt.of(2), ConstantInt.of(0), 3):树叶生成器,「一团球」,三个参数是半径、偏移、从树顶往下第几层开始长
  • TwoLayersFeatureSize(1, 0, 1):树的「尺寸」,管的是碰撞检测那套,一般照抄原版
  • ignoreVines() 是「不挂藤蔓」,我们的树没有藤蔓,加上也无所谓

注册

1
FeatureUtils.register(context, OAK, Feature.TREE, createOak().build());

FeatureUtils.register 三件事:注册上下文、注册键、地物类型(Feature.TREE),最后是配置

TreeGrower

树苗长大靠这个类

1
2
3
public static final TreeGrower OAK = new TreeGrower("oak", 0.1F, Optional.empty(), Optional.empty(),
Optional.of(TreeFeatures.OAK), Optional.of(TreeFeatures.FANCY_OAK),
Optional.of(TreeFeatures.OAK_BEES_005), Optional.of(TreeFeatures.FANCY_OAK_BEES_005));

TreeGrower 有两个构造

四个参数的:名字、大型树、普通树、带花树,适合只有一种形态的树

带 0.1F 那个是六个以上参数的,多了一个概率,用来在几种形态之间随机

橡树有四种形态(普通、珍异、带蜂巢的),所以用长的那套

我们只有一种树,用四个参数的就行

26.1 的树叶类

打开 LeavesBlock 你会发现它现在是抽象类

1
2
public abstract class LeavesBlock extends Block implements SimpleWaterloggedBlock {
public abstract MapCodec<? extends LeavesBlock> codec();

抽象的原因是「落叶粒子」有两个实现,得挑一个

TintedParticleLeavesBlock:随生物群系给树叶染色,原版橡树、白桦都是这个,贴图本身是灰的

UntintedParticleLeavesBlock:不染色,用你贴图本来的颜色,另外要指定掉什么粒子

我们的树叶贴图是自己画的固定颜色,不能再被染色,所以用后者

动手写

树叶

1
2
3
public static final Block ICE_ETHER_LEAVES = register("ice_ether_leaves",
p -> new UntintedParticleLeavesBlock(0.02F, ParticleTypes.SNOWFLAKE, p),
BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_LEAVES), true);

三个参数:落叶粒子的概率、粒子类型、方块属性

概率给0.02F,粒子类型可以大家自己挑一个,不一定要用落叶的粒子,后面也可以自己去搓一个

属性抄橡木树叶,distance、persistent、waterlogged 这些状态就都有了

树的配置

新建 ModConfiguredFeatures

1
2
3
4
5
6
7
public class ModConfiguredFeatures {
public static final ResourceKey<ConfiguredFeature<?, ?>> ICE_ETHER_TREE = createKey("ice_ether_tree");

public static ResourceKey<ConfiguredFeature<?, ?>> createKey(final String name) {
return ResourceKey.create(Registries.CONFIGURED_FEATURE, Identifier.fromNamespaceAndPath(TutorialMod.MOD_ID, name));
}
}

这里的createKey是原版FeatureUtils.createKey直接copy过来的,当然要加上我们自己的命名空间

树本身

1
2
3
4
5
6
7
8
9
private static TreeConfiguration.TreeConfigurationBuilder createIceEtherTree() {
return new TreeConfiguration.TreeConfigurationBuilder(
BlockStateProvider.simple(ModBlocks.ICE_ETHER_LOG),
new StraightTrunkPlacer(4, 2, 1),
BlockStateProvider.simple(ModBlocks.ICE_ETHER_LEAVES),
new BlobFoliagePlacer(ConstantInt.of(3), ConstantInt.of(0), 3),
new TwoLayersFeatureSize(1, 0, 2)
);
}

原木和树叶都换成我们自己的,其余照抄橡树

树叶半径给 3(橡树是 2),这样一棵树的叶子会大一圈,看着更厚实

TreeGrower

1
2
3
4
5
6
7
public class ModTreeGrower {
public static final TreeGrower ICE_ETHER_TREE = new TreeGrower(
TutorialMod.MOD_ID + ":ice_ether_tree",
Optional.empty(),
Optional.of(ModConfiguredFeatures.ICE_ETHER_TREE),
Optional.empty());
}

四个参数:名字(随便取,只是调试用)、大型树、普通树、带花树

我们只有「普通树」,所以第二个和第四个都是 Optional.empty()

树苗

1
2
3
public static final Block ICE_ETHER_TREE_SAPLING = register("ice_ether_tree_sapling",
p -> new SaplingBlock(ModTreeGrower.ICE_ETHER_TREE, p),
BlockBehaviour.Properties.ofFullCopy(Blocks.OAK_SAPLING), true);

SaplingBlock 两个参数:生成器、方块属性

属性抄橡木树苗,这样它会有 stage 状态,也会随机刻生长、也要摆在土上

记得加物品栏

1
output.accept(ModBlocks.ICE_ETHER_TREE_SAPLING);

数据生成

挂上动态注册表

ConfiguredFeature是数据包注册表,不是普通的游戏注册表

所以它走的同样是「动态注册」那条路,和我们之前做村民交易、唱片是一样的

首先我们要在 TutorialModDataGenerator 的 buildRegistry 里加一行

1
registryBuilder.add(Registries.CONFIGURED_FEATURE, ModConfiguredFeatures::bootstrap);

随后在 ModDynamicRegistryProvider 的 configure 里加一行

1
entries.addAll(registries.lookupOrThrow(Registries.CONFIGURED_FEATURE));

这两步少一个,跑完数据生成,worldgen 目录就是空的

生成出来在

1
data/tutorial/worldgen/configured_feature/ice_ether_tree.json

里面能看到 type: minecraft:tree,树干、树叶分别指向我们的方块

模型

树叶

1
blockModelGenerators.createTrivialBlock(ModBlocks.ICE_ETHER_LEAVES, TexturedModel.LEAVES);

TexturedModel.LEAVES 对应的父模型是 minecraft:block/leaves,那个模型的面没有 cullface,所以从外面看不会有剔除问题

树苗

1
2
blockModelGenerators.createCrossBlockWithDefaultItem(ModBlocks.ICE_ETHER_TREE_SAPLING,
BlockModelGenerators.PlantType.NOT_TINTED);

树苗是十字模型,用 createCrossBlockWithDefaultItem 一行搞定方块状态 + 方块模型 + 物品模型

PlantType 有三个值:TINTED(染色的,比如草)、NOT_TINTED(不染色)、EMISSIVE_NOT_TINTED(自发光)

战利品

树叶要改成「掉树苗」

1
2
3
4
add(ModBlocks.ICE_ETHER_LEAVES,
createLeavesDrops(ModBlocks.ICE_ETHER_LEAVES, ModBlocks.ICE_ETHER_TREE_SAPLING,
NORMAL_LEAVES_SAPLING_CHANCES));
dropSelf(ModBlocks.ICE_ETHER_TREE_SAPLING);

createLeavesDrops 会帮你写出「剪刀掉树叶、时运掉更多树苗」那一整套,NORMAL_LEAVES_SAPLING_CHANCES 是原版给的那组概率

树苗就直接掉自己

标签

1
2
3
4
valueLookupBuilder(BlockTags.LEAVES)
.add(ModBlocks.ICE_ETHER_LEAVES);
valueLookupBuilder(BlockTags.SAPLINGS)
.add(ModBlocks.ICE_ETHER_TREE_SAPLING);

物品标签里也要加树苗

1
2
valueLookupBuilder(ItemTags.SAPLINGS)
.add(ModBlocks.ICE_ETHER_TREE_SAPLING.asItem());

树叶进 #leaves 之后,树叶才会被当成树叶(比如树叶的衰减、剪刀判定这些)

贴图

树叶放 textures/block/ice_ether_leaves.png

树苗也放 textures/block/ice_ether_tree_sapling.png

这里容易踩坑:树苗虽然是个「物品」,但贴图得放 block 下面

因为树的十字模型和物品模型,都是按 block/方块名 去找贴图的

放 item 目录里,方块世界里会变成紫黑,物品栏也许正常,看着特别迷惑

测试

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

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

  • 树叶是实心黑块:贴图透明度丢了,或者模型没用 TexturedModel.LEAVES
  • 树叶颜色不对、发灰:用了 TintedParticleLeavesBlock,它会被生物群系染色,换成 UntintedParticleLeavesBlock
  • 树叶一直不掉:distance 和 persistent 状态没继承下来,属性抄错方块了
  • 树苗放不下去:SaplingBlock 只能种在 #dirt 这类方块上,普通石头不行
  • 用骨粉催没反应,或者树苗直接把骨粉吃了:TreeGrower 里的 Optional 给空了,或者注册键拼错
  • 数据生成后 worldgen 目录是空的:buildRegistry 或 DataDynamicRegistryProvider 里少写了一步
  • 游戏启动报找不到 ice_ether_tree:configure 里没 addAll(CONFIGURED_FEATURE),数据没写进 jar
  • 树长出来是原版的橡树:BlockStateProvider.simple 里传的还是 Blocks.OAK_LOG
  • 树苗是紫黑方块:贴图放到了 textures/item/ 下面,应该放 textures/block/
  • 树苗模型是歪的/只有一半:PlantType 用了 TINTED,换成 NOT_TINTED