User Tools

Site Tools


tutorial:trees

This is an old revision of the document!


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, TrunkPlacerTypes, FoliagePlacerTypes, TreeDecoratorTypes, BlockStateProviderTypes and IntProviderTypes 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:

  1. SaplingGenerator - creates your tree's ConfiguredFeature from a sapling depending on the context.
  2. TrunkPlacer - using it you generate the trunk of your tree.
  3. FoliagePlacer - using it you generate the foliage of your tree.
  4. TreeDecorator (optional) - using it you can generate additional elements on your tree, for example, beehives or apples.
  5. 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.
  6. FeatureSize - defines multiple Features combined into one. Most of the time with trees, you'll only need the vanilla TwoLayersFeatureSize implementation.
  7. 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:

  1. public static final ConfiguredFeature<?, ?> RICH_TREE = Feature.TREE
  2. // Reconfigure the feature using the builder
  3. .configure(new TreeFeatureConfig.Builder(
  4. // The SimpleBlockStateProvider just returns what you passed in it. This provider contains the trunk of your tree
  5. new SimpleBlockStateProvider(Blocks.NETHERITE_BLOCK.getDefaultState()),
  6. // The StraightTrunkPlacer places a straight trunk up in the air.
  7. new StraightTrunkPlacer(8, 3, 0),
  8. // This provider contains the foliage of your tree
  9. new SimpleBlockStateProvider(Blocks.DIAMOND_BLOCK.getDefaultState()),
  10. // This provider contains the sapling of your tree
  11. new SimpleBlockStateProvider(RICH_SAPLING.getDefaultState()),
  12. // The BlobFoliagePlacer places a big blob of your foliage.
  13. // The ConstantIntProvider simply returns the integer that you gave it.
  14. // The first provider determines the radius of the blob
  15. // The second provider determines the offset of the foliage from the trunk
  16. // The third integer determines the height of your foliage
  17. new BlobFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), 3),
  18. // The TwoLayersFeatureSize manages a two-layer Feature like a tree
  19. // The first integer is the layer limit
  20. // The second integer is the lower layer size
  21. // The third integer is the higher layer size
  22. // Most of the time, these values should not be edited
  23. new TwoLayersFeatureSize(1, 0, 1)
  24. ).build())
  25. // Now decorate the feature to make it spawn
  26. // Here we use the chance Decorator with a 30% chance of the tree spawning
  27. .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:

  1. static {
  2. RegistryKey<ConfiguredFeature<?, ?>> richTreeKey = RegistryKey.of(Registry.CONFIGURED_FEATURE_KEY, new Identifier("tutorial", "rich_tree"));
  3.  
  4. Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, richTreeKey.getValue(), RICH_TREE);
  5.  
  6. // You should use VEGETAL_DECORATION step for trees
  7. BiomeModifications.addFeature(BiomeSelectors.all(), GenerationStep.Feature.VEGETAL_DECORATION, richTreeKey);
  8. }

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:

  1. public class RichSaplingGenerator extends SaplingGenerator {
  2. private final ConfiguredFeature<TreeFeatureConfig, ?> feature;
  3.  
  4. public RichSaplingGenerator(ConfiguredFeature<?, ?> feature) {
  5. this.feature = (ConfiguredFeature<TreeFeatureConfig, ?>) feature;
  6. }
  7.  
  8. @Nullable
  9. @Override
  10. protected ConfiguredFeature<TreeFeatureConfig, ?> createTreeFeature(Random random, boolean bees) {
  11. return feature;
  12. }
  13. }

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.

  1. public class RichSaplingBlock extends SaplingBlock {
  2. public RichSaplingBlock(SaplingGenerator generator, Settings settings) {
  3. super(generator, settings);
  4. }
  5. }

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:

  1. public static final RICH_SAPLING = new RichSaplingBlock(new RichSaplingGenerator(RICH_TREE), FabricBlockSettings.copyOf(Blocks.BIRCH_SAPLING.getDefaultState()));
  2.  
  3. static {
  4. Registry.register(Registry.BLOCK, new Identifier("tutorial", "rich_sapling"), RICH_SAPLING);
  5. Registry.register(Registry.ITEM, new Identifier("tutorial", "rich_sapling"), new BlockItem(RICH_SAPLING, ItemGroup.MISC));
  6. }

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:

  • 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 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:

  1. @Mixin(TrunkPlacerType.class)
  2. public interface TrunkPlacerTypeInvoker {
  3. @Invoker
  4. static <TTrunkPlacer extends TrunkPlacer> TrunkPlacerType<TTrunkPlacer> callRegister(String id, Codec<TTrunkPlacer> codec) {
  5. }
  6. }

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 of TreeNodes you've created.

