mcpField
U
unity-procedural-gen

unity-procedural-gen

github.com/nice-wolf-studio开发4064 次安装6 周前更新
代码仓库

关于 unity-procedural-gen

面向 Unity 游戏开发者的程序化生成设计模式技能,涵盖噪声、网格、地牢、随机种子、内容约束及编辑器与运行时生成。重点是让生成参数可由设计师调整,并支持可复现结果和预览。

主要功能

  1. 01噪声与地形生成配置
  2. 02网格与放置规则
  3. 03BSP 地牢生成与连通性验证
  4. 04基于种子的可复现随机
  5. 05内容预算与约束校验
  6. 06编辑器烘焙及运行时生成

适用场景

  1. 01为不同生物群系调整地形噪声
  2. 02生成具有有效路径的地牢布局
  3. 03复现测试或玩家分享的关卡种子
  4. 04按难度配置敌人与道具数量

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/nice-wolf-studio/unity-claude-skills --skill unity-procedural-gen

安装后在 Claude Code 中输入/unity-procedural-gen确认已加载。

附带文件references/procgen-scaffolds.md

详细介绍

程序化生成——设计转换模式

前置技能: unity-3d-math(噪声数学、空间运算)、unity-data-driven(用于生成参数的 ScriptableObject 配置)、unity-editor-tools(预览工具、自定义检查器)

这些模式针对程序化生成中最常见的问题:Claude 生成的程序化内容使用硬编码算法,且没有设计师控制。结果虽然能运行一次,却无法调整、预览或约束。设计师需要可调参数、约束和预览工具,而不只是算法代码。每个生成系统都应将参数作为 ScriptableObject 配置公开,支持基于种子的可复现生成,并提供编辑器预览。


模式 1:基于噪声的生成

设计意图:设计师希望地形(“起伏的丘陵,偶尔出现山峰”)或摆放效果(“茂密的森林,间有空地”)呈现自然感。他们需要在不修改代码的情况下调整随机性的特征。

错误示例:

// Hardcoded single octave, no designer control, no reproducibility
float height = Mathf.PerlinNoise(x * 0.1f, y * 0.1f) * 10f;
terrain.SetHeight(x, y, height);

正确做法:使用带有频率、振幅、倍频程、持久度、间隙和偏移量的 NoiseConfig ScriptableObject。NoiseGenerator 静态工具类实现分形布朗运动(fBm)采样。支持域扭曲以获得自然的效果。所有参数都可由设计师编辑。

脚手架:

/// <summary>
/// Configuration for noise-based generation. Each biome or terrain type gets its own asset.
/// </summary>
[CreateAssetMenu(fileName = "New Noise Config", menuName = "ProcGen/Noise Config")]
public class NoiseConfig : ScriptableObject
{
    [Header("Base Noise")]
    [Range(0.001f, 1f)] public float frequency = 0.05f;
    [Range(0f, 100f)] public float amplitude = 10f;

    [Header("Fractal")]
    [Range(1, 8)] public int octaves = 4;
    [Range(0f, 1f)] public float persistence = 0.5f;
    [Range(1f, 4f)] public float lacunarity = 2f;

    [Header("Transform")]
    public Vector2 offset;
    [Range(0f, 1f)] public float domainWarpStrength = 0f;
}

/// <summary>
/// Stateless noise sampling utilities. Uses fBm for natural-looking results.
/// </summary>
public static class NoiseGenerator
{
    /// <summary>
    /// Samples 2D fractal Brownian motion noise at the given coordinates.
    /// </summary>
    /// <returns>Noise value scaled by config amplitude.</returns>
    public static float Sample2D(float x, float y, NoiseConfig config)
    {
        float sum = 0f;
        float freq = config.frequency;
        float amp = 1f;
        float maxAmp = 0f;

        for (int i = 0; i < config.octaves; i++)
        {
            float sampleX = (x + config.offset.x) * freq;
            float sampleY = (y + config.offset.y) * freq;
            sum += Mathf.PerlinNoise(sampleX, sampleY) * amp;
            maxAmp += amp;
            amp *= config.persistence;
            freq *= config.lacunarity;
        }

        return (sum / maxAmp) * config.amplitude;
    }
}

设计扩展点:设计师可在检查器中通过 [Range] 属性调整噪声。不同生物群系使用不同的 NoiseConfig ScriptableObject。通过 [ExecuteAlways] 和 OnValidate 重新生成预览。有关构建噪声预览 EditorWindow 的说明,请参阅 unity-editor-tools。

