# 从头开始种下一颗树
若有误或不明白的导方欢迎讨论,谢谢!
*****
## 要做些什么?
因为我也只是在学习,所有就只做一个普普通通的树,没有任何添加。可知主要是由树干、树冠组成。同时,也少不了最初的树苗。因为我们要做紫檀树,也就是说我们要做的方块有:紫檀树苗、紫檀原木和紫檀树叶。
## 紫檀树苗
一棵参天大树,无一不是自从树苗(种子)开始的。我的世界也一样,不过要先有一个确保可用的开发环境,我这里是 Fabric 1.16.5 + IDEA。然后开始编写你的代码吧!我们要做的就是学习官方WIKI、文档与看源码,以及自己写代码。在源码中的橡木树苗大致是这样的:
```
public static final Block OAK_SAPLING = register("oak_sapling", new SaplingBlock(new OakSaplingGenerator(),AbstractBlock.Settings.of(Material.PLANT).noCollision().ticksRandomly().breakInstantly().sounds(BlockSoundGroup.GRASS)));
```
可以看到源码使用了`register()`,即注册,首个参数是方块ID,其次为欲注册的方块,实际上此方法就是返回了第二个参数方块,也就是说看`SaplingBlock`类就好了,可是看构造方法竟然是子类可用(protected),也就是说我们要自己写个类并继承`SaplingBlock`后再实例化。构造方法声明:`protected SaplingBlock(SaplingGenerator generator, AbstractBlock.Settings settings)`
```
public class RedSandalwoodSaplingBlock extends SaplingBlock {
public RedSandalwoodSaplingBlock() {
super(new RedSandalwoodSaplingGenerator(),AbstractBlock.Settings.of(Material.PLANT).noCollision().ticksRandomly().breakInstantly().sounds(BlockSoundGroup.GRASS));
}
}
```
在这里构造器的第二个参数`settings`可能和官方教程有出入,官方写的是`FabricBlockSettings.copyOf(Blocks.OAK_SAPLING.getDefaultState())`,但在1.16.5中并不正确,参数类型为`Block`而非`BlockState`,故使用原版的方式`AbstractBlock.Settings.of(Material.PLANT).noCollision().ticksRandomly().breakInstantly().sounds(BlockSoundGroup.GRASS)`。`RedSandalwoodSaplingGenerator`是什么,后面便会讲解。然后实例化该方块。
```
public class Blocks {
public static final Block RED_SANDALWOOD_SAPLING = new RedSandalwoodSaplingBlock();
//...
}
```
包结构参考源码就好,在模组入口类`onInitialize`时注册`Registry.register(Registry.BLOCK, id, block)`就不讲了,官方挺详细的。
## 紫檀树苗的成长
众所周知,树苗会随游戏时间的流逝而成长为大树,用骨粉还可以加快成长,那么长成的大树是如何选择的呢?还记得刚刚的`RedSandalwoodSaplingGenerator`吗?这将用于决定树苗是否可以长大成树欲大树的形状特征。看前面橡木树苗源码,传入了一个`OakSaplingGenerator`对象,在此类的源码中有一个 createTreeFeature 方法,这便是最主要的了。
```
@Nullable
protected ConfiguredFeature<TreeFeatureConfig, ?> createTreeFeature(Random random, boolean bl) {
if (random.nextInt(10) == 0) {
return bl ? ConfiguredFeatures.FANCY_OAK_BEES_005 : ConfiguredFeatures.FANCY_OAK;
} else {
return bl ? ConfiguredFeatures.OAK_BEES_005 : ConfiguredFeatures.OAK;
}
}
```
看完代码,可看出该方法需要一个`ConfiguredFeature<TreeFeatureConfig, ?>`类型的返回值,而这就是长成后大树的外形特征了,配合参数`random`返回,即可达到随机效果;至于第二个参数,从下面语句中可看出是用于控制是否生成蜂巢的逻辑值。注意,虽然这里可用返回NULL,但这无需再用`random`判断是否要成长,即使你直接返回了`ConfiguredFeatures.OAK`,而没有其他判断,此树苗生长更新时(或使用骨粉催熟)也会自行判断是否成长,若为真就会调用此方法。接下来来看下`ConfiguredFeature`是什么。在前面的`createTreeFeature`方法的返回值中可知,我们只要去`ConfiguredFeatures`类看看其`OAK`的值是怎样的。
```
public static final ConfiguredFeature<TreeFeatureConfig, ?> OAK = register( "oak", Feature.TREE.configure(
(new TreeFeatureConfig.Builder(
new SimpleBlockStateProvider(ConfiguredFeatures.States.OAK\_LOG),
new SimpleBlockStateProvider(ConfiguredFeatures.States.OAK\_LEAVES),
new BlobFoliagePlacer(UniformIntDistribution.of(2), UniformIntDistribution.of(0), 3),
new StraightTrunkPlacer(4, 2, 0),
new TwoLayersFeatureSize(1, 0, 1)
)).ignoreVines().build()));
```
可以看出大树的外形特征就在于`TreeFeatureConfig.Builder`这个对象,看它的构造参数`public Builder(BlockStateProvider trunkProvider, BlockStateProvider leavesProvider, FoliagePlacer foliagePlacer, TrunkPlacer trunkPlacer, FeatureSize minimumSize)`,这与现在WIKI上的并不一样,让我们看下各个参数与其值:
* `BlockStateProvider trunkProvider`:树干提供者;即树干的方块,一般提供一个 `SimpleBlockStateProvider` 对象就可以了,其构造器也很简单,只需要一个 `BlockState`,从上面代码来看是位于 `ConfiguredFeatures` 下 `States` 中的 `OAK_LOG`,而其值为 `Blocks.OAK_LOG.getDefaultState()`;则此参数传入 `block.getDefaultState()` 即可。
* `BlockStateProvider leavesProvider`:树叶提供者;即树的叶子方块,见前一参数。
* `FoliagePlacer foliagePlacer`:树叶放置者;即树冠的形状,上面代码中是传入了一个 `BlobFoliagePlacer` 对象,即普通树叶外形;以下是可用的类型:
| 类名 | 描述 | 构造器描述 |
| --- | --- | --- |
| `AcaciaFoliagePlacer` | 金合欢树叶外形 | `UniformIntDistribution radius`:半径<br>`UniformIntDistribution offset`:相对于树干的偏移 |
| `BlobFoliagePlacer`(`BushFoliagePlacer`, `LargeOakFoliagePlacer`)| 普通树叶外形(灌木叶外形、大型橡木叶外形) | `UniformIntDistribution radius`<br>`UniformIntDistribution offset`<br>`int height`:高度 |
| `DarkOakFoliagePlacer` | 深色橡树叶外形 | `UniformIntDistribution radius`<br>`UniformIntDistribution offset` |
| `JungleFoliagePlacer` | 丛林树叶外形 | `UniformIntDistribution radius`<br>`UniformIntDistribution offset`<br>`int height` |
| `MegaPineFoliagePlacer` | 大型松树叶外形 | `UniformIntDistribution radius`<br>`UniformIntDistribution offset`<br>`UniformIntDistribution crownHeight`:树冠高度 |
| `PineFoliagePlacer` | 松树叶外形 | `UniformIntDistribution radius`<br>`UniformIntDistribution offset`<br>`int height` |
| `SpruceFoliagePlacer` | 云杉叶外形 | `UniformIntDistribution radius`<br>`UniformIntDistribution offset`<br>`UniformIntDistribution trunkHeight`:树干高度 |
另外 `UniformIntDistribution` 即为均匀整数分布,具体参数值参考 `ConfiguredFeatures` 类源码即可,格式为 `UniformIntDistribution.of(value)`。
* `TrunkPlacer trunkPlacer`:树干放置者;即树干的规格,上面代码中是传入了一个 `StraightTrunkPlacer` 对象,即笔直树干外形;以下是可用的类型:
| 类名 | 描述 | 构造器描述 |
| --- | --- | --- |
| `DarkOakTrunkPlacer` | 深色橡树干外形 | `int baseHeight`:基础高度<br>`int firstRandomHeight`:第一随机高度?(源码为2)<br>`int secondRandomHeight`:第二随机高度?(源码为1)
| `ForkingTrunkPlacer` | 分叉树干外形 | `int baseHeight`<br>`int firstRandomHeight`(源码为2)<br>`int secondRandomHeight`(源码为2) |
| `GiantTrunkPlacer`(`MegaJungleTrunkPlacer`) | 大型树干外形(大型丛林树干外形)| `int baseHeight`<br>`int firstRandomHeight`(源码为2)<br>`int secondRandomHeight`(源码为14、19)|
| `LargeOakTrunkPlacer` | 大型橡树干外形 | `int baseHeight`<br>`int firstRandomHeight`(源码为13)<br>`int secondRandomHeight`(源码为0) |
| `StraightTrunkPlacer` | 笔直树干外形 | `int baseHeight`<br>`int firstRandomHeight`(源码为2……)<br>`int secondRandomHeight`(源码为0……) |
部分参数我也不大清楚,所有就直接写上了源码中的值,写的时候参考源码与WIKI即可。
* `FeatureSize minimumSize`:最小尺寸;即不同层的树木的宽度,用于查看树木在不卡到方块中可以有多高;以下是可用的类型:
| 类名 | 描述 | 构造器描述 |
| --- | --- | --- |
| `ThreeLayersFeatureSize` | 三层特征尺寸 | `int limit`:限制范围<br>`int upperLimit`:顶面限制范围<br>`int lowerSize`:底面尺寸<br>`int middleSize`:中间尺寸<br>`int upperSize`:顶面尺寸<br>`OptionalInt minClippedHeight`:最小高度 |
| `TwoLayersFeatureSize` | 两层特征尺寸 | `int limit`<br>`int lowerSize`<br>`int upperSize`<br>(`OptionalInt minClippedHeight`)|
目前我也不大明白意思,不过抄源码就好,比如普通橡树是 `new TwoLayersFeatureSize(1, 0, 1)`。了解后开始写自己的代码:
```
public class RedSandalwoodSaplingGenerator extends SaplingGenerator {
@Nullable
@Override
protected ConfiguredFeature<TreeFeatureConfig, ?> createTreeFeature(Random random, boolean bl) {
return ConfiguredFeatures.RED_SANDALWOOD;
}
}
```
```
public class ConfiguredFeatures {
public static final ConfiguredFeature<TreeFeatureConfig, ?> RED_SANDALWOOD;
private static <FC extends FeatureConfig> ConfiguredFeature<FC, ?> register(String id, ConfiguredFeature<FC, ?> configuredFeature) {
//...
return configuredFeature;
}
static {
RED_SANDALWOOD = register("red_sandalwood", Feature.TREE.configure((new TreeFeatureConfig.Builder(
new SimpleBlockStateProvider(States.RAD_SANDALWOOD_LOG),
new SimpleBlockStateProvider(States.RAD_SANDALWOOD_LEAVES),
new BlobFoliagePlacer(UniformIntDistribution.of(2),
UniformIntDistribution.of(0), 3),
new StraightTrunkPlacer(4, 2, 0),
new TwoLayersFeatureSize(1, 0, 1))).ignoreVines().build())
);
}
public static final class States {
protected static final BlockState RAD_SANDALWOOD_LOG;
protected static final BlockState RAD_SANDALWOOD_LEAVES;
static {
RAD_SANDALWOOD_LOG = Blocks.RAD_SANDALWOOD_LOG.getDefaultState();
RAD_SANDALWOOD_LEAVES = Blocks.RAD_SANDALWOOD_LEAVES.getDefaultState();
}
}
//...
}
```
注意,这中间省略了注册部分,不要忘记使用 `Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, id, configuredFeature)` 进行注册!
紫檀树之前做了那么多,终于盼到的树苗长成大树。那么前面代码报错的部分原木方块与树叶方块就开始创建吧!老样子,看看橡树源码:
```
OAK_LOG = register("oak_log", createLogBlock(MaterialColor.WOOD, MaterialColor.SPRUCE));
OAK_LEAVES = register("oak_leaves", createLeavesBlock());
```
其中原木方块是 `createLogBlock()`;树叶方块是 `createLeavesBlock()`。下面是其源码:
```
private static PillarBlock createLogBlock(MaterialColor topMaterialColor, MaterialColor sideMaterialColor) {
return new PillarBlock(AbstractBlock.Settings.of(Material.WOOD, (blockState) -> {
return blockState.get(PillarBlock.AXIS) == Direction.Axis.Y ? topMaterialColor : sideMaterialColor;
}).strength(2.0F).sounds(BlockSoundGroup.WOOD));
}
private static Boolean canSpawnOnLeaves(BlockState state, BlockView world, BlockPos pos, EntityType<?> type) {
return type == EntityType.OCELOT || type == EntityType.PARROT;}private static boolean never(BlockState state, BlockView world, BlockPos pos) {
return false;
}
private static LeavesBlock createLeavesBlock() {
return new LeavesBlock(AbstractBlock.Settings.of(Material.LEAVES).strength(0.2F).ticksRandomly().sounds(BlockSoundGroup.GRASS).nonOpaque().allowsSpawning(Blocks::canSpawnOnLeaves).suffocates(Blocks::never).blockVision(Blocks::never));
}
private static Block register(String id, Block block) {
return (Block)Registry.register(Registry.BLOCK, (String)id, block);
}
```
非常简单,直接抄下来就好。其中 `createLogBlock` 方法的参数是顶面颜色与侧面颜色,即不同放置方式在地图上显示的颜色,使用 `MaterialColor` 下的值即可。需要注意的是,源码在初始化常量值时顺便就已经注册了,但我们在这样做的时候一定要确保这个 `Blocks` 类被初始化了,不然可能就没有注册到游戏中且其他地方调用时会空指针,可以做一个待注册元素表,在模组初始化时遍历注册。下面是所有方块了,同样别忘了注册方块哦。
```
public class Blocks {
public static final Block RAD_SANDALWOOD_LOG;
public static final Block RAD_SANDALWOOD_LEAVES;
public static final Block RED_SANDALWOOD_SAPLING;
private static BooleancanSpawnOnLeaves(BlockState state, BlockView world, BlockPos pos, EntityType<?> type) {
return type == EntityType.OCELOT || type == EntityType.PARROT;
}
private static PillarBlock createLogBlock(MaterialColor topMaterialColor, MaterialColor sideMaterialColor) {
return new PillarBlock(AbstractBlock.Settings.of(Material.WOOD, (blockState) -> blockState.get(PillarBlock.AXIS) == Direction.Axis.Y ? topMaterialColor : sideMaterialColor).strength(2.0F)
.sounds(BlockSoundGroup.WOOD));
}
private static LeavesBlock createLeavesBlock() {
return new LeavesBlock(
AbstractBlock.Settings.of(Material.LEAVES).strength(0.2F).ticksRandomly().sounds(BlockSoundGroup.GRASS)
.nonOpaque().allowsSpawning(Blocks::canSpawnOnLeaves).suffocates(Blocks::never).blockVision(Blocks::never));
}
private static boolean never(BlockState state, BlockView world, BlockPos pos) {
return false;
}
private static Block register(String id, Block block) {
//...
return block;
}
static {
RAD_SANDALWOOD_LOG = register("red_sandalwood_log", createLogBlock(MaterialColor.ORANGE, MaterialColor.STONE));
RAD_SANDALWOOD_LEAVES = register("red_sandalwood_leaves", createLeavesBlock());
RED_SANDALWOOD_SAPLING = register("red_sandalwood_sapling", new RedSandalwoodSaplingBlock());
}
//...
}
```
做完这些后,就差写资源包了,这个也很简单,抄源码或查WIKI教程。然后开始运行游戏吧!不知道咋回事一直上传不了图片,反正造我这个做出来的是和普通橡树差不多的,那就你们直接去探索吧!
## 灰色的紫檀叶
看原版贴图时,可以发现树叶材质都是差不多灰白的,除了丛林树叶的几个小黄点,那么我们照源码这样画,这样才能为灰白图上色呢?因为我也不大清楚,所有我在此只做一个简单推测,若有误或其他方法,感谢指出。在源码中有一个 `net.minecraft.client.color.block.BlockColors` 类,而在其 create 方法中有一句:
```
blockColors.registerColorProvider((state, world, pos, tintIndex) -> {
return world != null && pos != null ? BiomeColors.getFoliageColor(world, pos) : FoliageColors.getDefaultColor();
}, Blocks.OAK_LEAVES, Blocks.JUNGLE_LEAVES, Blocks.ACACIA_LEAVES, Blocks.DARK_OAK_LEAVES, Blocks.VINE);
```
这应该就是决定树叶颜色的了,看下 `registerColorProvider` 方法声明
```
public void registerColorProvider(net.minecraft.client.color.block.BlockColorProvider provider, Block... blocks)
```
第一个参数是方块颜色提供器,如果世界、位置不为空则提供该生物群系的树叶颜色,否则提供默认树叶颜色;第二个便是将被应用到的方块了。还有 `net.minecraft.client.color.item.ItemColors` 类中 `create` 方法有句:
```
itemColors.register((stack, tintIndex) -> {
BlockState blockState = ((BlockItem)stack.getItem()).getBlock().getDefaultState();
return blockColors.getColor(blockState, (BlockRenderView)null, (BlockPos)null, tintIndex);}, Blocks.GRASS_BLOCK, Blocks.GRASS, Blocks.FERN, Blocks.VINE, Blocks.OAK_LEAVES, Blocks.SPRUCE_LEAVES, Blocks.BIRCH_LEAVES, Blocks.JUNGLE_LEAVES, Blocks.ACACIA_LEAVES, Blocks.DARK_OAK_LEAVES, Blocks.LILY_PAD);
```
这个是该方块物品在物品栏中的颜色,就是使用该方块的颜色,和前面的都差不多就不多说了。那么重要的是,我们要如何为自己的树叶方块提供颜色呢?我也不清楚有没有一些接口,不过可以用 Mixin 来解决这个问题,具体教程在耗子的博客有。使用 Inject 注解进行向其 create 方法注入我们的代码,同时还需要加上 locals 获取变量 `blockColors` 和 `itemColors`。然后是物品颜色,也一样,怕误导大家就不写了。最后别忘了加到 mixin 配置,具体教程见官方WIKI。或者,还有一种方法,直接用有色的树叶贴图。但这可能会使这棵树在不同生物群系种植时树叶颜色与之有明显差异,不过确实不用这么麻烦了。
## 在主世界自然生成紫檀树
在之前的测试中,都是我们自己种下树苗的,那么如何像原版橡树那样自然生成呢?看了下源码,发现都是固定的了,但在官方WIKI中发现了有这个接口:
```
net.fabricmc.fabric.api.biome.v1.BiomeModificationspublic static void addFeature(
java.util.function.Predicate<net.fabricmc.fabric.api.biome.v1.BiomeSelectionContext> biomeSelector,
net.minecraft.world.gen.GenerationStep.Feature step,
net.minecraft.util.registry.RegistryKey<net.minecraft.world.gen.feature.ConfiguredFeature<?, ?>> configuredFeatureKey
)
```
这是 `BiomeModifications` 类中的 `addFeature` 方法,但看源码后发现这是一个实验性方法,但是有的话就凑合用吧,让我们来看下官方WIKI用例:`BiomeModifications.addFeature(BiomeSelectors.foundInOverworld(), GenerationStep.Feature.VEGETAL_DECORATION, treeRich);`非常简单,看下参数:
* `biomeSelector`:生物群系选择器,决定这棵树将在那些生物群系生成;类型是匿名函数,且传入了一个 `BiomeSelectionContext` 值,例子中使用了 `BiomeSelectors.foundInOverworld()`,即主世界中所有生物群系,还有其它 `BiomeSelectors` 类下的方法可以选择:
| 方法名 | 描述 |
| --- | --- |
| `all()` | 匹配所有生物群系。如果可能,请使用更具体的选择器。|
| `builtIn()` | 匹配最初未在数据包中定义但在代码中定义的生物群系。|
| `vanilla()` | 匹配minecraft命名空间中的所有生物群系。|
| `foundInOverworld()` | 假如主世界维度启用则匹配所有通常会在主世界生成的生物群系。|
| `foundInTheNether()` | 假如下界维度启用则匹配所有通常会在下届生成的生物群系。 |
| `foundInTheEnd()` | 假如末地维度启用则匹配所有通常会在末地生成的生物群系。|
| `excludeByKey(RegistryKey<Biome>... keys)` | 见下。 |
| `excludeByKey(Collection<RegistryKey<Biome>> keys)` | 匹配除传入群系外的所有生物群系。|
| `includeByKey(RegistryKey<Biome>... keys)` | 见下。 |
| `includeByKey(Collection<RegistryKey<Biome>> keys)` | 匹配所有传入的生物群系。 |
| `spawnsOneOf(EntityType<?>... entityTypes)` | 见下。|
| `spawnsOneOf(Set<EntityType<?>> entityTypes)` | 匹配所有可生成传入生物类型的生物群系。|
| `categories(Biome.Category... categories)` | 匹配拥有传入的群系种类之一的生物群系。|
一般我们要自定义生物群系,用 `includeByKey` 就好了,参数值可以为 `net.minecraft.world.biome.BiomeKeys` 类下的常量值。
* `step`:因为树是作物装饰,所以用 `GenerationStep.Feature.VEGETAL_DECORATION` 就好了。
* `configuredFeatureKey`:欲添加的配置特征键,这里为 `RegistryKey<ConfiguredFeature<?, ?>>` 类型,其值使用 `RegistryKey.of(RegistryKey<? extends Registry<T>> registry, Identifier value)` 方法赋值即可。
了解了这些后,我们来将紫檀树加到主世界的森林群系与繁花森林群系中。还记得刚刚 ConfiguredFeatures 中的 `RED_SANDALWOOD` 吗?这是紫檀树的配置特征,一般在注册它的时候就可以调用这个 `addFeature` 方法以添加到世界生成了。下面是我的代码:
```
BiomeModifications.addFeature(
BiomeSelectors.includeByKey(BiomeKeys.FOREST, BiomeKeys.FLOWER_FOREST),
GenerationStep.Feature.VEGETAL_DECORATION,
RegistryKey.of(Registry.CONFIGURED_FEATURE_WORLDGEN, new Identifier("tutorial", "red_sandalwood"))
);
```
这就是最基础的世界生成了,还没有生成概率之类的属性设置,不过WIKI上好像有用 `.applyChance()` 对之设置生成概率。快进游戏打开新世界吧~
*****
全部由糖果编写,创造不易,你的支持是我最大的动力,谢谢!