User Tools

Site Tools


Sidebar

← Go back to the homepage

Fabric Tutorials

Setup

Basics

These pages are essential must-reads when modding with Fabric, and modding Minecraft in general, if you are new to modding, it is recommended you read the following.

Items

Blocks and Block Entities

Data Generation

World Generation

Commands

These pages will guide you through Mojang's Brigadier library which allows you to create commands with complex arguments and actions.

Events

These pages will guide you through using the many events included in Fabric API, and how to create your own events for you or other mods to use.

Entities

Fluids

Mixins & ASM

These pages will guide you through the usage of SpongePowered's Mixin library, which is a highly complex topic. We recommend you read these pages thoroughly.

Miscellaneous

Yarn

Contribute to Fabric

tutorial:features

Adding Features

Rocks, trees, ores, and ponds are all examples of features. They are simple generation additions to the world which generate depending on how they are configured. In this tutorial, we'll generate a simple pillar feature.

If you want to do something similar to vanilla (like ores or flower patches), you should first look for an existing feature you can use. In that case, skip the “Creating a feature” part of this tutorial.

There are 3 steps that are required to add a feature to a biome.

Creating a feature

Let's create a feature that spawns a 1×1 pillar of blocks on the ground. As an added challenge, let's also make it configurable, so we can change the height and material of the pillars.

We'll first create a new class for our feature. This is where the generation logic will go.

public class ExampleFeature extends Feature<ExampleFeatureConfig> {
  public Example(Codec<DefaultFeatureConfig> configCodec) {
    super(configCodec);
  }
 
  // this method is what is called when the game tries to generate the feature. it is where the actual blocks get placed into the world.
  @Override
  public boolean generate(FeatureContext<ExampleFeatureConfig> context) {
        StructureWorldAccess world = context.getWorld();
        // the origin is the place where the game starts trying to place the feature
        BlockPos origin = context.getOrigin();
        // we won't use the random here, but we could if we wanted to
        WorldGenRandom random = context.getRandom();
        ExampleFeatureConfig config = context.getConfig();
 
        // don't worry about where these come from-- we'll implement these methods soon
        int number = config.number();
        Identifier blockID = config.blockID();
 
        BlockState blockState = Registry.BLOCK.get(blockID).getDefaultState();
//        ensure the ID is okay
        if (blockState == null) throw new IllegalStateException(blockID + " could not be parsed to a valid block identifier!");
 
        // find the surface of the world
        BlockPos testPos = new BlockPos(origin);
        for (int y = 0; y < world.getHeight(); y++) {
            testPos = testPos.up();
            // the tag name is dirt, but includes grass, mud, podzol, etc.
            if (world.getBlockState(testPos).isIn(BlockTags.DIRT)) {
                if (world.getBlockState(testPos.up()).isOf(Blocks.AIR)) {
                    for (int i = 0; i < number; i++) {
//            create a simple pillar of blocks
                        world.setBlockState(testPos, blockState, 0x10);
                        testPos = testPos.up();
 
                        // ensure we don't try to place blocks outside the world
                        if (testPos.getY() >= world.getTopY()) break;
                    }
                    return true;
                }
            }
        }
//        the game couldn't find a place to put the pillar
        return false;
    }
}
Now, we need to implement that ExampleFeatureConfig record. This is where we define the variables that we use in our Feature. This config is essentially a wrapper for the parameters we want to pass to our feature. Note: while this tutorial only uses integers and BlockStates, other useful objects in the game also have codecs that can give you more control over how your feature generates. BlockStateProviders are a good example of this.
public record ExampleFeatureConfig(int number, Identifier blockID) implements FeatureConfig {
    public ExampleFeatureConfig(int number, Identifier blockID) {
        this.blockID = blockID;
        this.number = number;
    }
 
    public static Codec<ExampleFeatureConfig> CODEC = RecordCodecBuilder.create(
        instance ->
                instance.group(
                        // you can add as many of these as you want, one for each parameter
                        Codecs.POSITIVE_INT.fieldOf("number").forGetter(ExampleFeatureConfig::number),
                        Identifier.CODEC.fieldOf("blockID").forGetter(ExampleFeatureConfig::blockID))
                .apply(instance, ExampleFeatureConfig::new));
 