注意事项:Mathf.PerlinNoise 返回 0-1 范围内的值,而不是大多数噪声库(libnoise、FastNoiseLite)使用的 -1 到 1 范围。为确保行为一致,请在 NoiseConfig 中归一化或注明预期范围。如果需要 -1 到 1 范围,请使用 Mathf.PerlinNoise(x, y) * 2f - 1f。


模式 2:瓦片与网格系统

设计意图:基于网格进行摆放(城市建造游戏、农场模拟游戏、战术 RPG),并遵循相邻关系和有效摆放规则。设计师通过数据而非代码定义瓦片类型和摆放规则。

错误示例:

// Raw 2D array, adjacency rules buried in placement code, no validation
int[,] grid = new int[width, height];
grid[x, y] = tileId;
// Scattered checks like: if (grid[x-1, y] == WATER) canPlace = false;

正确做法:使用带有坐标辅助方法(邻居、边界、迭代)的 Grid<T> 泛型容器。每种瓦片类型对应一个 TileDefinition ScriptableObject。使用 IPlacementRule 接口验证相邻关系和摆放条件。GridPlacer 在摆放前根据所有规则进行验证。

脚手架:

/// <summary>
/// Generic 2D grid container with coordinate utilities.
/// </summary>
[System.Serializable]
public class Grid<T>
{
    [SerializeField] private int width;
    [SerializeField] private int height;
    [SerializeField] private float cellSize = 1f;
    private T[] cells;

    public int Width => width;
    public int Height => height;
    public float CellSize => cellSize;

    public Grid(int width, int height, float cellSize = 1f)
    {
        this.width = width;
        this.height = height;
        this.cellSize = cellSize;
        cells = new T[width * height];
    }

    /// <summary>Returns true if coordinates are within grid bounds.</summary>
    public bool InBounds(Vector2Int coord)
        => coord.x >= 0 && coord.x < width && coord.y >= 0 && coord.y < height;

    public T this[Vector2Int coord]
    {
        get => cells[coord.y * width + coord.x];
        set => cells[coord.y * width + coord.x] = value;
    }

    /// <summary>Converts grid coordinates to world position (center of cell).</summary>
    public Vector3 GridToWorld(Vector2Int coord)
        => new Vector3((coord.x + 0.5f) * cellSize, 0f, (coord.y + 0.5f) * cellSize);

    /// <summary>Converts world position to grid coordinates.</summary>
    public Vector2Int WorldToGrid(Vector3 worldPos)
        => new Vector2Int(Mathf.FloorToInt(worldPos.x / cellSize),
                          Mathf.FloorToInt(worldPos.z / cellSize));
}

/// <summary>Defines a tile type and its placement constraints.</summary>
[CreateAssetMenu(fileName = "New Tile", menuName = "ProcGen/Tile Definition")]
public class TileDefinition : ScriptableObject
{
    public string tileName;
    public GameObject prefab;
    public TileDefinition[] allowedNeighbors;
}

/// <summary>Validates whether a tile can be placed at a grid position.</summary>
public interface IPlacementRule
{
    bool CanPlace(Vector2Int coord, TileDefinition tile, Grid<TileDefinition> grid);
}

设计扩展点:通过创建带有允许邻居列表的 TileDefinition ScriptableObject 来添加新瓦片。使用 Gizmos 可视化网格状态,以便调试。规则可组合——添加新的 IPlacementRule 实现即可,无需修改摆放逻辑。

注意事项:网格坐标与世界坐标之间的转换务必显式处理。提供带可配置单元格大小的 GridToWorld(Vector2Int) 和 WorldToGrid(Vector3) 辅助方法。网格边缘的差一错误是最常见的 bug;每次访问前都要使用 InBounds()。

模式 3:地牢/房间生成

设计意图:Roguelike 游戏需要随机但有效的地牢布局,其中包含房间、走廊,以及一条从起点到出口的必经路径。设计师可以控制房间大小、密度和特殊房间的位置。

错误做法:

// Random placement with no overlap check, no connectivity guarantee
for (int i = 0; i < roomCount; i++)
{
    var room = new Rect(Random.Range(0, mapWidth), Random.Range(0, mapHeight),
                        Random.Range(5, 15), Random.Range(5, 15));
    rooms.Add(room); // May overlap, may be disconnected
}

