AgentStack
SKILL verified MIT Self-run

Procedural Generation

skill-xeldaralz-everything-claude-unity-procedural-generation · by XeldarAlz

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

No reviews yet
0 installs
11 views
0.0% view→install

Install

$ agentstack add skill-xeldaralz-everything-claude-unity-procedural-generation

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No issues found. Passed automated security review. · v0.1.0 How review works →

  • Prompt-injection patterns
  • Secret / credential exfiltration
  • Dangerous shell & filesystem operations
  • Untrusted network calls
  • Known-malicious package signatures

What it can access

  • Network access No
  • Filesystem access No
  • Shell / process execution No
  • Environment & secrets No
  • Dynamic code execution No

From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.

Are you the author of Procedural Generation? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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.

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:

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
{
    /// 
    /// 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.

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

Reviews

No reviews yet — be the first.

Versions

  • v0.1.0 Imported from the upstream source.