public static final RegistryEntry> 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> 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 [[tutorial: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 ''TrunkPlacer''s 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 ''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 TrunkPlacerType
callRegister(String id, Codec
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 ''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 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 generate(TestableWorld world, BiConsumer 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 ''ModInitializer''s body:
public static final TrunkPlacerType 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 ''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 FoliagePlacerType
callRegister(String id, Codec
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 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 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 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 ''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 TreeDecoratorType
callRegister(String id, Codec
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 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 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 ''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> 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.
===== 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 ''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.