tutorial:features
Differences
This shows you the differences between two versions of the page.
| Both sides previous revisionPrevious revisionNext revision | Previous revision | ||
| tutorial:features [2021/07/29 09:59] – Fixed a parameter rename mschae23 | tutorial:features [2024/10/28 17:58] (current) – Fix new identifier vs Identifier.of cassiancc | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| - | ===== Adding Features | + | ===== 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' | + | In this tutorial, we' |
| - | 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 | + | In that case, skip the "Creating |
| 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:// | * Use [[https:// | ||
| - | Note that the Biome Modification API is marked as experimental. If the API doesn' | ||
| ==== 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, |
| - | <code java> | + | We'll first create a new class for our feature. This is where the generation logic will go. |
| - | public class StoneSpiralFeature | + | |
| - | public | + | <yarncode |
| + | public class ExampleFeature | ||
| + | public | ||
| 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 | + | public boolean |
| - | | + | |
| - | | + | // 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 here, but we could if we wanted to | ||
| + | class_5819 random = context.method_33654(); | ||
| + | | ||
| - | for (int y = 0; y < 15; y++) { | + | // don't worry about where these come from-- we'll implement these methods soon |
| - | offset = offset.rotateYClockwise(); | + | |
| - | | + | |
| - | } | + | |
| - | 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 implementation, we'll build a simple | + | // find the surface of the world |
| + | class_2338 testPos = new class_2338(origin); | ||
| + | for (int y = 0; y < world.method_31605(); | ||
| + | testPos = testPos.method_10086(); | ||
| + | // the tag name is dirt, but 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 | ||
| + | | ||
| + | testPos = testPos.method_10086(); | ||
| - | The '' | + | // ensure we don't try to place blocks outside the world |
| - | You can pass in '' | + | if (testPos.getY() |
| + | } | ||
| + | return true; | ||
| + | } | ||
| + | } | ||
| + | } | ||
| + | // the game couldn't find a place to put the pillar | ||
| + | return false; | ||
| + | } | ||
| + | } | ||
| - | '' | + | </ |
| - | If the feature | + | Now, we need to implement that '' |
| - | In the case of the feature being configured to spawn at a certain rate per biome, | + | <yarncode |
| - | + | public record | |
| - | Our feature | + | 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. | + | |
| - | 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(" | |
| - | <code java> | + | |
| - | public record | + | .apply(instance, |
| - | public static final Codec<SpiralFeatureConfig> CODEC = RecordCodecBuilder.create(instance -> instance.group( | + | |
| - | | + | |
| - | | + | |
| - | ).apply(instance, | + | |
| } | } | ||
| - | </code> | + | </yarncode> |
| - | Note that we use an '' | + | Now that we have our config defined, the errors in our feature |
| - | This is because they are more powerful to use. For example, you could configure | + | |
| - | + | ||
| - | Now, let's make our feature | + | |
| <code java> | <code java> | ||
| - | public class SpiralFeature extends Feature< | + | public class ExampleMod implements ModInitializer |
| - | public | + | public |
| - | | + | |
| - | } | + | |
| - | | + | |
| - | public | + | public |
| - | | + | |
| - | 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(), | + | |
| } | } | ||
| - | |||
| - | return true; | ||
| - | } | ||
| } | } | ||
| </ | </ | ||
| - | Features | + | If you plan to configure and use your feature using datapacks, you can stop here. To implement it in code, read 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 '' |
| - | private static final Feature< | + | |
| - | @Override | + | < |
| - | public | + | public |
| - | Registry.register(Registry.FEATURE, | + | |
| - | } | + | |
| - | } | + | |
| - | </ | + | |
| - | ==== Configuring a feature ==== | + | public static final Identifier EXAMPLE_FEATURE_ID |
| - | 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 |
| - | <code java> | + | |
| - | public class ExampleMod implements ModInitializer { | + | EXAMPLE_FEATURE, |
| - | | + | |
| - | | + | ); |
| - | .spreadHorizontally() | + | |
| - | .applyChance(5); | + | |
| - | | + | |
| - | public void onInitialize() { | + | |
| - | [...] | + | public void onInitialize() { |
| - | + | | |
| - | RegistryKey< | + | Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, |
| - | new Identifier(" | + | } |
| - | Registry.register(BuiltinRegistries.CONFIGURED_FEATURE, | + | |
| - | } | + | |
| } | } | ||
| - | </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 '' |
| - | <code java> | + | Our final initializer class looks like this: |
| + | <yarncode | ||
| public class ExampleMod implements ModInitializer { | public class ExampleMod implements ModInitializer { | ||
| - | [...] | ||
| - | | + | public static final Identifier EXAMPLE_FEATURE_ID = Identifier.of(" |
| - | public void onInitialize() { | + | public static final ExampleFeature EXAMPLE_FEATURE = new ExampleFeature(ExampleFeatureConfig.CODEC); |
| - | [...] | + | public static final ConfiguredFeature< |
| - | BiomeModifications.addFeature(BiomeSelectors.all(), GenerationStep.Feature.UNDERGROUND_ORES, stoneSpiral); | + | EXAMPLE_FEATURE, |
| - | } | + | new ExampleFeatureConfig(10, |
| + | ); | ||
| + | // 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()) | ||
| + | ); | ||
| + | |||
| + | | ||
| + | public void onInitialize() { | ||
| + | // register the features | ||
| + | Registry.register(class_7923.field_41144, | ||
| + | |||
| + | // add it to overworld biomes using FAPI | ||
| + | | ||
| + | | ||
| + | // the feature is to be added while flowers and trees are being generated | ||
| + | | ||
| + | RegistryKey.of(RegistryKeys.PLACED_FEATURE, | ||
| + | } | ||
| } | } | ||
| - | </code> | + | </yarncode> |
| The first argument of '' | The first argument of '' | ||
| Line 150: | Line 161: | ||
| For above-ground houses you may go with '' | For above-ground houses you may go with '' | ||
| - | === Result === | + | For more information, |
| - | {{https://i.imgur.com/Kr59o0B.png}} | + | |
tutorial/features.1627552748.txt.gz · Last modified: 2021/07/29 09:59 by mschae23