本篇教程的视频:

本篇教程源代码

GitHub地址:TutorialMod-Tree-1.21

介绍

在游戏中是一个非常重要的元素,它们可以提供木材

应该也算是一种多方块结构(个人理解),它们是属于地物的(Feature),它们从一棵简单的树苗长成一棵大树,长出原木和树叶

前面我们也添加了一些原木树叶,本篇教程我们将添加,后面我们还将讲解树的世界生成

当然,因为现在的版本特性,世界生成的很多东西都是数据驱动的,这也就意味着,如果你会编写对应的json文件,同样也可以实现世界生成的一些东西

包括一些构造参数,也同样是通过json文件来实现的,我们写的一堆代码,除了必要的注册外,其他的都是为后面的数据生成做铺垫的

本篇教程和视频教程会有所区别,因为视频教程把后面的世界生成也讲了,而这里只是讲解树的添加

查看源代码

这里我们去看TreeConfiguredFeatures类,这个类是写了树的构造特征的,也就是这棵树它长什么样子

其实我们后面还会接触到其他的ConfiguredFeatures,感兴趣的可以去看看

1
2
3
...
public static final RegistryKey<ConfiguredFeature<?, ?>> OAK = ConfiguredFeatures.of("oak");
...

首先我们可以看到一堆的RegistryKey,这些RegistryKeyConfiguredFeature的,它们是树的注册键

1
2
3
public static RegistryKey<ConfiguredFeature<?, ?>> of(String id) {
return RegistryKey.of(RegistryKeys.CONFIGURED_FEATURE, Identifier.ofVanilla(id));
}

注册方法是这里的这个,但是我们也要自己更改一下命名空间,不能直接调用

我们继续往下看,可以看到一些具体的树的构造特征

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
private static TreeFeatureConfig.Builder builder(Block log, Block leaves, int baseHeight, int firstRandomHeight, int secondRandomHeight, int radius) {
return new TreeFeatureConfig.Builder(
BlockStateProvider.of(log),
new StraightTrunkPlacer(baseHeight, firstRandomHeight, secondRandomHeight),
BlockStateProvider.of(leaves),
new BlobFoliagePlacer(ConstantIntProvider.create(radius), ConstantIntProvider.create(0), 3),
new TwoLayersFeatureSize(1, 0, 1)
);
}

private static TreeFeatureConfig.Builder oak() {
return builder(Blocks.OAK_LOG, Blocks.OAK_LEAVES, 4, 2, 0, 2).ignoreVines();
}

private static TreeFeatureConfig.Builder birch() {
return builder(Blocks.BIRCH_LOG, Blocks.BIRCH_LEAVES, 5, 2, 0, 2).ignoreVines();
}

private static TreeFeatureConfig.Builder superBirch() {
return builder(Blocks.BIRCH_LOG, Blocks.BIRCH_LEAVES, 5, 2, 6, 2).ignoreVines();
}

private static TreeFeatureConfig.Builder jungle() {
return builder(Blocks.JUNGLE_LOG, Blocks.JUNGLE_LEAVES, 4, 8, 0, 2);
}

我们看这几个树的构造特征,这里有橡树白桦树高白桦树丛林树,它们调用的是同一个builder方法

我们再看看这个builder方法

TreeFeatureConfig.Builder方法前两个是原木树干的生成器,这里的StraightTrunkPlacer是直树干生成器,上面的四个树都是直树干(当然橡树存在变种,但变种也是单独写的)

StraightTrunkPlacer的参数是基础高度 baseHeight第一个随机高度 firstRandomHeight第二个随机高度 secondRandomHeight,这里的基础高度是树的最小高度,第一个随机高度是在前者的基础上随机增加的高度,第二个随机高度是在第一个随机高度的基础上再随机增加的高度

第三个是树叶,第四个是树叶生成器,这里的BlobFoliagePlacer是其中一种树叶生成器,斑点状的树叶(应该是一团)

BlobFoliagePlacer的参数是半径 radius偏移 offset高度 height,这里的半径是树叶的半径,偏移是树叶的偏移量(相当于主干而言),高度是树叶的厚度