正确做法:使用 BSP(二叉空间分割)来放置房间——递归地将空间划分为多个区域,在叶节点内放置房间,并用走廊连接兄弟叶节点。生成后进行验证:通过泛洪填充检查连通性、确保房间数量符合最小值和最大值要求,并确定从起点到出口的关键路径。

脚手架:

/// <summary>
/// Configuration for dungeon generation. Designers tune all parameters in Inspector.
/// </summary>
[CreateAssetMenu(fileName = "New Dungeon Config", menuName = "ProcGen/Dungeon Config")]
public class DungeonConfig : ScriptableObject
{
    [Header("Dimensions")]
    [Min(20)] public int mapWidth = 64;
    [Min(20)] public int mapHeight = 64;

    [Header("Rooms")]
    [Range(4, 20)] public int minRoomSize = 6;
    [Range(6, 30)] public int maxRoomSize = 14;
    [Range(3, 20)] public int roomCountMin = 5;
    [Range(5, 30)] public int roomCountMax = 12;

    [Header("BSP")]
    [Range(2, 8)] public int splitDepth = 4;
    [Range(0.3f, 0.7f)] public float minSplitRatio = 0.4f;

    [Header("Corridors")]
    [Range(1, 5)] public int corridorWidth = 2;

    [Header("Special Rooms")]
    public GameObject bossRoomPrefab;
    public GameObject treasureRoomPrefab;
}

/// <summary>Represents a room in the dungeon layout.</summary>
public class Room
{
    public RectInt Bounds { get; }
    public Vector2Int Center => new(Bounds.x + Bounds.width / 2,
                                    Bounds.y + Bounds.height / 2);
    public bool IsSpecial { get; set; }

    public Room(RectInt bounds) => Bounds = bounds;
}

/// <summary>Represents a corridor connecting two rooms.</summary>
public class Corridor
{
    public Vector2Int Start { get; }
    public Vector2Int End { get; }
    public int Width { get; }

    public Corridor(Vector2Int start, Vector2Int end, int width)
    {
        Start = start;
        End = end;
        Width = width;
    }
}

/// <summary>
/// Validates dungeon connectivity using flood fill.
/// </summary>
public static class ConnectivityValidator
{
    /// <summary>
    /// Returns true if all rooms are reachable from the first room.
    /// </summary>
    public static bool IsFullyConnected(Grid<bool> walkableGrid, List<Room> rooms)
    {
        if (rooms.Count == 0) return true;
        var visited = new HashSet<Vector2Int>();
        var queue = new Queue<Vector2Int>();
        queue.Enqueue(rooms[0].Center);
        visited.Add(rooms[0].Center);

        while (queue.Count > 0)
        {
            var current = queue.Dequeue();
            foreach (var neighbor in GetCardinalNeighbors(current))
            {
                if (walkableGrid.InBounds(neighbor)
                    && walkableGrid[neighbor]
                    && visited.Add(neighbor))
                {
                    queue.Enqueue(neighbor);
                }
            }
        }

        return rooms.All(r => visited.Contains(r.Center));
    }

    private static IEnumerable<Vector2Int> GetCardinalNeighbors(Vector2Int pos)
    {
        yield return pos + Vector2Int.up;
        yield return pos + Vector2Int.down;
        yield return pos + Vector2Int.left;
        yield return pos + Vector2Int.right;
    }
}

设计扩展点:DungeonConfig ScriptableObject 让设计师可以调整所有生成参数。特殊房间(Boss 房间、宝藏房间)可以使用预先制作的房间模板 Prefab,并在配置中指定。设计师无需修改代码即可添加新的特殊房间类型。

注意事项:如果不限制分割比例,BSP 可能会生成非常狭窄的房间——应在分割步骤中强制要求房间的最小尺寸,而不只是分割后再检查。如果只在分割后检查,就会浪费 BSP 迭代次数,最终生成的房间可能少于预期。


模式 4:种子与可复现性

设计意图:玩家可以分享种子(“试试种子 42!”),速通玩家需要可复现的布局,QA 可以通过重新输入种子来复现 Bug。给定同一个种子时,每次运行都必须保持确定性。

错误做法:

// Global mutable state, non-reproducible, state leaks between systems
int count = UnityEngine.Random.Range(3, 8);
var pos = new Vector3(UnityEngine.Random.Range(-10f, 10f), 0,
                      UnityEngine.Random.Range(-10f, 10f));

