Procedural Generation Patterns
Patterns for generating content at runtime: terrain with noise, dungeons with BSP, caves with random walk, loot with weighted tables, and tile layouts with wave function collapse. All patterns support seed-based reproducibility.
Seed-Based Reproducibility
Every generation algorithm should accept a seed. Given the same seed, the output is identical. This enables shareable worlds, bug reproduction, and daily challenge modes.
Critical rule: Use System.Random (not UnityEngine.Random) for deterministic generation. UnityEngine.Random is a global singleton; any other code calling it between your generation steps will change the sequence.
public class SeededRandom
{
private System.Random _rng;
public int Seed { get; }
public SeededRandom(int seed)
{
Seed = seed;
_rng = new System.Random(seed);
}
public int Next(int min, int max) => _rng.Next(min, max);
public float NextFloat() => (float)_rng.NextDouble();
public float Range(float min, float max) => min + (max - min) * NextFloat();
public bool Chance(float probability) => NextFloat() < probability;
/// <summary>Shuffle a list in place using Fisher-Yates.</summary>
public void Shuffle<T>(IList<T> list)
{
for (int i = list.Count - 1; i > 0; i--)
{
int j = _rng.Next(0, i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
}
For world generation, derive sub-seeds from the master seed so different systems (terrain, dungeons, loot) do not interfere:
int masterSeed = 12345;
var terrainRng = new SeededRandom(masterSeed);
var dungeonRng = new SeededRandom(masterSeed + 1);
var lootRng = new SeededRandom(masterSeed + 2);
Noise-Based Terrain Generation
Use Perlin noise to generate height maps for terrain, biome maps, moisture maps, and other continuous fields.
Basic Height Map
using UnityEngine;
public static class NoiseGenerator
{
/// <summary>
/// Generate a 2D noise map. Values range from 0 to 1.
/// </summary>
public static float[,] GenerateNoiseMap(
int width, int height, int seed,
float scale, int octaves, float persistence, float lacunarity,
Vector2 offset)
{
var map = new float[width, height];
// Use seed to generate random octave offsets
var rng = new System.Random(seed);
var octaveOffsets = new Vector2[octaves];
for (int i = 0; i < octaves; i++)
{
float ox = rng.Next(-100000, 100000) + offset.x;
float oy = rng.Next(-100000, 100000) + offset.y;
octaveOffsets[i] = new Vector2(ox, oy);
}
if (scale <= 0f) scale = 0.001f;
float maxNoise = float.MinValue;
float minNoise = float.MaxValue;
float halfW = width / 2f;
float halfH = height / 2f;
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float amplitude = 1f;
float frequency = 1f;
float noiseHeight = 0f;
for (int o = 0; o < octaves; o++)
{
float sampleX = (x - halfW + octaveOffsets[o].x) / scale * frequency;
float sampleY = (y - halfH + octaveOffsets[o].y) / scale * frequency;
// Mathf.PerlinNoise returns 0-1; remap to -1 to 1
float perlin = Mathf.PerlinNoise(sampleX, sampleY) * 2f - 1f;
noiseHeight += perlin * amplitude;
amplitude *= persistence; // Each octave contributes less
frequency *= lacunarity; // Each octave has finer detail
}
map[x, y] = noiseHeight;
if (noiseHeight > maxNoise) maxNoise = noiseHeight;
if (noiseHeight < minNoise) minNoise = noiseHeight;
}
}
// Normalize to 0-1
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
map[x, y] = Mathf.InverseLerp(minNoise, maxNoise, map[x, y]);
return map;
}
}
Parameter guide: | Parameter | Effect | Typical Value | |-------------|--------------------------------|---------------| | scale | Zoom level (higher = smoother) | 20-100 | | octaves | Layers of detail | 4-6 | | persistence | Amplitude decay per octave | 0.4-0.6 | | lacunarity | Frequency increase per octave | 1.8-2.2 |
Applying Noise to a Tilemap
using UnityEngine;
using UnityEngine.Tilemaps;
public class TerrainGenerator : MonoBehaviour
{
[SerializeField] private Tilemap tilemap;
[SerializeField] private TileBase waterTile;
[SerializeField] private TileBase sandTile;
[SerializeField] private TileBase grassTile;
[SerializeField] private TileBase stoneTile;
[SerializeField] private TileBase snowTile;
[Header("Generation Settings")]
[SerializeField] private int width = 100;
[SerializeField] private int height = 100;
[SerializeField] private int seed = 42;
[SerializeField] private float scale = 30f;
[SerializeField] private int octaves = 4;
[SerializeField] private float persistence = 0.5f;
[SerializeField] private float lacunarity = 2f;
[Header("Height Thresholds")]
[SerializeField] private float waterLevel = 0.3f;
[SerializeField] private float sandLevel = 0.4f;
[SerializeField] private float grassLevel = 0.7f;
[SerializeField] private float stoneLevel = 0.85f;
public void Generate()
{
tilemap.ClearAllTiles();
var noiseMap = NoiseGenerator.GenerateNoiseMap(
width, height, seed, scale, octaves, persistence, lacunarity, Vector2.zero);
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
float value = noiseMap[x, y];
TileBase tile = GetTileForHeight(value);
tilemap.SetTile(new Vector3Int(x - width / 2, y - height / 2, 0), tile);
}
}
}
private TileBase GetTileForHeight(float height)
{
if (height < waterLevel) return waterTile;
if (height < sandLevel) return sandTile;
if (height < grassLevel) return grassTile;
if (height < stoneLevel) return stoneTile;
return snowTile;
}
}
BSP Dungeon Generation
Binary Space Partition creates rectangular rooms connected by corridors. It produces clean, grid-aligned dungeons suitable for roguelikes and RPGs.
Algorithm Overview
- Start with a large rectangle (the entire dungeon area).
- Recursively split it in half (horizontally or vertically) until pieces reach minimum size.
- Place a room inside each leaf partition (with random padding).
- Connect sibling rooms with corridors.
Implementation
using System.Collections.Generic;
using UnityEngine;
public class BSPDungeon
{
public class BSPNode
{
public RectInt Area;
public BSPNode Left;
public BSPNode Right;
public RectInt? Room;
public bool IsLeaf => Left == null && Right == null;
}
private int _minPartitionSize;
private int _minRoomSize;
private int _roomPadding;
private SeededRandom _rng;
private List<RectInt> _rooms = new();
private HashSet<Vector2Int> _corridors = new();
public IReadOnlyList<RectInt> Rooms => _rooms;
public IReadOnlyCollection<Vector2Int> Corridors => _corridors;
public BSPDungeon(int minPartitionSize = 10, int minRoomSize = 4, int roomPadding = 2)
{
_minPartitionSize = minPartitionSize;
_minRoomSize = minRoomSize;
_roomPadding = roomPadding;
}
public int[,] Generate(int width, int height, int seed)
{
_rng = new SeededRandom(seed);
_ro