This is an old revision of the document!
Table of Contents
添加树木 [1.17](高级)
阅读本文之前,建议先学习如何创建一个特征地形。
参见features
树木是在你的mod中拓展原版世界生成的一个好方法。
注意本话题较为高级,因此开始之前,最好要有关于修改世界生成的丰富经验。
关于API
有个API可以让你非常方便的添加树木,但是他还在开发中,但在2000年之后,相信我们的天神modmuss50从苍穹之空下凡审查了pull request之后,就可以使用了
PR见此处。.
创建简单的树木
结构
原版的树的结构分为不同的种类,方便你做出复杂并且漂亮的树。
概览如下:
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
然后用Fabric的API修改生物群系:
@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
避免做重复的工作:
StraightTrunkPlacer
ForkingTrunkPlacer
GiantTrunkPlacer
BendingTrunkPlacer
创建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)是其自己的话题,这里我们只需要使用
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
以避免另起炉灶:
BlobFoliagePlacer
BushFoliagePlacer
RandomSpreadFoliagePlacer
创建FoliagePlacerType
往游戏中注册FoliagePlacer
需要FoliagePlacerType
。
和TrunkPlacerType
类似,Fabric API不提供创建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
的获取器。 generate
方法,该方法创建树叶。getRandomHeight
方法。不管名字是什么,你通常应该返回你的树叶的最大高度。isInvalidForLeaves
方法,可以为放置树叶的地方设置限制。
我们的FoliagePlacer
会往各个方向(东南西北)创建4行的树叶方块:
public class RichFoliagePlacer extends FoliagePlacer { // 对于foliageHeight我们使用由IntProvider.createValidatingCodec生成的编码解码器 // 方法参数,我们传入IntProvider的最小值和最大值 // 往你的TrunkPlacer/FoliagePlacer/TreeDecorator等添加多个域,可调用多次.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
本质上是用于树木的一个后处理器,用于修饰树木的额外信息。
Vanilla TreeDecorators
Almost none vanilla TreeDecorator
s are reusable, except for LeavesVineTreeDecorator
and TrunkVineTreeDecorator
.
For anything non-trivial, you have to create your own TreeDecorator
s.
Creating a TreeDecoratorType
A TreeDecoratorType
is required to register your TreeDecorator
.
Fabric API doesn't provide utilities for creating TreeDecoratorType
s, so we have to use mixins again.
Our mixin will look almost exactly the same, don't forget to add it to your mixin config:
@Mixin(TreeDecoratorType.class) public interface TreeDecoratorTypeInvoker { @Invoker static <P extends TreeDecorator> TreeDecoratorType<P> callRegister(String id, Codec<P> codec) { throw new IllegalStateException(); } }
Creating the TreeDecorator
A TreeDecorator
has an extremely simple structure:
- A codec for serialization, but it's empty by default because the constructor has no arguments. You can always expand it if you want
- A getter for your
TreeDecoratorType
- The
generate
method to decorate the tree
Our TreeDecorator
will spawn gold blocks around the trunk of our tree with a 25% chance on a random side of the trunk:
public class RichTreeDecorator extends TreeDecorator { public static final RichTreeDecorator INSTANCE = new RichTreeDecorator(); // Our constructor doesn't have any arguments, so we create a unit codec that returns the singleton instance 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) { // Iterate through block positions for (BlockPos logPosition : logPositions) { // Pick a value from 0 (inclusive) to 4 (exclusive) and if it's 0, continue // This is the chance for spawning the gold block if (random.nextInt(4) == 0) { // Pick a random value from 0 to 4 and determine the side where the gold block will be placed using it 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"); }; // Offset the log position by the resulting side BlockPos targetPosition = logPosition.offset(side, 1); // Place the gold block using the replacer BiConsumer // This is the standard way of placing blocks in TrunkPlacers, FoliagePlacers and TreeDecorators replacer.accept(targetPosition, Blocks.GOLD_BLOCK.getDefaultState()); } } } }
Registering and using your TreeDecorator
First, create your TreeDecoratorType
using the invoker:
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 SaplingGenerator
s 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 SaplingGenerator
s.
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 TreeDecorator
s to your tree.
This was briefly showcased in the TreeDecorator
section of this tutorial.
If you want, you can add multiple TreeDecorator
s 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.