It is recommended that you learn how to create a Feature in Minecraft first before reading this tutorial.
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.
Firstly, you need to understand that a PlacedFeature
is what gets placed in the world. There are a few steps to retrieve a tree placed feature:
You need a feature, in our case Feature.TREE
, we configure it with TreeFeatureConfig
, make a placed feature, make a configured feature and finally a placed feature (This is the placed feature that gets placed in the world)
Minecraft's tree configuration architecture is split into different classes to allow for very complex and beautiful trees.
Here's an overview:
TrunkPlacer
- this generates the trunk of the tree.FoliagePlacer
- this generates the leaves of the tree.SaplingGenerator
- creates your tree's ConfiguredFeature
from a sapling depending on the context.TreeDecorator
- you can generate additional elements on your tree with this, for example, beehives or vines. (optional)BlockStateProvider
- Can return blocks depending on context. This is useful if you want a part of your tree to be block A, and the other one block B.You can create custom implementations of these if you want a tree that does not look like vanilla's. However, the vanilla implementations are usually enough.
We don't need to create a new Feature
, as the vanilla TreeFeature
is configurable.
Add this into your ModInitializer
's (This could be your content initializer if you prefer) body:
public static final RegistryEntry<ConfiguredFeature<TreeFeatureConfig, ?>> TREE_RICH = ConfiguredFeatures.register("tutorial:tree_rich", Feature.TREE // Configure the feature using the builder new TreeFeatureConfig.Builder( BlockStateProvider.of(Blocks.NETHERITE_BLOCK), // Trunk block provider new StraightTrunkPlacer(8, 3, 0), // places a straight trunk BlockStateProvider.of(Blocks.DIAMOND_BLOCK), // Foliage block provider new BlobFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), 3), // places leaves as a blob (radius, offset from trunk, height) new TwoLayersFeatureSize(1, 0, 1) // The width of the tree at different layers; used to see how tall the tree can be without clipping into blocks ).build()));
A sapling is a special kind of block to grow trees that requires a SaplingGenerator
.
A simple generator that takes your tree's ConfiguredFeature
and returns it would look like this:
public class RichSaplingGenerator extends SaplingGenerator { @Nullable @Override protected RegistryEntry<ConfiguredFeature<TreeFeatureConfig, ?>> getTreeFeature(Random random, boolean bees) { return Tutorial.TREE_RICH; } }
An example of an advanced SaplingGenerator
will be shown in a later section.
Creating the block itself requires you to extend SaplingBlock
instead of just instantiating it, because its constructor has protected access.
public class RichSaplingBlock extends SaplingBlock { public RichSaplingBlock(SaplingGenerator generator, Settings settings) { super(generator, settings); } }
To register your sapling, follow the normal steps for registering a block (see blocks), but pass in the instance of your generator with the ConfiguredFeature
.
Put this in the class you use for your blocks:
public static final RichSaplingBlock RICH_SAPLING = new RichSaplingBlock(new RichSaplingGenerator(TREE_RICH), FabricBlockSettings.copyOf(Blocks.OAK_SAPLING)); public static void register() { Registry.register(Registries.BLOCK, new Identifier("tutorial", "rich_sapling"), RICH_SAPLING); Registry.register(Registries.ITEM, new Identifier("tutorial", "rich_sapling"), new BlockItem(RICH_SAPLING, new FabricItemSettings())); }
A TrunkPlacer
creates the tree's trunk out of the block given by the BlockStateProvider
.
Before creating one, look at the reusable vanilla TrunkPlacer
s available and try not to reinvent the wheel:
StraightTrunkPlacer
ForkingTrunkPlacer
GiantTrunkPlacer
BendingTrunkPlacer
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("register") static <P extends TrunkPlacer> TrunkPlacerType<P> callRegister(String id, Codec<P> codec) { throw new IllegalStateException(); } }
A TrunkPlacer
contains multiple things in it:
fillTrunkPlacerFields
method to generate it.TrunkPlacerType
generate
method where you place the trunk and return a list of TreeNode
s, which are used by the foliage placer for where to place the leaves.
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 getAndSetState method from TrunkPlacer 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); } // 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 return ImmutableList.of(new FoliagePlacer.TreeNode(startPos.up(height), 0, false), new FoliagePlacer.TreeNode(startPos.east().north().up(height), 0, false)); } }
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("tutorial:rich_trunk_placer", RichTrunkPlacer.CODEC);
Now just replace your StraightTrunkPlacer
with your newly created RichTrunkPlacer
and you're done:
[...] new RichTrunkPlacer(8, 3, 0), [...]
A FoliagePlacer
creates the tree's foliage out of the block given by the BlockStateProvider
.
Before creating a FoliagePlacer
, look at the reusable vanilla FoliagePlacer
s to not reinvent the wheel:
BlobFoliagePlacer
BushFoliagePlacer
RandomSpreadFoliagePlacer
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 <P extends FoliagePlacer> FoliagePlacerType<P> callRegister(String id, Codec<P> codec) { throw new IllegalStateException(); } }
A FoliagePlacer
is a bit more complicated to create than a TrunkPlacer
. It contains:
FoliagePlacerType
.generate
method where you create the foliage.getRandomHeight
method. Despite the name, you normally should just return the maximum height of your foliage.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 { // 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 // 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(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 ( // 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)) { 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) { // 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; } }
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("tutorial: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)), [...]
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.
Almost none vanilla TreeDecorator
s are reusable, except for LeavesVineTreeDecorator
and TrunkVineTreeDecorator
.
For anything non-trivial, you have to create your own TreeDecorator
s.
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(); } }
A TreeDecorator
has an extremely simple structure:
TreeDecoratorType
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); private RichTreeDecorator() {} @Override protected TreeDecoratorType<?> getType() { return Tutorial.RICH_TREE_DECORATOR; } @Override public void generate(TreeDecorator.Generator generator) { // Iterate through block positions generator.getLogPositions().forEach(pos -> { Random random = generator.getRandom(); // 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()); } }); } }
First, create your TreeDecoratorType
using the invoker:
public static final TreeDecoratorType<RichTreeDecorator> RICH_TREE_DECORATOR = TreeDecoratorTypeInvoker.callRegister("tutorial: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)) [...]
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 { @Nullable @Override protected RegistryEntry<ConfiguredFeature<TreeFeatureConfig, ?>> getTreeFeature(Random random, boolean bees) { int chance = random.nextInt(100); // Each tree has a 10% chance return switch (chance) { case 10 -> TreeConfiguredFeatures.OAK; case 20 -> TreeConfiguredFeatures.BIRCH; case 30 -> TreeConfiguredFeatures.MEGA_SPRUCE; case 40 -> TreeConfiguredFeatures.PINE; case 50 -> TreeConfiguredFeatures.MEGA_PINE; case 60 -> TreeConfiguredFeatures.MEGA_JUNGLE_TREE; default -> Tutorial.RICH } } }
This isn't a very practical example, but it shows what you can achieve using SaplingGenerator
s.
Using the extra TreeFeatureConfig.Builder
methods, you can add more settings to your tree:
Sets the BlockStateProvider
for the block of dirt generated under the tree.
Example:
[...] .dirtProvider(BlockStateProvider.of(Blocks.IRON_BLOCK)) [...]
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 )) [...]
Makes the tree generation ignore vines stuck in the way.
Example:
[...] .ignoreVines() [...]
Forces the TreeFeature
to generate the dirt underneath the tree.
Example:
[...] .forceDirt() [...]
Coming soon.