ChunkGenerator
s are the mechanisms by which the game generates the world. They handle terrain shaping, surface building, and biome placement.
Vanilla Minecraft's NoiseChunkGenerator
is extremely powerful and configurable. Using DensityFunction
s and SurfaceRule
s, the possibilities for customization are almost endless. If you're wondering “can I do x with NoiseChunkGenerator
and datapacks”, the answer is probably yes. Most situations do not call for a custom ChunkGenerator
, and since creating one requires a lot of boilerplate code, it is highly recommended that you avoid making custom ChunkGenerator
s when possible.
As a rule of thumb, you should only implement a ChunkGenerator
if you need to create a world that is not based on random noise. Examples of this might include a world generated from an external voxel engine, or a custom map like SethBling's Skygrid. If you're not sure whether a ChunkGenerator
is right for you, ask someone in the Discord.
Ready? Let's get started.
The first step in creating a chunk generator is making a new ChunkGenerator
class. Extend vanilla's ChunkGenerator
, and let your IDE generate all the required function stubs. Here's an explanation of everything you likely need to worry about:
public class ExampleChunkGenerator extends ChunkGenerator { /* this is a very important field, we will come back to the codec later */ public static final Codec<ExampleChunkGenerator> CODEC; /* you can add whatever fields you want to this constructor, as long as they're added to the codec as well */ public ExampleChunkGenerator() { } /* the method that creates non-noise caves (i.e., all the caves we had before the caves and cliffs update) */ @Override public void carve(ChunkRegion chunkRegion, long seed, NoiseConfig noiseConfig, BiomeAccess biomeAccess, StructureAccessor structureAccessor, Chunk chunk, GenerationStep.Carver carverStep) { } /* the method that places grass, dirt, and other things on top of the world, as well as handling the bedrock and deepslate layers, as well as a few other miscellaneous things. without this method, your world is just a blank stone (or whatever your default block is) canvas (plus any ores, etc) */ @Override public void buildSurface(ChunkRegion region, StructureAccessor structures, NoiseConfig noiseConfig, Chunk chunk) { } /* the method that paints biomes on top of the already-generated terrain. if you leave this method alone, the entire world will be a River biome. note that this does not mean that the world will all be water; but drowned and salmon will spawn. */ @Override public CompletableFuture<Chunk> populateBiomes(Registry<Biome> biomeRegistry, Executor executor, NoiseConfig noiseConfig, Blender blender, StructureAccessor structureAccessor, Chunk chunk) { return super.populateBiomes(biomeRegistry, executor, noiseConfig, blender, structureAccessor, chunk); } /* this method spawns entities in the world */ @Override public void populateEntities(ChunkRegion region) { } /* the distance between the highest and lowest points in the world. in vanilla, this is 384 (64+320) */ @Override public int getWorldHeight() { return 0; } /* this method builds the shape of the terrain. it places stone everywhere, which will later be overwritten with grass, terracotta, snow, sand, etc by the buildSurface method. it also is responsible for putting the water in oceans. it returns a CompletableFuture-- you'll likely want this to be delegated to worker threads. */ @Override public CompletableFuture<Chunk> populateNoise(Executor executor, Blender blender, NoiseConfig noiseConfig, StructureAccessor structureAccessor, Chunk chunk) { } @Override public int getSeaLevel() { return 0; } /* the lowest value that blocks can be placed in the world. in a vanilla world, this is -64. */ @Override public int getMinimumY() { return 0; } /* this method returns the height of the terrain at a given coordinate. it's used for structure generation */ @Override public int getHeight(int x, int z, Heightmap.Type heightmap, HeightLimitView world, NoiseConfig noiseConfig) { return 0; } /* this method returns a "core sample" of the world at a given coordinate. it's used for structure generation */ @Override public VerticalBlockSample getColumnSample(int x, int z, HeightLimitView world, NoiseConfig noiseConfig) { } /* this method adds text to the f3 menu. for NoiseChunkGenerator, it's the NoiseRouter line */ @Override public void getDebugHudText(List<String> text, NoiseConfig noiseConfig, BlockPos pos) { } @Override protected Codec<? extends ChunkGenerator> getCodec() { return CODEC; } }
Pretty much all of these methods need to be overridden for your generator to work properly. You can copy some of them from NoiseChunkGenerator
. It is especially recommended that you implement the private populateNoise
method from NoiseChunkGenerator
as well, and just copy over the public method. The public method from NoiseChunkGenerator
just delegates the actual generation to worker threads, which each run the private method. This is an easy way to get parallelized generation– the chunk generator is not multithreaded by default!
Like other features in the game, ChunkGenerator
s have to be registered. However, you don't pass the registry a static instance– instead, you give it a Codec
instance. You can put whatever you want in the codec– Mojang provides serialization codecs for a lot of useful objects, including entire registries. The easiest way to do this is via RecordCodecBuilder
. Here's an example codec:
public static final Codec<ExampleChunkGenerator> CODEC = RecordCodecBuilder.create(instance -> instance.group( BiomeSource.CODEC.fieldOf("biome_source").forGetter(ExampleChunkGenerator::getBiomeSource), Codec.INT.fieldOf("sea_level").forGetter(ExampleChunkGenerator::getSeaLevel), Codec.INT.fieldOf("world_height").forGetter(ExampleChunkGenerator::getWorldHeight), Identifier.fieldOf("custom_block").forGetter(ExampleChunkGenerator::getCustomBlockID) ).apply(instance, ExampleChunkGenerator::new));
Each line in the codec corresponds to a different field in your chunk generator that should be configurable. In this example, I pass a BiomeSource
and a few int
s. Most objects you will want to add into your chunk generator will have a Codec
defined for them. Primitives have theirs defined in the Codec
class, and other classes (for example, BiomeSource
or Identifier
) have theirs defined in their classes.
To use the fields properly, you have to add them, along with getters, into your class:
private final BiomeSource biomeSource; public BiomeSource getBiomeSource() { return this.biomeSource; } private final int seaLevel; public int getSeaLevel() { return this.seaLevel; } // and the same for the other fields
and then, create a constructor for your generator that uses each of those fields in the same order that you listed them in the codec:
public ExampleChunkGenerator(BiomeSource biomeSource, int seaLevel, int worldHeight, Identifier customBlockID) { super(biomeSource); this.seaLevel = seaLevel; this.worldHeight = worldHeight; this.customBlock = Registries.BLOCK.getOrThrow(RegistryKey.of(RegistryKeys.BLOCK, defaultBlock)).getDefaultState(); this.customBlockID = customBlockID; // this line is included because we need to have a getter for the ID specifically }
Then, all that's left is to register your chunk generator. Put the following line into your mod initializer onInitialize
method:
Registry.register(Registry.CHUNK_GENERATOR, new Identifier("wiki-example", "example"), ExampleChunkGenerator.CODEC);
Now, you can create json files to use your new chunk generator:
// resources/data/wiki-example/dimension/example.json { "type": "minecraft:overworld", "generator": { "type": "wiki-example:example", "custom_block": "minecraft:dirt", "sea_level": 0, "world_height": 384, "biome_source": // etc.
And you're done! If you want to use your new generator, it might be helpful to wrap it with a WorldPreset.