This is an old revision of the document!
Table of Contents
添加树木 [1.17](高级)
阅读本文之前,建议先学习如何创建一个特征地形。
参见 features
树木是在你的 mod 中拓展原版世界生成的一个好方法。
注意本话题较为高级,因此开始之前,最好要有关于修改世界生成的丰富经验。
创建简单的树木
结构
原版的树的结构分为不同的种类,方便你做出复杂并且漂亮的树。
概览如下:
TrunkPlacer:生成树干。FoliagePlacer:生成树叶。SaplingGenerator:根据周围环境,从树苗产生树的ConfiguredFeature。TreeDecorator:可以用这个来为树生成额外的元素,如蜂箱、藤蔓(可选)。BlockStateProvider:根据周围环境返回方块。如果需要让树的一部分是A方块,另一部分是B方块,就可以使用这个函数。
如果想让树木长得不那么像是原版的树木,可以选择创建自定义的实现。但是实际上原版的实现通常足够模组的开发了。
创建 ConfiguredFeature
不需要创建新的 Feature ,因为原版的 TreeFeature 是可以配置的。
把这个添加到你的 ModInitializer 主体:
public static final ConfiguredFeature<?, ?> TREE_RICH = Feature.TREE // 使用builder配置特征地形 .configure(new TreeFeatureConfig.Builder( new SimpleBlockStateProvider(Blocks.NETHERITE_BLOCK.getDefaultState()), // 树干方块提供器 new StraightTrunkPlacer(8, 3, 0), // 放置竖直树干 new SimpleBlockStateProvider(Blocks.DIAMOND_BLOCK.getDefaultState()), // 树叶方块提供器 new SimpleBlockStateProvider(RICH_SAPLING.getDefaultState()), // 树苗提供器,用来决定树木可以生长在什么方块上 new BlobFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), 3), // 生成水滴状的树叶(半径、相对于树干的偏移、高度) new TwoLayersFeatureSize(1, 0, 1) // 不同层的树木的宽度,用于查看树木在不卡到方块中可以有多高 ).build()) .spreadHorizontally() .applyChance(3); // 每个区块大约33%的概率生成(1/x)
现在只需要像往常那样向游戏注册 ConfiguredFeature 然后用 FabricAPI 修改生物群系:
@Override public void onInitialize() { RegistryKey<ConfiguredFeature<?, ?>> treeRich = RegistryKey.of(Registry.CONFIGURED_FEATURE_KEY, new Identifier("tutorial", "tree_rich")); Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, treeRich.getValue(), TREE_RICH); // 应该为树木使用 VEGETAL_DECORATION 生成步骤 BiomeModifications.addFeature(BiomeSelectors.foundInOverworld(), GenerationStep.Feature.VEGETAL_DECORATION, treeRich); }
创建树苗
树苗是生长树木的一类特殊方块,需要 SaplingGenerator 。
创建SaplingGenerator
简单的生成器接收树木的ConfiguredFeature并将其返回,如下所示:
public class RichSaplingGenerator extends SaplingGenerator { private final ConfiguredFeature<TreeFeatureConfig, ?> feature; public RichSaplingGenerator(ConfiguredFeature<?, ?> feature) { this.feature = (ConfiguredFeature<TreeFeatureConfig, ?>) feature; } @Nullable @Override protected ConfiguredFeature<TreeFeatureConfig, ?> getTreeFeature(Random random, boolean bees) { return feature; } }
后面会展示高级的SaplingGenerator的例子。
创建SaplingBlock
创建方块本身需要继承SaplingBlock类,而不是直接将其实例化,因为其构造器的访问权限是protected的。
public class RichSaplingBlock extends SaplingBlock { public RichSaplingBlock(SaplingGenerator generator, Settings settings) { super(generator, settings); } }
注册SaplingBlock
要注册树苗,按照注册方块的以下步骤(参见blocks),但传入带有ConfiguredFeature的生成器的实例。
把这个放在用于你的树苗方块的类中:
public static final RICH_SAPLING = new RichSaplingBlock(new RichSaplingGenerator(TREE_RICH), FabricBlockSettings.copyOf(Blocks.OAK_SAPLING.getDefaultState())); public static void register() { Registry.register(Registry.BLOCK, new Identifier("tutorial", "rich_sapling"), RICH_SAPLING); Registry.register(Registry.ITEM, new Identifier("tutorial", "rich_sapling"), new BlockItem(RICH_SAPLING, ItemGroup.MISC)); }
创建 TrunkPlacer
TrunkPlacer 创建由 BlockStateProvider 提供的树干方块。
原版 TrunkPlacers
在创建 TrunkPlacer 之前,先看看可以从原版复用的 TrunkPlacer 避免做重复的工作:
StraightTrunkPlacerForkingTrunkPlacerGiantTrunkPlacerBendingTrunkPlacer
创建TrunkPlacerType
往游戏注册TrunkPlacer需要TrunkPlacerType。
可惜Fabric API目前没有用于创建和注册TrunkPlacer的API,所以我们需要使用mixins。
我们准备创建一个调用器(见https://github.com/2xsaiko/mixin-cheatsheet/blob/master/invoker.md)来调用私有静态的TrunkPlacerType.register方法。
以下是我们的mixin,不要忘记加到mixin配置中:
@Mixin(TrunkPlacerType.class) public interface TrunkPlacerTypeInvoker { @Invoker static <P extends TrunkPlacer> TrunkPlacerType<P> callRegister(String id, Codec<P> codec) { throw new IllegalStateException(); } }
创建 TrunkPlacer
TrunkPlacer 包含:
- 用于序列化的编码解码器。编码解码器(codec)是其自己的话题(topic),这里我们只需要使用
fillTrunkPlacerFields方法来生成。 - 获取器(getter),返回
TrunkPlacerType。 generate方法,该方法中放置树干并返回TreeNode列表,用于树叶放置器放置树木。
TrunkPlacer 将在世界中创建两个对角线形的树干:
public class RichTrunkPlacer extends TrunkPlacer { // 使用fillTrunkPlacerFields来创建编码解码器 public static final Codec<RichTrunkPlacer> CODEC = RecordCodecBuilder.create(instance -> fillTrunkPlacerFields(instance).apply(instance, RichTrunkPlacer::new)); public RichTrunkPlacer(int baseHeight, int firstRandomHeight, int secondRandomHeight) { super(baseHeight, firstRandomHeight, secondRandomHeight); } @Override protected TrunkPlacerType<?> getType() { return Tutorial.RICH_TRUNK_PLACER; } @Override public List<FoliagePlacer.TreeNode> generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, int height, BlockPos startPos, TreeFeatureConfig config) { // 将树干下的方块设为泥土 this.setToDirt(world, replacer, random, startPos.down(), config); // 迭代到树干高度限制,并使用 TrunkPlacer 中的 getAndSetState 方法放置两个方块 for (int i = 0; i < height; i++) { this.getAndSetState(world, replacer, random, startPos.up(i), config); this.getAndSetState(world, replacer, random, startPos.up(i).east().north(), config); } // 创建两个树木节点——一个用于第一个树干,另一个用于第二个 // 将树干中最高的方块设为中心坐标给 FoliagePlacer 使用 return ImmutableList.of(new FoliagePlacer.TreeNode(startPos.up(height), 0, false), new FoliagePlacer.TreeNode(startPos.east().north().up(height), 0, false)); } }
注册并使用TrunkPlacer
使用你的调用器,为你的 TrunkPlacer 创建并注册 TrunkPlacerType 的实例。把这个放到你的 ModInitializer 主体中:
public static final TrunkPlacerType<RichTrunkPlacer> RICH_TRUNK_PLACER = TrunkPlacerTypeInvoker.callRegister("rich_trunk_placer", RichTrunkPlacer.CODEC);
现在将你的 StraightTrunkPlacer 替换为你刚创建的 RichTrunkPlacer 就好了:
[...] new RichTrunkPlacer(8, 3, 0), [...]
创建FoliagePlacer
FoliagePlacer 会从由 BlockStateProvider 提供的方块创建树叶,如果你提供钻石矿方块的话或许会变成钻石矿树叶。竟然会如此的闪耀!
原版FoliagePlacer
创建 FoliagePlacer 之前,看看可直接使用的原版 FoliagePlacer 以避免重复造轮子:
BlobFoliagePlacerBushFoliagePlacerRandomSpreadFoliagePlacer
创建 FoliagePlacerType
往游戏中注册 FoliagePlacer 需要 FoliagePlacerType 。
和 TrunkPlacerType 类似,FabricAPI 不提供创建 FoliagePlacerType 的实用功能。我们的 mixin 看上去几乎相同,同时不要忘记修改你的 mixin 配置!
@Mixin(FoliagePlacerType.class) public interface FoliagePlacerTypeInvoker { @Invoker static <P extends FoliagePlacer> FoliagePlacerType<P> callRegister(String id, Codec<P> codec) { throw new IllegalStateException(); } }
创建 FoliagePlacer
FoliagePlacer 比 TrunkPlacer 更加复杂一些,包括:
- 用于序列化的编码解码器。在此例中我们展示了如何往编码解码器中添加一个额外的 IntProvider。
- 用于获取
FoliagePlacerType的获取器(getter)。 generate方法,该方法创建树叶。getRandomHeight方法。不管名字是什么,你通常应该返回你的树叶的最大高度。isInvalidForLeaves方法,可以为放置树叶的地方设置限制。
我们的 FoliagePlacer 会往各个方向(东南西北)创建4行的树叶方块:
public class RichFoliagePlacer extends FoliagePlacer { // 对于foliageHeight我们使用由 IntProvider.createValidatingCodec 生成的编码解码器 // 方法参数,我们传入 IntProvider 的最小值和最大值 // 为了向你的 TrunkPlacer/FoliagePlacer/TreeDecorator 等添加多个域(fields),可调用多次.and。 // // 如果想创建属于我们自己的编码解码器类型,可以参考 IntProvider.createValidatingCodec 方法的源代码。 public static final Codec<RichFoliagePlacer> CODEC = RecordCodecBuilder.create(instance -> fillFoliagePlacerFields(instance) .and(IntProvider.createValidatingCodec(1, 512).fieldOf("foliage_height").forGetter(RichFoliagePlacer::getFoliageHeight) .apply(instance, RichFoliagePlacer::new)); private final IntProvider foliageHeight; public RichFoliagePlacer(IntProvider radius, IntProvider offset, IntProvider foliageHeight) { super(radius, offset); this.foliageHeight = foliageHeight; } public IntProvider getFoliageHeight() { return this.foliageHeight; } @Override protected FoliagePlacerType<?> getType() { return Tutorial.RICH_FOLIAGE_PLACER; } @Override protected void generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, TreeFeatureConfig config, int trunkHeight, TreeNode treeNode, int foliageHeight, int radius, int offset) { BlockPos.Mutable center = treeNode.getCenter().mutableCopy(); for ( // 从 X 开始:中心 - 半径 Vec3i vec = center.subtract(new Vec3i(radius, 0, 0)); // 在 X 结束:中心 + 半径 vec.compareTo(center.add(new Vec3i(radius, 0, 0))) == 0; // 每次移动1 vec.add(1, 0, 0)) { this.placeFoliageBlock(world, replacer, random, config, new BlockPos(vec)); } for (Vec3i vec = center.subtract(new Vec3i(0, radius, 0)); vec.compareTo(center.add(new Vec3i(0, radius, 0))) == 0; vec.add(0, 1, 0)) { this.placeFoliageBlock(world, replacer, random, config, new BlockPos(vec)); } } @Override public int getRandomHeight(Random random, int trunkHeight, TreeFeatureConfig config) { // 使用 IntProvider 挑选随机高度 return foliageHeight.get(random); } @Override protected boolean isInvalidForLeaves(Random random, int dx, int y, int dz, int radius, boolean giantTrunk) { // 我们的 FoliagePlacer 不为树叶设置限制 return false; } }
注册并使用你的 FoliagePlacer
该过程几乎相同,只需要使用你的调用器创建并注册 FoliagePlacerType
public static final FoliagePlacerType<RichFoliagePlacer> RICH_FOLIAGE_PLACER = FoliagePlacerTypeInvoker.callRegister("rich_foliage_placer", RichFoliagePlacer.CODEC);
并将旧的 FoliagePlacer 替换成你的新的:
[...] new RichFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), ConstantIntProvider.create(3)), [...]
创建一个 TreeDecorator
TreeDecorator 允许你添加额外的元素到你的树之中在执行你的 TrunkPlacer 和 FoliagePlacer (比如苹果,蜂巢等) 之后。如果你有游戏后台开发经验的话,TreeDecorator 本质上是用于树木的一个后处理器(post-processer),用于修饰树木的额外信息。
原版的 TreeDecorators
原版的 TreeDecorator 几乎是没办法复用的,除了 LeavesVineTreeDecorator
和 TrunkVineTreeDecorator。
虽然这是一件非常繁琐的事情,但是你还是需要创建你自己的 TreeDecorator。
创建一个 TreeDecoratorType
一个 TreeDecoratorType 是需要注册到你的 TreeDecorator 之中的。
FabricAPI 没有提供任何工具用于创建 TreeDecoratorType, 所以我们需要再次使用 mixin 了。
我们的 mixin 大概会看起来非常像是以下内容,同时不要忘记把他们添加到你自己的 mixin 配置文件当中:
@Mixin(TreeDecoratorType.class) public interface TreeDecoratorTypeInvoker { @Invoker static <P extends TreeDecorator> TreeDecoratorType<P> callRegister(String id, Codec<P> codec) { throw new IllegalStateException(); } }
创建 TreeDecorator
TreeDecorator 有一个特别简单的结构:
- 一个可用于序列化的编码解码器。但默认情况下为空,因为构造函数是没有参数的。如果需要,你可以随时扩展(expand)它。
- 你的
TreeDecoratorType的获取器(getter)。 - 为修饰树而存在的
generate方法。
我们的 TreeDecorator 将在树干周围以 25% 的几率在树干的一侧产生金块(简直不要太爽太炫酷对不对):
public class RichTreeDecorator extends TreeDecorator { public static final RichTreeDecorator INSTANCE = new RichTreeDecorator(); // 我们的构造函数没有任何参数,所以我们创建一个单元编解码器,让他返回一个单例对象。 public static final Codec<RichTreeDecorator> CODEC = Codec.unit(() -> INSTANCE); @Override protected TreeDecoratorType<?> getType() { return Tutorial.RICH_TREE_DECORATOR; } @Override public void generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, List<BlockPos> logPositions, List<BlockPos> leavesPositions) { // 遍历方块位置 for (BlockPos logPosition : logPositions) { // 选择一个从 0(含)到 4(不含)的值,如果是 0,则继续 // 这是一个让树生成金块从而让我们走向富裕的机会,太爽了。 if (random.nextInt(4) == 0) { // 选择一个从 0 到 4 的随机值,并使用它确定将放置金块到树的一侧 int sideRaw = random.nextInt(4); Direction side = switch (sideRaw) { case 0 -> Direction.NORTH; case 1 -> Direction.SOUTH; case 2 -> Direction.EAST; case 3 -> Direction.WEST; default -> throw new ArithmeticException("The picked side value doesn't fit in the 0 to 4 bounds"); }; // 通过结果边偏移树木位置 BlockPos targetPosition = logPosition.offset(side, 1); // 使用 BiConsumer replacer 放置金块! // 这是在 TrunkPlacers、FoliagePlacers 和 TreeDecorators 中放置方块的标准方法。 replacer.accept(targetPosition, Blocks.GOLD_BLOCK.getDefaultState()); } } } }
注册和使用你的 TreeDecorator
首先,使用 @Invoker 注解你创建的 TreeDecoratorType :
public static final TreeDecoratorType<RichTreeDecorator> RICH_TREE_DECORATOR = TreeDecoratorTypeInvoker.callRegister("rich_tree_decorator", RichTreeDecorator.CODEC);
Then, between the creation of your TreeFeatureConfig.Builder and the build method call, put this:
[...] .decorators(Collections.singletonList(RichTreeDecorator.INSTANCE)) [...]
Creating an advanced SaplingGenerator
So, remember how I told you that SaplingGenerators can actually contain more complex logic?
Here's an example of that - we create several vanilla trees instead of the actual trees depending on the chance:
public class RichSaplingGenerator extends SaplingGenerator { private final ConfiguredFeature<TreeFeatureConfig, ?> feature; public RichSaplingGenerator(ConfiguredFeature<?, ?> feature) { this.feature = (ConfiguredFeature<TreeFeatureConfig, ?>) feature; } @Nullable @Override protected ConfiguredFeature<TreeFeatureConfig, ?> getTreeFeature(Random random, boolean bees) { int chance = random.nextInt(100); // Each tree has a 10% chance if (chance < 10) { return ConfiguredFeatures.OAK; } else if (chance < 20) { return ConfiguredFeatures.BIRCH; } else if (chance < 60) { return ConfiguredFeatures.SPRUCE; } else if (chance < 40) { return ConfiguredFeatures.MEGA_SPRUCE; } else if (chance < 50) { return ConfiguredFeatures.PINE; } else if (chance < 60) { return ConfiguredFeatures.MEGA_PINE; } else if (chance < 70) { return ConfiguredFeatures.MEGA_JUNGLE_TREE; } // If none of that happened (the random value was between 70 and 100), create the actual tree return feature; } }
This isn't a very practical, but it shows what you can achieve using SaplingGenerators.
Extra settings for your tree
Using the extra TreeFeatureConfig.Builder methods, you can add more settings to your tree:
dirtProvider
Sets the BlockStateProvider for the block of dirt generated under the tree.
Example:
[...] .dirtProvider(new SimpleBlockStateProvider(Blocks.IRON_BLOCK.getDefaultState())) [...]
decorators
Used to add TreeDecorators to your tree.
This was briefly showcased in the TreeDecorator section of this tutorial.
If you want, you can add multiple TreeDecorators to the same tree using a convenience method like Arrays.asList.
Example:
[...] .decorators(Arrays.asList( FirstTreeDecorator.INSTANCE, SecondTreeDecorator.INSTANCE, ThirdTreeDecorator.INSTANCE )) [...]
ignoreVines
Makes the tree generation ignore vines stuck in the way.
Example:
[...] .ignoreVines() [...]
forceDirt
Forces the TreeFeature to generate the dirt underneath the tree.
Example:
[...] .forceDirt() [...]
Creating a BlockStateProvider
Coming soon.