    public int number() {
        return number;
    }
    public Identifier blockID() {
        return blockID;
    }

Now that we have our config defined, the errors in our feature class will resolve. But we're not done yet– now we need to add our feature to the game. Features can be registered like most other content in the game, and there aren't any special builders or mechanics you'll have to worry about.

public class FeatureExampleMod implements ModInitializer {
    public static final Identifier EXAMPLE_FEATURE_ID = new Identifier("wiki-example", "example_feature");
    public static Feature<ExampleFeatureConfig> EXAMPLE_FEATURE = new ExampleFeature(ExampleFeatureConfig.CODEC);
 
    @Override
    public void onInitialize() {
        Registry.register(Registry.FEATURE, EXAMPLE_FEATURE_ID, EXAMPLE_FEATURE);
    }
}

If you plan to configure and use your feature using datapacks, you can stop here. To implement it in code, read on.

Configuring a feature

We need to give a configuration to a feature, before we can add it to biomes. Make sure to register configured features as well as features. Here is where we specify the parameters of our feature. We'll generate 10-block-high pillars out of netherite. In fact, we could register as many of these ConfiguredFeatures as we wanted, just changing the config parameters each time.

public class FeatureExampleMod implements ModInitializer {
 
    public static final Identifier EXAMPLE_FEATURE_ID = new Identifier("wiki-example", "example_feature");
    public static Feature<ExampleFeatureConfig> EXAMPLE_FEATURE = new ExampleFeature(ExampleFeatureConfig.CODEC);
 
    public static ConfiguredFeature<ExampleFeatureConfig, ExampleFeature> EXAMPLE_FEATURE_CONFIGURED = new ConfiguredFeature<>(
                    (ExampleFeature) EXAMPLE_FEATURE,
                    new ExampleFeatureConfig(10, new Identifier("minecraft", "netherite_block"))
    );
 
 
    @Override
    public void onInitialize() {
        Registry.register(Registry.FEATURE, EXAMPLE_FEATURE_ID, EXAMPLE_FEATURE);
        Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, EXAMPLE_FEATURE_ID, EXAMPLE_FEATURE_CONFIGURED);
    }
}

Adding a configured feature to a biome

We use the Biome Modification API. The final stage of feature configuration is creating a PlacedFeature. This class is a replacement for Decorators on ConfiguredFeatures. Don't forget to register it as well!

Our final initializer class looks like this:

public class FeatureExampleMod implements ModInitializer {
 
    public static final Identifier EXAMPLE_FEATURE_ID = new Identifier("wiki-example", "example_feature");
    public static Feature<ExampleFeatureConfig> EXAMPLE_FEATURE = new ExampleFeature(ExampleFeatureConfig.CODEC);
    public static ConfiguredFeature<ExampleFeatureConfig, ExampleFeature> EXAMPLE_FEATURE_CONFIGURED = new ConfiguredFeature<>(
                    (ExampleFeature) EXAMPLE_FEATURE,
                    new ExampleFeatureConfig(10, new Identifier("minecraft", "netherite_block"))
    );
    // our PlacedFeature. this is what gets passed to the biome modification API to add to the biome.
    public static PlacedFeature EXAMPLE_FEATURE_PLACED = new PlacedFeature(
            RegistryEntry.of(
                    EXAMPLE_FEATURE_CONFIGURED
//                    the SquarePlacementModifier makes the feature generate a cluster of pillars each time
            ), List.of(SquarePlacementModifier.of())
    );
 
    @Override
    public void onInitialize() {
//        register the features
        Registry.register(Registry.FEATURE, EXAMPLE_FEATURE_ID, EXAMPLE_FEATURE);
        Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, EXAMPLE_FEATURE_ID, EXAMPLE_FEATURE_CONFIGURED);
        Registry.register(BuiltinRegistries.PLACED_FEATURE, EXAMPLE_FEATURE_ID, EXAMPLE_FEATURE_PLACED);
 
//        add it to overworld biomes using FAPI
        BiomeModifications.addFeature(
                BiomeSelectors.foundInOverworld(),
                // the feature is to be added while flowers and trees are being generated
                GenerationStep.Feature.VEGETAL_DECORATION,
                RegistryKey.of(Registry.PLACED_FEATURE_KEY, EXAMPLE_FEATURE_ID));
    }
}

The first argument of addFeature determines what biomes the structure is generated in.

The second argument helps determine when the structure is generated. For above-ground houses you may go with SURFACE_STRUCTURES, and for caves, you might go with RAW_GENERATION.

For more information, the code used in this tutorial is available on Github.

tutorial/features.txt · Last modified: 2022/09/03 23:28 by miir