User Tools

Site Tools


tutorial:features

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
tutorial:features [2021/07/29 09:59] – Fixed a parameter rename mschae23tutorial:features [2023/12/18 01:19] (current) – [Adding a configured feature to a biome] update code solidblock
Line 1: Line 1:
-===== Adding Features [1.17] =====+===== Adding Features =====
 Rocks, trees, ores, and ponds are all examples of 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. 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.+In this tutorial, we'll generate a simple pillar feature.
  
-If you want to do something similar to vanilla (like ores), you should first look for an existing feature you can use. +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 "Create a feature" part of this tutorial.+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. There are 3 steps that are required to add a feature to a biome.
Line 12: Line 12:
   * Use [[https://github.com/FabricMC/fabric/pull/1097|Biome Modification API in Fabric API]] to add your feature to biomes.   * Use [[https://github.com/FabricMC/fabric/pull/1097|Biome Modification API in Fabric API]] to add your feature to biomes.
  
-Note that the Biome Modification API is marked as experimental. If the API doesn't work, consider using [[?rev=1599388928|the mixin version]]. 
  
 ==== Creating a feature ==== ==== Creating a feature ====
-A simple Feature looks like this:+Let's create a feature that spawns a 1x1 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.
  
-<code java> +We'll first create a new class for our feature. This is where the generation logic will go. 
-public class StoneSpiralFeature extends Feature<DefaultFeatureConfig> { + 
-  public StoneSpiralFeature(Codec<DefaultFeatureConfig> configCodec) {+<yarncode java> 
 +public class ExampleFeature extends class_3031<ExampleFeatureConfig> { 
 +  public ExampleFeature(Codec<FeatureConfig> configCodec) {
     super(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   @Override
-  public boolean generate(FeatureContext<DefaultFeatureConfig> context) { +  public boolean method_13151(class_5821<ExampleFeatureConfig> context) { 
-    BlockPos topPos = context.getWorld().getTopPosition(Heightmap.Type.OCEAN_FLOOR_WG, context.getOrigin()); +        class_5281 world = context.method_34383()
-    Direction offset Direction.NORTH;+        // the origin is the place where the game starts trying to place the feature 
 +        class_2338 origin = context.getOrigin(); 
 +        // we won't use the random herebut we could if we wanted to 
 +        class_5819 random = context.method_33654(); 
 +        ExampleFeatureConfig config context.method_33656();
  
-    for (int 0; y < 15; y++) { +        // don't worry about where these come from-- we'll implement these methods soon 
-      offset = offset.rotateYClockwise(); +        int number config.number(); 
-      context.getWorld().setBlockState(topPos.up(y).offset(offset), Blocks.STONE.getDefaultState(), 3); +        class_2960 blockId = config.blockId();
-    }+
  
-    return true+        class_2680 blockState = class_7923.field_41175.get(blockId).method_9564()
-  } +        // ensure the ID is okay 
-+        if (blockState == null) throw new IllegalStateException(blockId + " could not be parsed to a valid block identifier!");
-</code>+
  
-In our implementationwe'll build a simple 15-block tall spiral of stone starting at the top block in the world.+        // find the surface of the world 
 +        class_2338 testPos = new class_2338(origin); 
 +        for (int y = 0; y < world.method_31605(); y++) { 
 +            testPos = testPos.method_10086(); 
 +            // the tag name is dirtbut includes grass, mud, podzol, etc. 
 +            if (world.method_8320(testPos).isIn(class_3481.field_29822)) { 
 +                if (world.method_8320(testPos.method_10086()).isOf(class_2246.field_10124)) { 
 +                    for (int i = 0; i < number; i++) { 
 +                        // create a simple pillar of blocks 
 +                        world.method_8501(testPos, blockState, 0x10); 
 +                        testPos = testPos.method_10086();
  
-The ''Feature<DefaultFeatureConfig>'' constructor takes in a ''Codec<DefaultFeatureConfig>''+                        // ensure we don't try to place blocks outside the world 
-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.+                        if (testPos.getY() >= world.method_8624()) break; 
 +                    } 
 +                    return true; 
 +                } 
 +            } 
 +        } 
 +       // the game couldn't find a place to put the pillar 
 +        return false; 
 +    } 
 +}
  
-''generate'' is called when the chunk decides to generate the feature +</yarncode> 
-If the feature is configured to spawn every chunk, this would be called for each chunk being generated as well. +Now, we need to implement that ''ExampleFeatureConfig'' recordThis 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 featureNote: 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''BlockStateProvider''s are a good example of this. 
-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 +<yarncode java> 
- +public record ExampleFeatureConfig(int numberIdentifier blockIdimplements FeatureConfig 
-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, +    public static final Codec<ExampleFeatureConfig> CODEC = RecordCodecBuilder.create( 
-as that allows you to reuse it for different things, and also lets players change them through data packs if they want. +        instance -> instance.group( 
-A simple config for our feature could look like this+                        // you can add as many of these as you want, one for each parameter 
- +                        Codecs.POSITIVE_INT.fieldOf("number").forGetter(ExampleFeatureConfig::number), 
-<code java> +                        Identifier.CODEC.fieldOf("blockID").forGetter(ExampleFeatureConfig::blockId)
-public record SpiralFeatureConfig(IntProvider heightBlockStateProvider block) { +                .apply(instance, ExampleFeatureConfig::new));
-  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)));+
 } }
-</code>+</yarncode>
  
-Note that we use an ''IntProvider'' for the heightand a ''BlockStateProvider'' for the block instead of directly using an int or a ''BlockState''+Now that we have our config defined, the errors in our feature class will resolveBut 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.
-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'':+
  
 <code java> <code java>
-public class SpiralFeature extends Feature<SpiralFeatureConfig> +public class ExampleMod implements ModInitializer 
-  public StoneSpiralFeature(Codec<SpiralFeatureConfig> configCodec{ +    public static final Identifier EXAMPLE_FEATURE_ID = new Identifier("tutorial", "example_feature"); 
-    super(configCodec); +    public static final ExampleFeature EXAMPLE_FEATURE = new ExampleFeature(ExampleFeatureConfig.CODEC);
-  }+
  
-  @Override +    @Override 
-  public boolean generate(FeatureContext<SpiralFeatureConfig> context) { +    public void onInitialize() { 
-    BlockPos pos = context.getOrigin(); +        Registry.register(Registries.FEATUREEXAMPLE_FEATURE_IDEXAMPLE_FEATURE);
-    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(blockPosconfig.block().getBlockState(context.getRandom(), blockPos)3);+
     }     }
- 
-    return true; 
-  } 
 } }
 </code> </code>
  
-Features can be registered like most other content in the gameand there aren't any special builders or mechanics you'll have to worry about+If you plan to configure and use your feature using datapacks, you can stop here. To implement it in coderead on.
  
-<code java> +==== Configuring a feature ==== 
-public class ExampleMod implements ModInitializer { +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.
-  private static final Feature<SpiralFeatureConfig> SPIRAL new SpiralFeature(SpiralFeatureConfig.CODEC);+
  
-  @Override +<yarncode java> 
-  public void onInitialize() { +public class FeatureExampleMod implements ModInitializer {
-    Registry.register(Registry.FEATURE, new Identifier("tutorial", "spiral"), SPIRAL); +
-  } +
-+
-</code>+
  
-==== Configuring a feature ==== +    public static final Identifier EXAMPLE_FEATURE_ID new Identifier("tutorial", "example_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 static final ExampleFeature EXAMPLE_FEATURE new ExampleFeature(ExampleFeatureConfig.CODEC);
  
-<code java> +    public static final ConfiguredFeature<ExampleFeatureConfigExampleFeatureEXAMPLE_FEATURE_CONFIGURED = new ConfiguredFeature<>( 
-public class ExampleMod implements ModInitializer { +                    EXAMPLE_FEATURE, 
-  public static final ConfiguredFeature<??STONE_SPIRAL SPIRAL.configure(new SpiralFeatureConfig(ConstantIntProvider.create(15), new SimpleBlockStateProvider(Blocks.STONE.getDefaultState()))) +                    new ExampleFeatureConfig(10, new Identifier("minecraft", "netherite_block"))); 
-      .decorate(Decorator.HEIGHTMAP.configure(new HeightmapDecoratorConfig(Heightmap.Type.OCEAN_FLOOR_WG))) +    );
-      .spreadHorizontally() +
-      .applyChance(5);+
  
-  @Override + 
-  public void onInitialize() { +    @Override 
-    [...] +    public void onInitialize() { 
-     +        Registry.register(Registry.FEATUREEXAMPLE_FEATURE_IDEXAMPLE_FEATURE); 
-    RegistryKey<ConfiguredFeature<?, ?>> stoneSpiral = RegistryKey.of(Registry.CONFIGURED_FEATURE_KEY, +        Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, EXAMPLE_FEATURE_IDEXAMPLE_FEATURE_CONFIGURED); 
-        new Identifier("tutorial""stone_spiral")); +    }
-    Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, stoneSpiral.getValue()STONE_SPIRAL); +
-  }+
 } }
-</code>+</yarncode
  
-Note that the height of the spiral (15 blocks) and the block to place (stone) is now configurable here. +FIXME The last line has to be updated
- +
-The Decorator represents how and where the world will place your feature. +
-To choose the correct Decorator, check out vanilla features with a similar style to your own.+
  
 ==== Adding a configured feature to a biome ==== ==== Adding a configured feature to a biome ====
-We use the Biome Modification API.+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!
  
-<code java>+Our final initializer class looks like this: 
 +<yarncode java>
 public class ExampleMod implements ModInitializer { public class ExampleMod implements ModInitializer {
-  [...] 
  
-  @Override +    public static final Identifier EXAMPLE_FEATURE_ID = new Identifier("tutorial", "example_feature"); 
-  public void onInitialize() { +    public static final ExampleFeature EXAMPLE_FEATURE = new ExampleFeature(ExampleFeatureConfig.CODEC); 
-    [...] +    public static final ConfiguredFeature<ExampleFeatureConfig, ExampleFeature> EXAMPLE_FEATURE_CONFIGURED = new ConfiguredFeature<>
-    BiomeModifications.addFeature(BiomeSelectors.all(), GenerationStep.Feature.UNDERGROUND_ORESstoneSpiral); +                    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(class_7923.field_41144, EXAMPLE_FEATURE_ID, EXAMPLE_FEATURE); 
 + 
 +        // 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(RegistryKeys.PLACED_FEATURE, EXAMPLE_FEATURE_ID)); 
 +    }
 } }
-</code>+</yarncode>
  
 The first argument of ''addFeature'' determines what biomes the structure is generated in. The first argument of ''addFeature'' determines what biomes the structure is generated in.
Line 150: Line 161:
 For above-ground houses you may go with ''SURFACE_STRUCTURES'', and for caves, you might go with ''RAW_GENERATION''. For above-ground houses you may go with ''SURFACE_STRUCTURES'', and for caves, you might go with ''RAW_GENERATION''.
  
-=== Result === +For more information, the code used in this tutorial is available [[https://github.com/itsmiir/wiki-example/tree/1.19/src/main/java/com/miir/example/feature|on Github]]. 
-{{https://i.imgur.com/Kr59o0B.png}}+ 
  
tutorial/features.1627552748.txt.gz · Last modified: 2021/07/29 09:59 by mschae23