正确做法:使用 SeededRandom 封装 System.Random。主种子通过哈希派生出各子系统的种子(地牢布局、战利品、敌人放置分别使用独立的随机数流)。生成代码中完全不使用 UnityEngine.Random。种子会保存在存档数据中,并显示给玩家。

脚手架:

/// <summary>
/// Deterministic random number generator wrapping System.Random.
/// Each subsystem gets its own instance derived from a master seed.
/// </summary>
public class SeededRandom
{
    private readonly System.Random rng;

    /// <summary>The seed used to initialize this instance.</summary>
    public int Seed { get; }

    public SeededRandom(int seed)
    {
        Seed = seed;
        rng = new System.Random(seed);
    }

    /// <summary>Returns a random int in [min, max).</summary>
    public int Range(int min, int max) => rng.Next(min, max);

    /// <summary>Returns a random float in [0, 1).</summary>
    public float Float01() => (float)rng.NextDouble();

    /// <summary>Returns a random float in [min, max).</summary>
    public float Range(float min, float max) => min + Float01() * (max - min);

    /// <summary>
    /// Selects an item from weighted entries. Higher weight = more likely.
    /// </summary>
    public T WeightedChoice<T>(IReadOnlyList<(T item, float weight)> entries)
    {
        float totalWeight = 0f;
        foreach (var e in entries) totalWeight += e.weight;

        float roll = Float01() * totalWeight;
        float cumulative = 0f;
        foreach (var e in entries)
        {
            cumulative += e.weight;
            if (roll < cumulative) return e.item;
        }

        return entries[^1].item;
    }

    /// <summary>
    /// Derives a child seed for a named subsystem. Deterministic for the
    /// same master seed + subsystem name combination.
    /// </summary>
    public static int DeriveSeed(int masterSeed, string subsystem)
        => HashCode.Combine(masterSeed, subsystem.GetHashCode(StringComparison.Ordinal));
}

设计扩展点:种子可以由玩家输入(UI 中的文本字段),也可以自动生成(System.Environment.TickCount)。种子会显示在暂停菜单中,并与本次运行的数据一同保存。设计师可以在试玩测试期间锁定种子,以便进行一致的迭代。

注意事项:System.Random 不是线程安全的。如果使用 Jobs 系统或 Burst,请改用 Unity.Mathematics.Random(它是值类型,可安全地按线程使用)。切勿在线程之间共享同一个 System.Random 实例。此外,string.GetHashCode() 在不同 .NET 版本间并不确定——请使用 GetHashCode(StringComparison.Ordinal) 来稳定地派生种子。


模式 5:内容预算与约束系统

设计意图:生成的内容必须满足设计师设定的约束——每个房间至少有 3 个敌人,每层最多有 2 个宝箱,难度曲线从简单逐渐升高到困难。纯随机放置会导致游戏体验缺乏连贯性。

错误做法:

// No awareness of balance, guarantees, or budgets
for (int i = 0; i < Random.Range(1, 10); i++)
{
    var prefab = enemyPrefabs[Random.Range(0, enemyPrefabs.Length)];
    Instantiate(prefab, randomPosition, Quaternion.identity);
}

正确做法:ContentBudget ScriptableObject 定义每个类别的最小/最大数量、稀有度权重和必放内容。ContentPlacer 消耗预算并跟踪累计数量,优先确保达到最小数量。放置后进行验证,确保满足所有约束。

脚手架:

/// <summary>
/// A single entry in a content budget -- one prefab with placement rules.
/// </summary>
[System.Serializable]
public struct ContentEntry
{
    public GameObject prefab;
    public string category;
    [Range(0.01f, 100f)] public float weight;
    [Min(0)] public int minimum;
    [Min(0)] public int maximum;
}

/// <summary>
/// Defines what content can be placed and in what quantities.
/// One per floor, biome, or difficulty tier.
/// </summary>
[CreateAssetMenu(fileName = "New Content Budget", menuName = "ProcGen/Content Budget")]
public class ContentBudget : ScriptableObject
{
    [Tooltip("Total placement slots available for this budget.")]
    [Min(1)] public int totalSlots = 20;

    public ContentEntry[] entries;
}