Our TrunkPlacer is going to create two trunks placed diagonally in the world:

  1. public class RichTrunkPlacer extends TrunkPlacer {
  2. // Use the fillTrunkPlacerFields to create our codec
  3. public static final Codec<RichTrunkPlacer> CODEC = RecordCodecBuilder.create(
  4. (instance) -> fillTrunkPlacerFields(instance).apply(instance, RichTrunkPlacer::new));
  5.  
  6. public RichTrunkPlacer(int baseHeight, int firstRandomHeight, int secondRandomHeight) {
  7. super(baseHeight, firstRandomHeight, secondRandomHeight);
  8. }
  9.  
  10. @Override
  11. protected TrunkPlacerType<?> getType() {
  12. return Tutorial.RICH_TRUNK_PLACER;
  13. }
  14.  
  15. @Override
  16. public List<FoliagePlacer.TreeNode> generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, int height, BlockPos startPos, TreeFeatureConfig config) {
  17. // Set the ground beneath the trunk to dirt
  18. setToDirt(world, replacer, random, startPos.down(), config);
  19.  
  20. // Iterate until the trunk height limit and place two blocks using the built-in getAndSetState method
  21. for (int i = 0; i < height; i++) {
  22. getAndSetState(world, replacer, random, startPos.up(i), config);
  23. getAndSetState(world, replacer, random, startPos.up(i).east().north(), config);
  24. }
  25.  
  26. // We create two TreeNodes - one for the first trunk, and the other for the second
  27. // Put the highest block in the trunk as the center position for the FoliagePlacer to use
  28. return ImmutableList.of(new FoliagePlacer.TreeNode(startPos.up(height), 0, false),
  29. new FoliagePlacer.TreeNode(startPos.east().north().up(height), 0, false));
  30. }
  31. }

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:

  1. 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:

  1. [...]
  2. new RichTrunkPlacer(8, 3, 0),
  3. [...]

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!

  1. @Mixin(FoliagePlacerType.class)
  2. public interface FoliagePlacerTypeInvoker {
  3. @Invoker
  4. static <TFoliagePlacer extends FoliagePlacer> FoliagePlacerType<TFoliagePlacer> callRegister(String id, Codec<TFoliagePlacer> codec) {
  5. }
  6. }

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):

  1. public class RichFoliagePlacer extends FoliagePlacer {
  2. // Here we use the built-in fillFoliagePlacerFields for basic fields
  3. //
  4. // For the foliageHeight we use a codec generated by IntProvider.createValidatingCodec
  5. // As the method's arguments we pass in the minimum and maximum value of the IntProvider
  6. // In fieldOf we put the name of the codec entry
  7. // In forGetter we return the value of the entry through a lambda expression
  8. //
  9. // To add more fields into your TrunkPlacer/FoliagePlacer/TreeDecorator etc., use multiple .and calls
  10. //
  11. // For an example of creating your own type of codec, see the IntProvider.createValidatingCodec method's source
  12. public static final Codec<RichFoliagePlacer> CODEC = RecordCodecBuilder.create(
  13. (instance) ->
  14. fillFoliagePlacerFields(instance)
  15. .and(IntProvider
  16. .createValidatingCodec(1, 512)
  17. .fieldOf("foliage_height")
  18. .forGetter((placer) -> placer.foliageHeight))
  19. .apply(instance, RichFoliagePlacer::new));
  20.  
  21. private final IntProvider foliageHeight;
  22.  
  23. public RichFoliagePlacer(IntProvider radius, IntProvider offset, IntProvider foliageHeight) {
  24. super(radius, offset);
  25.  
  26. this.foliageHeight = foliageHeight;
  27. }
  28.  
  29. @Override
  30. protected FoliagePlacerType<?> getType() {
  31. return Tutorial.RICH_FOLIAGE_PLACER;
  32. }
  33.  
  34. @Override
  35. protected void generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, TreeFeatureConfig config, int trunkHeight, TreeNode treeNode, int foliageHeight, int radius, int offset) {
  36. BlockPos.Mutable center = treeNode.getCenter().mutableCopy();
  37.  
  38. for (
  39. // Start from X: center-radius
  40. Vec3i vec = center.subtract(new Vec3i(radius, 0, 0));
  41. // End in X: center+radius
  42. vec.compareTo(center.add(new Vec3i(radius, 0, 0))) == 0;
  43. // Move by 1 each time
  44. vec.add(1, 0, 0)) {
  45. placeFoliageBlock(world, replacer, random, config, new BlockPos(vec));
  46. }
  47.  
  48. for (
  49. // Start from Y: center-radius
  50. Vec3i vec = center.subtract(new Vec3i(0, radius, 0));
  51. // End in Y: center+radius
  52. vec.compareTo(center.add(new Vec3i(0, radius, 0))) == 0;
  53. // Move by 1 each time
  54. vec.add(0, 1, 0)
  55. ) {
  56. placeFoliageBlock(world, replacer, random, config, new BlockPos(vec));
  57. }
  58. }
  59.  
  60. @Override
  61. public int getRandomHeight(Random random, int trunkHeight, TreeFeatureConfig config) {
  62. // Just pick the random height using the IntProvider
  63. return foliageHeight.get(random);
  64. }
  65.  
  66. @Override
  67. protected boolean isInvalidForLeaves(Random random, int dx, int y, int dz, int radius, boolean giantTrunk) {
  68. // Our FoliagePlacer doesn't set any restrictions on leaves
  69. return false;
  70. }
  71. }

