← Back to projects

Procedural World Generation

A fully deterministic, data-driven rogue-like generation system

Role Prog
Team Solo
Timeline Academic Assignment May 2026 | Bachelor 1st Year
Stack Unity / C#

The Context

The goal of this project was to build a system capable of generating a simplified version of a rogue-like environment (like Aincrad in Sword Art Online but smaller). The core constraint was strict determinism: the system had to generate a complete set of 15 unique, reproducible levels based entirely on a single starting seed string.

100% DETERMINISTIC GENERATION

Dynamic generation of the overworld, organic villages, and BSP dungeons.

Architecture & Implementation

01. Data Matrices & Multi-Layered Noise

To optimize performance, the world is not generated by instantiating 3D blocks immediately. Instead, everything is processed as abstract data matrices.

I generate a raw terrain matrix using Perlin noise, normalize it against a water level threshold, and independently generate a biome matrix. These abstract grids are merged and evaluated before a single GameObject is instantiated in the scene.

WorldProceduralGeneration.cs MATRIX PROCESSING

void RegenerateMap(Vector2 seed)
{
    // 1. Dynamic map shrinking based on current floor level (RSO)
    int currentWidth = Mathf.Max(width - (runData.currentLevel - 1), 21);
    int currentHeight = Mathf.Max(height - (runData.currentLevel - 1), 21);

    // 2. Generate and normalize abstract Data Matrices
    float[,] rawTerrainMap = GenerateNoiseMap(currentWidth, currentHeight, scale, octaves, seed);
    float[,] normalizedTerrain = NormalizeTerrain(rawTerrainMap, waterLevel, seed, scale);

    Vector2 biomePosition = seed + new Vector2(10000f, 10000f);
    float[,] rawBiomeMap = GenerateNoiseMap(currentWidth, currentHeight, scale, octaves, biomePosition);
    float[,] normalizedBiome = NormalizeBiome(rawBiomeMap);

    // 3. Merge data before any physical instantiation
    float[,] finalMap = MergeTerrainWithBiome(normalizedTerrain, normalizedBiome);

    InstantiateMap(finalMap, rawTerrainMap);
    GenerateVillage(finalMap, seed, rawTerrainMap);
}

02. Binary Space Partitioning (BSP)

While the overworld village uses an organic growth algorithm to expand naturally, the Dungeon required a more clinical and structured approach.

I implemented a BSP (Binary Space Partitioning) algorithm to recursively subdivide the available space. This guarantees non-overlapping rooms and creates a methodical exploration loop before the player reaches the boss room.

BSP Generation Logic

The global space is recursively subdivided into varying-sized areas to guarantee a non-overlapping structure.

In-game Dungeon

Randomly sized rooms are calculated within each leaf node while strictly enforcing safety margins (padding).

In-game Dungeon

The BSP logic tree is traversed bottom-up to mathematically connect the center of each zone with corridors of a defined thickness.

03. Deterministic Seed Derivation

A true rogue-like engine must guarantee that a specific text seed (e.g., "Aincrad") will always generate the exact same 15-floor tower layout. Relying on Unity's default Random.InitState was not robust enough for a multi-layered progression system.

I created a custom Seed Derivation Pipeline. The engine takes the Master Seed string, hashes it into a numeric value, and derives a unique sub-seed for the current floor level. This sub-seed is then injected into an isolated System.Random (PRNG) instance passed down to all generation scripts (World, Village, Dungeon), ensuring absolute mathematical determinism regardless of the execution order.

SeedManager.cs / DungeonGenerator.cs PRNG INJECTION

// --- 1. SEED DERIVATION (In the Manager) ---
private void LaunchCurrentLevel()
{
    // Derive a unique seed based on the Master Hash and the Current Level
    Vector2 levelSeed = runData.GetDerivedLevelSeed();
    
    // Broadcast the deterministic mathematical constraint to the engine
    OnSeedReady?.RaiseEvent(levelSeed);
}

// --- 2. PRNG ISOLATION (In the Generators) ---
void GenerateDungeonDeterministic(Vector2 seed)
{
    // Create an isolated PRNG instance tied exclusively to this floor's seed
    System.Random prng = new System.Random(seed.GetHashCode());

    // Inject the PRNG into the generation pipeline
    GenerateDungeon(prng); 
}

// Every random decision (BSP splitting, room sizes) now uses 'prng.Next()'
// guaranteeing 100% reproducible results for this specific floor.