/// <summary>
/// Places content according to budget constraints.
/// Guarantees minimums first, then fills with weighted random.
/// </summary>
public class ContentPlacer
{
    private readonly Dictionary<string, int> placedCounts = new();

    /// <summary>
    /// Generates a placement list respecting budget constraints.
    /// </summary>
    public List<GameObject> GeneratePlacements(ContentBudget budget, SeededRandom rng)
    {
        var result = new List<GameObject>();
        placedCounts.Clear();

        // Phase 1: Place all mandatory minimums
        foreach (var entry in budget.entries)
        {
            for (int i = 0; i < entry.minimum; i++)
            {
                result.Add(entry.prefab);
                IncrementCount(entry.category);
            }
        }

        // Phase 2: Fill remaining slots with weighted random
        int remaining = budget.totalSlots - result.Count;
        var eligible = BuildWeightedList(budget);

        for (int i = 0; i < remaining && eligible.Count > 0; i++)
        {
            var chosen = rng.WeightedChoice(eligible);
            result.Add(chosen.prefab);
            IncrementCount(chosen.category);

            // Rebuild eligible list (removes entries at max)
            eligible = BuildWeightedList(budget);
        }

        return result;
    }

    private List<(ContentEntry item, float weight)> BuildWeightedList(ContentBudget budget)
    {
        var list = new List<(ContentEntry, float)>();
        foreach (var entry in budget.entries)
        {
            int placed = placedCounts.GetValueOrDefault(entry.category);
            if (entry.maximum <= 0 || placed < entry.maximum)
                list.Add((entry, entry.weight));
        }
        return list;
    }

    private void IncrementCount(string category)
    {
        placedCounts.TryGetValue(category, out int count);
        placedCounts[category] = count + 1;
    }
}

设计扩展点:设计师可以为每一层或每个生物群系创建 ContentBudget ScriptableObject。使用 [Range] 和 [Min] 属性,在 Inspector 中调整权重和约束。不同难度等级使用不同预算(简单层 = 更多生命值补给,困难层 = 更多精英敌人)。

注意事项:加权随机必须先确保达到最小数量,再通过加权随机选择填满剩余预算。如果将必放内容和随机放置交错进行,预算耗尽时必放内容可能无法放入。务必验证 sum of all minimums <= totalSlots。


模式 6:运行时生成与烘焙生成

设计意图:某些内容在构建时或加载时生成一次(世界地图地形网格、静态环境);某些内容则按会话生成(地牢布局、战利品随机结果);还有一些内容需要在编辑器中预览,以便设计师迭代调整。生成算法应能在这三种场景中复用。

错误示例:

// Everything at runtime -- huge load spike, no editor preview
void Start()
{
    for (int x = 0; x < 1000; x++)
        for (int y = 0; y < 1000; y++)
            GenerateChunk(x, y); // Freezes for seconds
}

// Or everything baked -- no dynamic content
// Designer manually places 500 trees in the editor

正确做法:使用 IGenerator<TOutput> 接口及其 Generate(GenerationContext) 方法。GenerationContext 用于指示当前是运行时还是编辑器环境。编辑器生成器通过 AssetDatabase 生成资源;运行时生成器则生成轻量数据,供异步实例化使用。算法相同,输出目标不同。

脚手架:

/// <summary>Indicates whether generation runs in Editor (bake) or Runtime (dynamic).</summary>
public enum GenerationMode { Editor, Runtime }

/// <summary>Context passed to generators with mode and seed information.</summary>
public struct GenerationContext
{
    public GenerationMode Mode;
    public SeededRandom Random;
    public CancellationToken CancellationToken;
}

/// <summary>
/// Interface for all procedural generators. Implement once, run in Editor or Runtime.
/// </summary>
public interface IGenerator<TOutput>
{
    /// <summary>Generates output data from the given context.</summary>
    TOutput Generate(GenerationContext context);
}

// --- Runtime runner: async, non-blocking ---
public class RuntimeGeneratorRunner : MonoBehaviour
{
    /// <summary>
    /// Runs a generator asynchronously, yielding periodically to avoid frame spikes.
    /// </summary>
    public async Awaitable<TOutput> RunAsync<TOutput>(
        IGenerator<TOutput> generator, int seed)
    {
        var context = new GenerationContext
        {
            Mode = GenerationMode.Runtime,
            Random = new SeededRandom(seed),
            CancellationToken = destroyCancellationToken
        };

        // Run generation off the main thread if it's pure data
        return await Awaitable.FromAsyncOperation(
            System.Threading.Tasks.Task.Run(
                () => generator.Generate(context),
                context.CancellationToken));
    }
}