Registering and using your FoliagePlacer

This process is almost exactly the same, just use your invoker to create and register the FoliagePlacerType

  1. public static final FoliagePlacerType<RichFoliagePlacer> RICH_FOLIAGE_PLACER = FoliagePlacerTypeInvoker.callRegister("rich_foliage_placer", RichFoliagePlacer.CODEC);

and replace the old FoliagePlacer with your new one:

  1. [...]
  2. new RichFoliagePlacer(ConstantIntProvider.create(5), ConstantIntProvider.create(0), ConstantIntProvider.create(3)),
  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:

  1. @Mixin(TreeDecoratorType.class)
  2. public interface TreeDecoratorTypeInvoker {
  3. @Invoker
  4. static <TTreeDecorator extends TreeDecorator> TreeDecoratorType<TTreeDecorator> callRegister(String id, Codec<TTreeDecorator> codec) {
  5. }
  6. }

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:

  1. public class RichTreeDecorator extends TreeDecorator {
  2. public static final RichTreeDecorator INSTANCE = new RichTreeDecorator();
  3. // Our constructor doesn't have any arguments, so we create an empty codec that returns the singleton instance
  4. public static final Codec<RichTreeDecorator> CODEC = Codec.unit(() -> INSTANCE);
  5.  
  6. @Override
  7. protected TreeDecoratorType<?> getType() {
  8. return Tutorial.RICH_TREE_DECORATOR;
  9. }
  10.  
  11. @Override
  12. public void generate(TestableWorld world, BiConsumer<BlockPos, BlockState> replacer, Random random, List<BlockPos> logPositions, List<BlockPos> leavesPositions) {
  13. // Iterate through block positions
  14. for (BlockPos logPosition : logPositions) {
  15. // Pick a value from 0 to 100 and if it's in the 0 to 25 range, continue
  16. // This is the chance for spawning the gold block
  17. if (random.nextInt(100) <= 25) {
  18. // Pick a random value from 0 to 3 and determine the side where the gold block will be placed using it
  19. int sideRaw = random.nextInt(4);
  20. Direction side = switch (sideRaw) {
  21. case 0 -> Direction.NORTH;
  22. case 1 -> Direction.SOUTH;
  23. case 2 -> Direction.EAST;
  24. case 3 -> Direction.WEST;
  25. default -> throw new ArithmeticException("The picked sideRaw value doesn't fit in the 0 to 3 bounds");
  26. };
  27.  
  28. // Offset the log position by the resulting side
  29. BlockPos targetPosition = logPosition.offset(side, 1);
  30.  
  31. // Place the gold block using the replacer BiConsumer
  32. // This is the standard way of placing blocks in TrunkPlacers, FoliagePlacers and TreeDecorators
  33. replacer.accept(targetPosition, Blocks.GOLD_BLOCK.getDefaultState());
  34. }
  35. }
  36. }
  37. }

