This is an old revision of the document!
Table of Contents
Adding Trees [1.17] (Advanced)
It is recommended that you learn how to create a Feature
in Minecraft first before reading this tutorial.
See https://fabricmc.net/wiki/tutorial:features
Trees are a great way to expand Minecraft's world generation in your mod.
Beware that this topic is advanced and preferably you should have decent experience with modding world generation in Minecraft before starting.
API news
Currently, TrunkPlacerType
s, FoliagePlacerType
s, TreeDecoratorType
s, BlockStateProviderType
s and IntProviderType
s cannot be created and you have to use mixins.
I'm working on a pull request to the Fabric API that adds these mixins into
the Object Builders API (v1) for you to use in a nice way.
I hope everything works out and the pull request will be merged into Fabric API for all of us to use!
Creating a Simple Tree
Architecture
Minecraft's tree architecture is split into different classes to allow for very complex and beautiful trees.
Here's an overview:
SaplingGenerator
- creates your tree'sConfiguredFeature
from a sapling depending on the context.TrunkPlacer
- using it you generate the trunk of your tree.FoliagePlacer
- using it you generate the foliage of your tree.TreeDecorator
(optional) - using it you can generate additional elements on your tree, for example, beehives or apples.BlockStateProvider
- it allows you to programmatically return a block depending on the context. This can be useful when you want a part of your tree to be block A, and the other one - block B.FeatureSize
- defines multipleFeature
s combined into one. Most of the time with trees, you'll only need the vanillaTwoLayersFeatureSize
implementation.IntProvider
- provides an integer value depending on the context.
The creation of these will be documented later, for now we'll use the vanilla implementations to simplify the process.
Creating the ConfiguredFeature
We won't need to create a new Feature
, instead we'll reuse the vanilla TreeFeature
with our own settings.
Add this into your ModInitializer
's body:
public static final ConfiguredFeature<?, ?> RICH_TREE = Feature.TREE // Reconfigure the feature using the builder .configure(new TreeFeatureConfig.Builder( // The SimpleBlockStateProvider just returns what you passed in it. This provider contains the trunk of your tree new SimpleBlockStateProvider(Blocks.NETHERITE_BLOCK.getDefaultState()), // The StraightTrunkPlacer places a straight trunk up in the air. new StraightTrunkPlacer(8, 3, 0), // This provider contains the foliage of your tree new SimpleBlockStateProvider(Blocks.DIAMOND_BLOCK.getDefaultState()), // This provider contains the sapling of your tree new SimpleBlockStateProvider(RICH_SAPLING.getDefaultState()), // The BlobFoliagePlacer places a big blob of your foliage. // The ConstantIntProvider simply returns the integer that you gave it. // The first provider determines the radius of the blob // The second provider determines the offset of the foliage from the trunk // The third integer determines the height of your foliage new BlobFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), 3), // The TwoLayersFeatureSize manages a two-layer Feature like a tree // The first integer is the layer limit // The second integer is the lower layer size // The third integer is the higher layer size // Most of the time, these values should not be edited new TwoLayersFeatureSize(1, 0, 1) ).build()) // Now decorate the feature to make it spawn // Here we use the chance Decorator with a 30% chance of the tree spawning .decorate(Decorator.CHANCE.configure(new ChanceDecoratorConfig(30)));
Now we just register the ConfiguredFeature
to the game like normal and make the biome modification using Fabric's API:
static { RegistryKey<ConfiguredFeature<?, ?>> richTreeKey = RegistryKey.of(Registry.CONFIGURED_FEATURE_KEY, new Identifier("tutorial", "rich_tree")); // You should use VEGETAL_DECORATION step for trees BiomeModifications.addFeature(BiomeSelectors.all(), GenerationStep.Feature.VEGETAL_DECORATION, richTreeKey); }
Creating the sapling
A sapling is a special kind of block to grow trees that requires a SaplingGenerator
.
Creating the SaplingGenerator
A simple generator that takes your tree's ConfiguredFeature
and returns it would look like this:
public class RichSaplingGenerator extends SaplingGenerator { private final ConfiguredFeature<TreeFeatureConfig, ?> feature; public RichSaplingGenerator(ConfiguredFeature<?, ?> feature) { this.feature = (ConfiguredFeature<TreeFeatureConfig, ?>) feature; } @Nullable @Override return feature; } }
An example of an advanced SaplingGenerator
will be shown in a later section.
Creating the SaplingBlock
Creating the block itself requires you to extend from SaplingBlock
because it's constructor has protected access.
public class RichSaplingBlock extends SaplingBlock { public RichSaplingBlock(SaplingGenerator generator, Settings settings) { super(generator, settings); } }
Registering the SaplingBlock
To register your sapling, follow the normal steps for registering a block (see https://fabricmc.net/wiki/tutorial:blocks),
but pass in the instance of your generator with the ConfiguredFeature
.
Put this after your ConfiguredFeature
declaration:
public static final RICH_SAPLING = new RichSaplingBlock(new RichSaplingGenerator(RICH_TREE), FabricBlockSettings.copyOf(Blocks.BIRCH_SAPLING.getDefaultState())); static { }
Creating a TrunkPlacer
A TrunkPlacer
creates the tree's trunk out of the block given by the BlockStateProvider
.
Vanilla TrunkPlacers
Before creating one, look at the reusable vanilla TrunkPlacer
s available and try not to reinvent the wheel:
BendingTrunkPlacer
ForkingTrunkPlacer
GiantTrunkPlacer
StraightTrunkPlacer
Creating a TrunkPlacerType
A TrunkPlacerType
is necessary to register your TrunkPlacer
into the game.
Unfortunately, Fabric API currently doesn't have an API for creating and registering TrunkPlacer
s,
so we have to use mixins.
We're going to create an invoker (see https://github.com/2xsaiko/mixin-cheatsheet/blob/master/invoker.md) to
invoke the private static TrunkPlacerType.register
method.
Here's our mixin, and don't forget to add it to your mixin config:
@Mixin(TrunkPlacerType.class) public interface TrunkPlacerTypeInvoker { @Invoker static <TTrunkPlacer extends TrunkPlacer> TrunkPlacerType<TTrunkPlacer> callRegister(String id, Codec<TTrunkPlacer> codec) { } }
Creating the TrunkPlacer
A TrunkPlacer
contains multiple things in it:
- A codec for serialization. Codecs are a topic of their own, here we'll just use the
fillTrunkPlacerFields
method to generate it. - A getter where you return your
TrunkPlacerType
- The
generate
method where you create your trunk and return a list ofTreeNode
s you've created.
Our TrunkPlacer
is going to create two trunks placed diagonally in the world:
public class RichTrunkPlacer extends TrunkPlacer { // Use the fillTrunkPlacerFields to create our codec 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) { // Set the ground beneath the trunk to dirt setToDirt(world, replacer, random, startPos.down(), config); // Iterate until the trunk height limit and place two blocks using the built-in getAndSetState method for (int i = 0; i < height; i++) { getAndSetState(world, replacer, random, startPos.up(i), config); getAndSetState(world, replacer, random, startPos.up(i).east().north(), config); } // We create two TreeNodes - one for the first trunk, and the other for the second // Put the highest block in the trunk as the center position for the FoliagePlacer to use } }
Registering and using your TrunkPlacer
Using your invoker, create and register an instance of a TrunkPlacerType
for your TrunkPlacer
.
Put this into your ModInitializer
s body:
public static final TrunkPlacerType<RichTrunkPlacer> RICH_TRUNK_PLACER = TrunkPlacerTypeInvoker.callRegister("rich_trunk_placer", RichTrunkPlacer.CODEC);
Now just replace your StraightTrunkPlacer
with your newly created RichTrunkPlacer
and you're done:
[...] new RichTrunkPlacer(8, 3, 0), [...]
Creating a FoliagePlacer
A FoliagePlacer
creates the tree's foliage out of the block given by the BlockStateProvider
.
Vanilla FoliagePlacers
Before creating a FoliagePlacer
, look at the reusable vanilla FoliagePlacer
s to not reinvent the wheel:
BlobFoliagePlacer
BushFoliagePlacer
RandomSpreadFoliagePlacer
Creating a FoliagePlacerType
A FoliagePlacerType
is necessary to register a FoliagePlacer
into the game.
Similarly to the TrunkPlacerType
, Fabric API doesn't provide utilities for creating a FoliagePlacerType
.
Our mixin will look almost exactly the same. Don't forget to add it to your mixin config!
@Mixin(FoliagePlacerType.class) public interface FoliagePlacerTypeInvoker { @Invoker static <TFoliagePlacer extends FoliagePlacer> FoliagePlacerType<TFoliagePlacer> callRegister(String id, Codec<TFoliagePlacer> codec) { } }
Creating the FoliagePlacer
A FoliagePlacer
is a bit more complicated to create than a TrunkPlacer
. It contains:
- A codec for serialization. In this example we show how to add an extra IntProvider to the codec.
- A getter for your
FoliagePlacerType
. - The
generate
method where you create the foliage. - The
getRandomHeight
method. Despite the name, you normally should just return the maximum height of your foliage. - The
isInvalidForLeaves
method where you can set restrictions on where to put the leaves.
Our FoliagePlacer
will create 4 lines of our foliage block in all directions (north, south, east, west):
public class RichFoliagePlacer extends FoliagePlacer { // Here we use the built-in fillFoliagePlacerFields for basic fields // // For the foliageHeight we use a codec generated by IntProvider.createValidatingCodec // As the method's arguments we pass in the minimum and maximum value of the IntProvider // In fieldOf we put the name of the codec entry // In forGetter we return the value of the entry through a lambda expression // // To add more fields into your TrunkPlacer/FoliagePlacer/TreeDecorator etc., use multiple .and calls // // For an example of creating your own type of codec, see the IntProvider.createValidatingCodec method's source public static final Codec<RichFoliagePlacer> CODEC = RecordCodecBuilder.create( (instance) -> fillFoliagePlacerFields(instance) .and(IntProvider .createValidatingCodec(1, 512) .fieldOf("foliage_height") .forGetter((placer) -> placer.foliageHeight)) .apply(instance, RichFoliagePlacer::new)); private final IntProvider foliageHeight; public RichFoliagePlacer(IntProvider radius, IntProvider offset, IntProvider foliageHeight) { super(radius, offset); this.foliageHeight = foliageHeight; } @Override protected FoliagePlacerType<?> getType() { return Tutorial.RICH_FOLIAGE_PLACER; } @Override BlockPos.Mutable center = treeNode.getCenter().mutableCopy(); for ( // Start from X: center-radius Vec3i vec = center.subtract(new Vec3i(radius, 0, 0)); // End in X: center+radius vec.compareTo(center.add(new Vec3i(radius, 0, 0))) == 0; // Move by 1 each time vec.add(1, 0, 0)) { placeFoliageBlock(world, replacer, random, config, new BlockPos(vec)); } for ( // Start from Y: center-radius Vec3i vec = center.subtract(new Vec3i(0, radius, 0)); // End in Y: center+radius vec.compareTo(center.add(new Vec3i(0, radius, 0))) == 0; // Move by 1 each time vec.add(0, 1, 0) ) { placeFoliageBlock(world, replacer, random, config, new BlockPos(vec)); } } @Override // Just pick the random height using the IntProvider return foliageHeight.get(random); } @Override protected boolean isInvalidForLeaves(Random random, int dx, int y, int dz, int radius, boolean giantTrunk) { // Our FoliagePlacer doesn't set any restrictions on leaves return false; } }
Registering and using your FoliagePlacer
This process is almost exactly the same, just use your invoker to create and register the FoliagePlacerType
public static final FoliagePlacerType<RichFoliagePlacer> RICH_FOLIAGE_PLACER = FoliagePlacerTypeInvoker.callRegister("rich_foliage_placer", RichFoliagePlacer.CODEC);
and replace the old FoliagePlacer
with your new one:
[...] new RichFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), ConstantIntProvider.create(3)), [...]
Creating a TreeDecorator
A TreeDecorator
allows you to add extra elements to your tree (apples, beehives etc.) after the execution of your TrunkPlacer
and FoliagePlacer
.
If you have a game development background, it's essentially a post-processor, but for trees.
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 <TTreeDecorator extends TreeDecorator> TreeDecoratorType<TTreeDecorator> callRegister(String id, Codec<TTreeDecorator> codec) { } }
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 an empty 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 to 100 and if it's in the 0 to 25 range, continue // This is the chance for spawning the gold block if (random.nextInt(100) <= 25) { // Pick a random value from 0 to 3 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 sideRaw value doesn't fit in the 0 to 3 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:
[...] [...]
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 int chance = random.nextInt(100); // With a 10% chance, an oak tree will be created if (chance <= 10) { return ConfiguredFeatures.OAK; } // With a 20% chance, a birch tree will be created if (chance <= 20) { return ConfiguredFeatures.BIRCH; } // With a 30% chance, a spruce tree will be created if (chance <= 30) { return ConfiguredFeatures.SPRUCE; } // With a 40% chance, a mega spruce tree will be created if (chance <= 40) { return ConfiguredFeatures.MEGA_SPRUCE; } // With a 50% chance, a pine tree will be created if (chance <= 50) { return ConfiguredFeatures.PINE; } // With a 60% chance, a mega pine tree will be created if (chance <= 60) { return ConfiguredFeatures.MEGA_PINE; } // With a 70% chance, a jungle tree will be created if (chance <= 70) { return ConfiguredFeatures.MEGA_JUNGLE_TREE; } // If none of that happened (the chance is between 71 and 99 percents), 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 dirt generated beneath the tree.
Example:
[...] .dirtProvider(new SimpleBlockStateProvider(Blocks.IRON_BLOCK.getDefaultState())) [...]
decorators
Used to add TreeDecorator
s to your tree.
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 automatically no matter if there are blocks in the way.
Example:
[...] .forceDirt() [...]
Creating a BlockStateProvider
Coming soon.
Creating an IntProvider
Coming soon.