#if UNITY_EDITOR
// --- Editor runner: bakes output to asset ---
public static class EditorGeneratorRunner
{
    [UnityEditor.MenuItem("ProcGen/Bake Selected Generator")]
    private static void BakeFromMenu()
    {
        // Editor-only bake workflow
        // Produces assets saved to disk via AssetDatabase
    }
}
#endif

设计要点:同一生成器实现既可在编辑器中运行(通过 MenuItem 烘焙为资源),也可在运行时运行(使用异步 Awaitable)。设计师可以在自定义 EditorWindow 中或通过 [ExecuteAlways] 组件预览结果。有关构建生成预览窗口的信息,请参阅 unity-editor-tools。

注意事项:编辑器专用代码(AssetDatabase、EditorWindow、MenuItem)必须置于 #if UNITY_EDITOR 守卫内,否则运行时构建会因编译错误而失败。生成器逻辑应放在运行时可访问的代码中;只有烘焙工具代码需要置于守卫内。


生成策略对比

策略最适合的场景优势劣势
Perlin/Simplex 噪声(fBm)地形、高度图、生物群系分布、有机式布局连续、可平铺、参数直观不适用于离散结构(房间、走廊)
BSP(二叉空间分割)地牢房间、建筑平面图、空间划分保证不会重叠,能自然地连接走廊房间容易呈矩形,有机感较弱
波函数坍缩基于瓦片的关卡、街区、模式驱动生成自动遵循相邻约束,设计师可方便地提供示例大型网格处理速度慢,可能无法求解(需要回溯)
L 系统植物、分支结构、道路网络、河流规则表示精简,分支自然难以限制在特定边界内,规则编写不直观
泊松圆盘采样对象布局(树木、岩石、NPC)均匀分布且保持最小间距,外观自然密度固定,不适用于聚集式布局
Voronoi / Delaunay区域划分、生物群系边界、领地地图区域形状自然,双图适用于寻路需要外部库或自定义实现

反模式

反模式问题修复方法
硬编码噪声参数无法在不修改代码的情况下调整使用带有 [Range] 属性的 NoiseConfig ScriptableObject
在生成过程中使用 UnityEngine.Random全局状态、无法复现、状态泄漏使用封装 System.Random 的 SeededRandom
不验证连通性地牢中存在无法到达的房间生成后进行泛洪填充验证
在 Start() 中生成所有内容加载时出现帧卡顿或冻结使用异步 Awaitable 生成,并分散到多个帧中
忽略布局约束随机内容缺乏平衡使用施加最小值/最大值/权重约束的 ContentBudget SO
编辑器烘焙与运行时烘焙使用相同代码核心逻辑中充斥 #if UNITY_EDITOR使用 IGenerator<T> 接口,并采用独立的运行器
混淆网格坐标与世界坐标对象放置在错误位置使用明确的 GridToWorld / WorldToGrid 转换辅助方法
在线程间共享 System.Random竞争条件、输出不确定在 Jobs/Burst 中使用 Unity.Mathematics.Random(值类型)

相关技能

  • unity-3d-math -- 噪声数学基础、空间运算、坐标变换
  • unity-data-driven -- 用于生成参数的 ScriptableObject 配置层级
  • unity-editor-tools -- 自定义检查器、用于预览生成器输出的 EditorWindow
  • unity-level-design -- 将创作内容整合到生成内容中
  • unity-performance -- 异步生成、分块加载、生成网格的 LOD
  • unity-game-loop -- 相对于游戏状态转换的生成时机
  • unity-physics -- 对生成的放置结果进行碰撞验证(重叠检查)

其他资源

  • Unity.Mathematics.noise -- 适用于 GPU 的噪声函数,用于 Burst/Jobs(noise.cnoise、noise.snoise)
  • 泊松圆盘采样 -- Bridson 算法,用于均匀分布点
  • 波函数坍缩 -- 基于约束的图块放置(参见 OpenUPM 上的社区软件包)
  • Red Blob Games -- Amit Patel 关于网格、寻路和程序化生成的指南(redblobgames.com)
  • 游戏设计中的程序化生成(Tanya X. Short、Tarn Adams)-- 侧重设计的生成系统参考资料

评论

支持 Markdown