Registering and using your TreeDecorator

First, create your TreeDecoratorType using the invoker:

  1. 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:

  1. [...]
  2. .decorators(Collections.singletonList(RichTreeDecorator.INSTANCE))
  3. [...]

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:

  1. public class RichSaplingGenerator extends SaplingGenerator {
  2. private final ConfiguredFeature<TreeFeatureConfig, ?> feature;
  3.  
  4. public RichSaplingGenerator(ConfiguredFeature<?, ?> feature) {
  5. this.feature = (ConfiguredFeature<TreeFeatureConfig, ?>) feature;
  6. }
  7.  
  8. @Nullable
  9. @Override
  10. protected ConfiguredFeature<TreeFeatureConfig, ?> createTreeFeature(Random random, boolean bees) {
  11. int chance = random.nextInt(100);
  12.  
  13. // With a 10% chance, an oak tree will be created
  14. if (chance <= 10) {
  15. return ConfiguredFeatures.OAK;
  16. }
  17. // With a 20% chance, a birch tree will be created
  18. if (chance <= 20) {
  19. return ConfiguredFeatures.BIRCH;
  20. }
  21. // With a 30% chance, a spruce tree will be created
  22. if (chance <= 30) {
  23. return ConfiguredFeatures.SPRUCE;
  24. }
  25. // With a 40% chance, a mega spruce tree will be created
  26. if (chance <= 40) {
  27. return ConfiguredFeatures.MEGA_SPRUCE;
  28. }
  29. // With a 50% chance, a pine tree will be created
  30. if (chance <= 50) {
  31. return ConfiguredFeatures.PINE;
  32. }
  33. // With a 60% chance, a mega pine tree will be created
  34. if (chance <= 60) {
  35. return ConfiguredFeatures.MEGA_PINE;
  36. }
  37. // With a 70% chance, a jungle tree will be created
  38. if (chance <= 70) {
  39. return ConfiguredFeatures.MEGA_JUNGLE_TREE;
  40. }
  41.  
  42. // If none of that happened (the chance is between 71 and 99 percents), create the actual tree
  43. return feature;
  44. }
  45. }

This isn't a very practical, 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 dirt generated beneath the tree.

Example:

[...]
.dirtProvider(new SimpleBlockStateProvider(Blocks.IRON_BLOCK.getDefaultState()))
[...]

decorators

Used to add TreeDecorators to your tree.
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 automatically no matter if there are blocks in the way.

Example:

[...]
.forceDirt()
[...]

Creating a BlockStateProvider

Coming soon.

Creating an IntProvider

Coming soon.

tutorial/trees.1624208988.txt.gz · Last modified: 2021/06/20 17:09 by redgrapefruit