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
本质上是用于树木的一个后处理器,用于修饰树木的额外信息。
原版的 TreeDecorators
原版的 TreeDecorator
几乎是没办法复用的,除了 LeavesVineTreeDecorator
和 TrunkVineTreeDecorator
。
虽然这是一件非常繁琐的事情,但是你还是需要创建你自己的 TreeDecorator
.
创建一个 TreeDecoratorType
一个 TreeDecoratorType
是需要注册到你的 TreeDecorator
之中的。
Fabric API 没有提供任何工具用于创建 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(); } }
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.