# Procedural Generation

> Procedural generation patterns — Perlin/Simplex noise, BSP dungeon generation, random walk, loot tables with weighted random, wave function collapse basics, seed-based reproducibility.

- **Type:** Skill
- **Install:** `agentstack add skill-xeldaralz-everything-claude-unity-procedural-generation`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [XeldarAlz](https://agentstack.voostack.com/s/xeldaralz)
- **Installs:** 0
- **Category:** [AI & ML](https://agentstack.voostack.com/c/ai-and-ml)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [XeldarAlz](https://github.com/XeldarAlz)
- **Source:** https://github.com/XeldarAlz/everything-claude-unity/tree/main/.claude/skills/gameplay/procedural-generation
- **Website:** https://github.com/XeldarAlz/everything-claude-unity#readme

## Install

```sh
agentstack add skill-xeldaralz-everything-claude-unity-procedural-generation
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# 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.

```csharp
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() Shuffle a list in place using Fisher-Yates.
    public void Shuffle(IList 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:

```csharp
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

```csharp
using UnityEngine;

public static class NoiseGenerator
{
    /// 
    /// Generate a 2D noise map. Values range from 0 to 1.
    /// 
    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  maxNoise) maxNoise = noiseHeight;
                if (noiseHeight  Left == null && Right == null;
    }

    private int _minPartitionSize;
    private int _minRoomSize;
    private int _roomPadding;
    private SeededRandom _rng;

    private List _rooms = new();
    private HashSet _corridors = new();

    public IReadOnlyList Rooms => _rooms;
    public IReadOnlyCollection 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);
        _rooms.Clear();
        _corridors.Clear();

        // 0 = wall, 1 = floor
        var grid = new int[width, height];

        // Build BSP tree
        var root = new BSPNode { Area = new RectInt(0, 0, width, height) };
        Split(root);

        // Place rooms in leaves
        PlaceRooms(root);

        // Connect rooms
        ConnectRooms(root);

        // Write rooms to grid
        foreach (var room in _rooms)
        {
            for (int x = room.x; x = 0 && pos.x = 0 && pos.y 
    /// Generate cave-like floor positions using a random walk.
    /// 
    public static HashSet DrunkWalk(
        Vector2Int startPos, int steps, int seed)
    {
        var rng = new SeededRandom(seed);
        var floorPositions = new HashSet { startPos };
        var current = startPos;

        Vector2Int[] directions = {
            Vector2Int.up, Vector2Int.down, Vector2Int.left, Vector2Int.right
        };

        for (int i = 0; i 
    /// Multiple random walks from the same origin for wider caves.
    /// 
    public static HashSet MultiWalk(
        Vector2Int startPos, int walks, int stepsPerWalk, int seed)
    {
        var rng = new SeededRandom(seed);
        var allFloors = new HashSet();

        for (int w = 0; w 
    /// Corridor-constrained walk: bias in one direction for path-like caves.
    /// 
    public static HashSet CorridorWalk(
        Vector2Int startPos, int length, Vector2Int primaryDirection, int seed,
        float straightChance = 0.7f)
    {
        var rng = new SeededRandom(seed);
        var positions = new HashSet { startPos };
        var current = startPos;

        Vector2Int[] perpendicular = primaryDirection.x != 0
            ? new[] { Vector2Int.up, Vector2Int.down }
            : new[] { Vector2Int.left, Vector2Int.right };

        for (int i = 0; i 
{
    public T item;
    public float weight;
}

public class WeightedRandom
{
    private List> _items = new();
    private float _totalWeight;

    public void Add(T item, float weight)
    {
        _items.Add(new WeightedItem { item = item, weight = weight });
        _totalWeight += weight;
    }

    public void AddRange(IEnumerable> items)
    {
        foreach (var item in items)
            Add(item.item, item.weight);
    }

    /// 
    /// Pick one item based on weights. Uses System.Random for determinism.
    /// 
    public T Pick(System.Random rng)
    {
        float roll = (float)rng.NextDouble() * _totalWeight;
        float cumulative = 0f;

        foreach (var entry in _items)
        {
            cumulative += entry.weight;
            if (roll 
    /// Pick N unique items (no repeats). For loot tables that drop multiple items.
    /// 
    public List PickMultipleUnique(int count, System.Random rng)
    {
        var result = new List();
        var remaining = new List>(_items);
        float remainingWeight = _totalWeight;

        for (int i = 0; i  0; i++)
        {
            float roll = (float)rng.NextDouble() * remainingWeight;
            float cumulative = 0f;

            for (int j = 0; j  Roll(System.Random rng, int pityCounter = 0)
    {
        var result = new List();

        // Guaranteed drops first
        foreach (var guaranteed in guaranteedDrops)
        {
            if (guaranteed != null)
                result.Add((guaranteed, 1));
        }

        // Random drops
        int dropCount = rng.Next(minDrops, maxDrops + 1);
        var table = new WeightedRandom();

        foreach (var entry in entries)
        {
            float weight = entry.weight;

            // Pity system: boost rare item weights when pity counter is high
            if (pityCounter >= pityThreshold && weight  allowedUp,
            1 => allowedRight,
            2 => allowedDown,
            3 => allowedLeft,
            _ => null
        };
    }
}

public class WFCGenerator
{
    private int _width, _height;
    private List _allTiles;
    private HashSet[,] _possibilities;
    private string[,] _result;
    private System.Random _rng;

    private static readonly Vector2Int[] Directions = {
        Vector2Int.up, Vector2Int.right, Vector2Int.down, Vector2Int.left
    };

    private static readonly int[] Opposite = { 2, 3, 0, 1 };

    public string[,] Generate(int width, int height, List tiles, int seed)
    {
        _width = width;
        _height = height;
        _allTiles = tiles;
        _rng = new System.Random(seed);
        _result = new string[width, height];
        _possibilities = new HashSet[width, height];

        // Initialize: every cell can be any tile
        var allIds = tiles.Select(t => t.tileId).ToHashSet();
        for (int x = 0; x (allIds);

        // Iterate until fully collapsed
        while (true)
        {
            var cell = FindLowestEntropyCell();
            if (cell == null) break; // All collapsed

            Collapse(cell.Value);
            Propagate(cell.Value);
        }

        return _result;
    }

    private Vector2Int? FindLowestEntropyCell()
    {
        int minEntropy = int.MaxValue;
        Vector2Int? best = null;

        for (int x = 0; x ();
        foreach (var id in possible)
        {
            var tile = _allTiles.First(t => t.tileId == id);
            table.Add(id, tile.weight);
        }

        string chosen = table.Pick(_rng);
        _possibilities[pos.x, pos.y] = new HashSet { chosen };
        _result[pos.x, pos.y] = chosen;
    }

    private void Propagate(Vector2Int start)
    {
        var stack = new Stack();
        stack.Push(start);

        while (stack.Count > 0)
        {
            var current = stack.Pop();

            for (int d = 0; d = _width ||
                    neighbor.y = _height)
                    continue;

                // Compute which tiles the neighbor can be, given current cell's possibilities
                var allowedAtNeighbor = new HashSet();
                foreach (var tileId in _possibilities[current.x, current.y])
                {
                    var tile = _allTiles.First(t => t.tileId == tileId);
                    foreach (var allowed in tile.GetAllowed(d))
                        allowedAtNeighbor.Add(allowed);
                }

                int before = _possibilities[neighbor.x, neighbor.y].Count;
                _possibilities[neighbor.x, neighbor.y].IntersectWith(allowedAtNeighbor);
                int after = _possibilities[neighbor.x, neighbor.y].Count;

                // If we reduced possibilities, propagate further
                if (after  Generate(
        float width, float height, float minDistance, int seed, int maxAttempts = 30)
    {
        var rng = new System.Random(seed);
        float cellSize = minDistance / Mathf.Sqrt(2f);
        int gridW = Mathf.CeilToInt(width / cellSize);
        int gridH = Mathf.CeilToInt(height / cellSize);
        var grid = new int[gridW, gridH];
        for (int x = 0; x ();
        var activeList = new List();

        // Start with a random point
        var initial = new Vector2(
            (float)rng.NextDouble() * width,
            (float)rng.NextDouble() * height);
        points.Add(initial);
        activeList.Add(0);
        grid[(int)(initial.x / cellSize), (int)(initial.y / cellSize)] = 0;

        while (activeList.Count > 0)
        {
            int activeIdx = rng.Next(0, activeList.Count);
            var point = points[activeList[activeIdx]];
            bool found = false;

            for (int attempt = 0; attempt = width ||
                    candidate.y = height)
                    continue;

                int cx = (int)(candidate.x / cellSize);
                int cy = (int)(candidate.y / cellSize);

                if (IsValid(candidate, grid, points, cellSize, minDistance, gridW, gridH))
                {
                    points.Add(candidate);
                    activeList.Add(points.Count - 1);
                    grid[cx, cy] = points.Count - 1;
                    found = true;
                    break;
                }
            }

            if (!found)
                activeList.RemoveAt(activeIdx);
        }

        return points;
    }

    private static bool IsValid(Vector2 candidate, int[,] grid, List points,
        float cellSize, float minDist, int gridW, int gridH)
    {
        int cx = (int)(candidate.x / cellSize);
        int cy = (int)(candidate.y / cellSize);

        for (int x = Mathf.Max(0, cx - 2); x = 0)
                {
                    if (Vector2.Distance(candidate, points[idx]) = 0; i--)
        {
            if (floor >= difficultyProfiles[i].floorNumber)
                return difficultyProfiles[i];
        }
        return difficultyProfiles[0];
    }
}
```

---

## Practical Tips

- **Generate data, then render.** Always separate generation logic (produces an int grid or list of positions) from rendering (applies tiles, spawns prefabs). This makes generation testable without Unity.
- **Validate output.** After generating a dungeon, flood-fill from the entrance to verify all rooms are reachable. If not, regenerate with a different seed.
- **Generation budget.** Complex generation can freeze the game. Run it in a coroutine (yield every N iterations) or on a background thread (for pure math, no Unity API calls).
- **Preview in editor.** Add `[ContextMenu("Generate")]` or a custom editor button to regenerate in edit mode. Fast iteration on generation parameters is essential.
- **Combine techniques.** Use BSP for room layout, random walk for cave-like connectors, noise for decorative detail within rooms, and Poisson disk for object placement.
- **Save the seed, not the map.** If generation is deterministic, you only need to save the seed and floor number to reconstruct the entire level. This keeps save files small.

## Source & license

This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [XeldarAlz](https://github.com/XeldarAlz)
- **Source:** [XeldarAlz/everything-claude-unity](https://github.com/XeldarAlz/everything-claude-unity)
- **License:** MIT
- **Homepage:** https://github.com/XeldarAlz/everything-claude-unity#readme

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** no
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/skill-xeldaralz-everything-claude-unity-procedural-generation
- Seller: https://agentstack.voostack.com/s/xeldaralz
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