最后一个是树的最小尺寸,更准确来讲是树的外形特征,根据可生长空间的大小,树的外形会有所不同(具体可进一步深挖)

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
private static TreeFeatureConfig.Builder fancyOak() {
return new TreeFeatureConfig.Builder(
BlockStateProvider.of(Blocks.OAK_LOG),
new LargeOakTrunkPlacer(3, 11, 0),
BlockStateProvider.of(Blocks.OAK_LEAVES),
new LargeOakFoliagePlacer(ConstantIntProvider.create(2), ConstantIntProvider.create(4), 4),
new TwoLayersFeatureSize(0, 0, 0, OptionalInt.of(4))
)
.ignoreVines();
}

private static TreeFeatureConfig.Builder cherry() {
return new TreeFeatureConfig.Builder(
BlockStateProvider.of(Blocks.CHERRY_LOG),
new CherryTrunkPlacer(
7,
1,
0,
new WeightedListIntProvider(
DataPool.<IntProvider>builder().add(ConstantIntProvider.create(1), 1).add(ConstantIntProvider.create(2), 1).add(ConstantIntProvider.create(3), 1).build()
),
UniformIntProvider.create(2, 4),
UniformIntProvider.create(-4, -3),
UniformIntProvider.create(-1, 0)
),
BlockStateProvider.of(Blocks.CHERRY_LEAVES),
new CherryFoliagePlacer(ConstantIntProvider.create(4), ConstantIntProvider.create(0), ConstantIntProvider.create(5), 0.25F, 0.5F, 0.16666667F, 0.33333334F),
new TwoLayersFeatureSize(1, 0, 2)
)
.ignoreVines();
}

这里还有一些特殊的树,比如深色橡树樱花树,它们的生成器也是不同的,比如LargeOakTrunkPlacerCherryTrunkPlacer,它们的参数也是不同的

同样的,你也可以在实际开发过程中尝试自定义树干和树叶的生成器,也可以参考其他生物群系相关的模组

另外,我们还可以看到一个bootstrap,显然易见,待会数据生成得用上

1
ConfiguredFeatures.register(featureRegisterable, OAK, Feature.TREE, oak().build());

里面其实写的就是前面写的builder方法,这里的oak().build()就是橡树的构造特征

当然还有其他一堆东西,这里就不一一赘述了,感兴趣的可以自行研究

添加树

接下来我们就来写我们自己的树的构造特征

创建ModConfiguredFeatures类

原版的矿物啥的构造特征类其实都是分开写的,我们也可以合在一起写,这里我们创建一个ModConfiguredFeatures

1
2
3
public class ModConfiguredFeatures {

}

我们先把前面看到的of方法搬过来,并改写命名空间

1
2
3
public static RegistryKey<ConfiguredFeature<?, ?>> of(String id) {
return RegistryKey.of(RegistryKeys.CONFIGURED_FEATURE, Identifier.of(TutorialMod.MOD_ID, id));
}

而后,我们就可以写树构造特征的注册键了

1
public static final RegistryKey<ConfiguredFeature<?, ?>> ICE_ETHER_TREE_KEY = of("ice_ether_tree");

然后我们直接来写bootstrap方法,并在这里面直接写我们的树的构造特征

1
2
3
4
5
6
7
8
9
public static void bootstrap(Registerable<ConfiguredFeature<?, ?>> featureRegisterable) {
ConfiguredFeatures.register(featureRegisterable, ICE_ETHER_TREE_KEY, Feature.TREE, new TreeFeatureConfig.Builder(
BlockStateProvider.of(ModBlocks.ICE_ETHER_LOG),
new StraightTrunkPlacer(4, 3, 2),
BlockStateProvider.of(ModBlocks.ICE_ETHER_LEAVES),
new BlobFoliagePlacer(ConstantIntProvider.create(4), ConstantIntProvider.create(2), 2),
new TwoLayersFeatureSize(1, 0, 2)
).build());
}

视频教程中我们是把ConfiguredFeatures中的register方法给搬过来了,其实不用搬,因为它是public的,也没有要改的东西

TreeFeatureConfig那部分其实等同于上面的oak(),你也可以像原版那样给它单独拿出来写

我们的树按照橡树的写法,改了一些数值

各个参数上面也解释过了,这里就不再赘述

