User Tools

Site Tools


Sidebar

Setup

Basics

Advanced

Items

Blocks and Block Entities

Fluids

Entities

World Generation

Recipe Types

Miscellaneous

Events

Mixins

Dynamic Data Generation

Tutorials for Minecraft 1.15

Tutorials for Minecraft 1.14

Documentation

tutorial:features

Adding Features [1.17]

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 look at generating a simple stone spiral feature randomly.

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

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

Note that the Biome Modification API is marked as experimental. If the API doesn't work, consider using the mixin version.

Creating a feature

A simple Feature looks like this:

public class StoneSpiralFeature extends Feature<DefaultFeatureConfig> {
  public StoneSpiralFeature(Codec<DefaultFeatureConfig> configCodec) {
    super(configCodec);
  }
 
  @Override
  public boolean generate(FeatureContext<DefaultFeatureConfig> context) {
    BlockPos topPos = context.getWorld().getTopPosition(Heightmap.Type.OCEAN_FLOOR_WG, context.getOrigin());
    Direction offset = Direction.NORTH;
 
    for (int y = 0; y < 15; y++) {
      offset = offset.rotateYClockwise();
      context.getWorld().setBlockState(topPos.up(y).offset(offset), Blocks.STONE.getDefaultState(), 3);
    }
 
    return true;
  }
}

In our implementation, we'll build a simple 15-block tall spiral of stone starting at the top block in the world.

The Feature<DefaultFeatureConfig> constructor takes in a Codec<DefaultFeatureConfig>. You can pass in DefaultFeatureConfig.CODEC for default config features, either directly in the super call in the constructor or when you instantiate the feature.

generate is called when the chunk decides to generate the feature. If the feature is configured to spawn every chunk, this would be called for each chunk being generated as well. In the case of the feature being configured to spawn at a certain rate per biome, generate would only be called in instances where the world wants to spawn the structure.

Our feature uses a DefaultFeatureConfig at the moment, which means that it is not configurable. However, you should always try to make your features configurable, as that allows you to reuse it for different things, and also lets players change them through data packs if they want. A simple config for our feature could look like this:

public record SpiralFeatureConfig(IntProvider height, BlockStateProvider block) implements FeatureConfig {
  public static final Codec<SpiralFeatureConfig> CODEC = RecordCodecBuilder.create(instance -> instance.group(
    IntProvider.VALUE_CODEC.fieldOf("height").forGetter(SpiralFeatureConfig::height),
    BlockStateProvider.TYPE_CODEC.fieldOf("block").forGetter(SpiralFeatureConfig::block)
  ).apply(instance, instance.stable(SpiralFeatureConfig::new)));
}

Note that we use an IntProvider for the height, and a BlockStateProvider for the block instead of directly using an int or a BlockState. This is because they are more powerful to use. For example, you could configure the feature with a UniformIntProvider, so that its height can vary.

Now, let's make our feature use the SpiralFeatureConfig (the class name was also changed, as the feature doesn't always place stone now):

public class SpiralFeature extends Feature<SpiralFeatureConfig> {
  public SpiralFeature(Codec<SpiralFeatureConfig> configCodec) {
    super(configCodec);
  }
 
  @Override
  public boolean generate(FeatureContext<SpiralFeatureConfig> context) {
    BlockPos pos = context.getOrigin();
    SpiralFeatureConfig config = context.getConfig();
 
    Direction offset = Direction.NORTH;
    int height = config.height().get(context.getRandom());
 
    for (int y = 0; y < height; y++) {
      offset = offset.rotateYClockwise();
      BlockPos blockPos = pos.up(y).offset(offset);
 
      context.getWorld().setBlockState(blockPos, config.block().getBlockState(context.getRandom(), blockPos), 3);
    }
 
    return true;
  }
}

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 ExampleMod implements ModInitializer {
  private static final Feature<SpiralFeatureConfig> SPIRAL = new SpiralFeature(SpiralFeatureConfig.CODEC);
 
  @Override
  public void onInitialize() {
    Registry.register(Registry.FEATURE, new Identifier("tutorial", "spiral"), SPIRAL);
  }
}

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.

public class ExampleMod implements ModInitializer {
  public static final ConfiguredFeature<?, ?> STONE_SPIRAL = SPIRAL.configure(new SpiralFeatureConfig(ConstantIntProvider.create(15), new SimpleBlockStateProvider(Blocks.STONE.getDefaultState())))
      .decorate(Decorator.HEIGHTMAP.configure(new HeightmapDecoratorConfig(Heightmap.Type.OCEAN_FLOOR_WG)))
      .spreadHorizontally()
      .applyChance(5);
 
  @Override
  public void onInitialize() {
    [...]
 
    RegistryKey<ConfiguredFeature<?, ?>> stoneSpiral = RegistryKey.of(Registry.CONFIGURED_FEATURE_KEY,
        new Identifier("tutorial", "stone_spiral"));
    Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, stoneSpiral.getValue(), STONE_SPIRAL);
  }
}

Note that the height of the spiral (15 blocks) and the block to place (stone) is now configurable here.

The decorators (added with decorate or with helper methods like spreadHorizontally and applyChance) are responsible for how and where your feature will be placed. To choose the correct decorators, check out vanilla features with a similar style to your own.

Adding a configured feature to a biome

We use the Biome Modification API.

public class ExampleMod implements ModInitializer {
  [...]
 
  @Override
  public void onInitialize() {
    [...]
    BiomeModifications.addFeature(BiomeSelectors.all(), GenerationStep.Feature.UNDERGROUND_ORES, stoneSpiral);
  }
}

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.

Result

tutorial/features.txt · Last modified: 2021/08/31 13:21 by mschae23