User Tools

Site Tools


tutorial:trees

Adding Trees [1.19.2] (Advanced)

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)

Creating a Simple Tree

Architecture

Minecraft's tree configuration architecture is split into different classes to allow for very complex and beautiful trees.
Here's an overview:

  1. TrunkPlacer - this generates the trunk of the tree.
  2. FoliagePlacer - this generates the leaves of the tree.
  3. SaplingGenerator - creates your tree's ConfiguredFeature from a sapling depending on the context.
  4. TreeDecorator - you can generate additional elements on your tree with this, for example, beehives or vines. (optional)
  5. 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.

Creating the ConfiguredFeature

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()));

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 {
  @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 SaplingBlock

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);
  }
}

Registering the SaplingBlock

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()));
}

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 TrunkPlacers available and try not to reinvent the wheel:

  • StraightTrunkPlacer
  • ForkingTrunkPlacer
  • GiantTrunkPlacer
  • BendingTrunkPlacer

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 TrunkPlacers,
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();
    }
}

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 place the trunk and return a list of TreeNodes, 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));
    }
}

Registering and using your TrunkPlacer

Using your invoker, create and register an instance of a TrunkPlacerType for your TrunkPlacer. Put this into your ModInitializers 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),
[...]

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 FoliagePlacers 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 <P extends FoliagePlacer> FoliagePlacerType<P> callRegister(String id, Codec<P> codec) {
        throw new IllegalStateException();
    }
}

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 {
    // 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;
    }
}

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("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)),
[...]

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 TreeDecorators are reusable, except for LeavesVineTreeDecorator and TrunkVineTreeDecorator.

For anything non-trivial, you have to create your own TreeDecorators.

Creating a TreeDecoratorType

A TreeDecoratorType is required to register your TreeDecorator.

Fabric API doesn't provide utilities for creating TreeDecoratorTypes, 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);
 
    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());
            }
        });
    }
}

Registering and using your TreeDecorator

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))
[...]

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 {
    @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 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(BlockStateProvider.of(Blocks.IRON_BLOCK))
[...]

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.

tutorial/trees.txt · Last modified: 2022/12/21 01:40 by haykam