创建ModTreeGenerator类

视频教程多讲了一个ModPlacedFeatures类,这个类是用来放置特征的,本篇教程我们就不讲了,在后面一篇世界生成中讲

我们直接来写ModTreeGenerator类,这个类是用来生成树的,也就是让树苗长大成为一棵大树

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

这个类和1.20里面的又不太一样,1.20中我们还是去继承了SaplingGenerator类,但在这里我们实例化SaplingGenerator

SaplingGenerator的参数分别是注册名 id巨型变种 megaVariant标准体 regularVariant带蜂巢变种 beesVariant

这里我们只用到了注册名标准体巨型变种带蜂巢变种我们都是Optional.empty(),也就是空的,因为我们没有编写相关的内容

当然,我们这里用的只是其中一个SaplingGenerator的构造函数,还有其他的构造函数,感兴趣的可以自行研究

1
2
3
4
5
6
7
8
9
10
public static final SaplingGenerator OAK = new SaplingGenerator(
"oak",
0.1F,
Optional.empty(),
Optional.empty(),
Optional.of(TreeConfiguredFeatures.OAK),
Optional.of(TreeConfiguredFeatures.FANCY_OAK),
Optional.of(TreeConfiguredFeatures.OAK_BEES_005),
Optional.of(TreeConfiguredFeatures.FANCY_OAK_BEES_005)
);

比如橡树,就有一堆变种,它用的构造函数参数更多

注册树苗

那么接下来就是注册树苗了,毕竟我们要有树得先有树苗

1
2
public static final Block ICE_ETHER_SAPLING = register("ice_ether_tree_sapling",
new SaplingBlock(ModTreeGenerator.ICE_ETHER_TREE, AbstractBlock.Settings.copy(Blocks.OAK_SAPLING)));

这里我们注册了一个ICE_ETHER_SAPLING,实例化SaplingBlock,它的生成器是我们前面写的ICE_ETHER_TREE,后面是Settings

加入物品栏

不要忘了将我们的树苗加入物品栏

1
entries.add(ModBlocks.ICE_ETHER_TREE_SAPLING);

渲染层设置

最后我们还要设置渲染层,让树苗能够正常显示(一般都和我们的作物一样,带有透明通道的吧)

1
BLOCKS.put(ModBlocks.ICE_ETHER_SAPLING, RenderLayer.getCutout());

上面是我们写的Mixin

也可以写在客户端类的onInitializeClient方法中

1
BlockRenderLayerMap.INSTANCE.putBlock(ModBlocks.ICE_ETHER_SAPLING, RenderLayer.getCutout());

数据文件

数据生成

上面写的bootstrap方法,不要忘了在数据生成类的buildRegistry调用

1
registryBuilder.addRegistry(RegistryKeys.CONFIGURED_FEATURE, ModConfiguredFeatures::bootstrap);

创建ModWorldGen类

另外我们还要创建一个ModWorldGen类,这个是动态注册的,从它继承的类就能看出来

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ModWorldGen extends FabricDynamicRegistryProvider {
public ModWorldGen(FabricDataOutput output, CompletableFuture<RegistryWrapper.WrapperLookup> registriesFuture) {
super(output, registriesFuture);
}

@Override
protected void configure(RegistryWrapper.WrapperLookup registries, Entries entries) {
entries.addAll(registries.getWrapperOrThrow(RegistryKeys.CONFIGURED_FEATURE));
}

@Override
public String getName() {
return "World Gen Data Provider";
}
}

继承FabricDynamicRegistryProvider,重写getName方法

随后我们在数据生成类中调用

1
pack.addProvider(ModWorldGen::new);

语言文件

1
translationBuilder.add(ModBlocks.ICE_ETHER_SAPLING, "Ice Ether Sapling");

模型文件

1
blockStateModelGenerator.registerTintableCross(ModBlocks.ICE_ETHER_SAPLING, BlockStateModelGenerator.TintType.NOT_TINTED);

这里我们用到了BlockStateModelGeneratorregisterTintableCross方法,十字型的方块模型,当然,它不可以被染色

而后我们就可以跑数据生成了,之后我们就可以在worldgen文件夹里面找到生成的一个构造特征的json文件

启动游戏继续测试吧