# 從頭開始種下一顆樹
若有誤或不明白的導方歡迎討論,謝謝!
*****
## 要做些什么?
因為我也只是在學習,所有就只做一個普普通通的樹,沒有任何添加。可知主要是由樹干、樹冠組成。同時,也少不了最初的樹苗。因為我們要做紫檀樹,也就是說我們要做的方塊有:紫檀樹苗、紫檀原木和紫檀樹葉。
## 紫檀樹苗
一棵參天大樹,無一不是自從樹苗(種子)開始的。我的世界也一樣,不過要先有一個確保可用的開發環境,我這里是 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()`?對之設置生成概率。快進游戲打開新世界吧~
*****
全部由糖果編寫,創造不易,你的支持是我最大的動力,謝謝!