mcpField
U
unity-procedural-gen

unity-procedural-gen

github.com/nice-wolf-studio開発4064 回インストール6 週間前に更新
リポジトリ

unity-procedural-gen について

Unity 開発者向けのプロシージャル生成パターンをまとめたスキルです。ノイズ、グリッド、ダンジョン、シード付き乱数、コンテンツ制約、エディターとランタイムでの生成を扱い、調整可能な設定と再現性、プレビューを重視します。

主な機能

  1. 01ノイズ・地形生成設定
  2. 02グリッドと配置ルール
  3. 03連結性検証付き BSP ダンジョン
  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) のヘルパーを用意します。グリッド端でのオフバイワンエラーは最もよくあるバグです。アクセスの前には必ず InBounds() を使ってください。


パターン 3:ダンジョン/部屋の生成

設計意図:ローグライクでは、部屋、通路、開始地点から出口までの確実な経路を備えた、ランダムでありながら有効なダンジョンレイアウトが必要です。デザイナーは部屋のサイズ、密度、特殊な部屋の配置を制御します。

誤り:

// 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 により、デザイナーはすべての生成パラメーターを調整できます。ボス部屋や宝物部屋などの特殊な部屋には、あらかじめ作成したプレハブの部屋テンプレートを用意し、設定内で割り当てられます。デザイナーはコードを変更せずに、新しい特殊な部屋の種類を追加できます。

注意点:分割比率が制限されていないと、BSP によって非常に狭い部屋が生成されることがあります。分割後だけでなく、分割の段階で部屋の最小寸法を適用してください。分割後にだけ確認すると、BSP の反復が無駄になり、要求された数より少ない部屋しか生成されないことがあります。


パターン 4:シードと再現性

設計意図:プレイヤーはシードを共有し(「シード 42 を試してみて!」)、スピードランナーには再現可能なレイアウトが必要で、QA はシードを再入力してバグを再現します。同じシードであれば、すべてのプレイが決定論的でなければなりません。

誤り:

// 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));

正しい方法:System.Random をラップする SeededRandom を使用します。マスターシードからハッシュを使ってサブシステムごとのシードを導出し、ダンジョンのレイアウト、戦利品、敵の配置それぞれに独立したストリームを割り当てます。生成コードでは 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:実行時生成と焼き込み生成

設計意図:コンテンツによっては、ビルド時/読み込み時に一度だけ生成します(ワールドマップの地形メッシュ、静的な環境など)。セッションごとに生成するものもあります(ダンジョンのレイアウト、戦利品の抽選など)。デザイナーが反復作業を行えるよう、エディターでのプレビューが必要なものもあります。生成アルゴリズムは、この3つすべてのコンテキストで再利用できる必要があります。

誤り:

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

正しい例:Generate(GenerationContext) を備えた IGenerator<TOutput> インターフェースを使用します。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 Noise(fBm)地形、高さマップ、バイオームの分布、有機的な配置連続性があり、タイル状に配置でき、パラメーターが直感的です個別の構造物(部屋、通路)には適しません
BSP(Binary Space Partition)ダンジョンの部屋、建物のフロアプラン、空間分割重なりが発生せず、自然な通路の接続が保証されます部屋が長方形になりやすく、有機的な印象に欠けます
Wave Function Collapseタイルベースのレベル、市街区、パターン駆動型の生成隣接制約を自動的に守り、デザイナーが扱いやすい例を入力できます大きなグリッドでは遅く、解を見つけられない場合があります(バックトラッキングが必要です)
L-Systems植物、分岐構造、道路網、河川コンパクトなルール表現で、自然な分岐を生成できます特定の範囲内に収めるのが難しく、ルールの作成が直感的ではありません
Poisson Disk Samplingオブジェクトの配置(木、岩、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 -- Burst/Jobs 向けの GPU 対応ノイズ関数(noise.cnoise、noise.snoise)
  • ポアソンディスクサンプリング -- 点を均等に配置する Bridson のアルゴリズム
  • 波動関数崩壊 -- 制約ベースのタイル配置(OpenUPM のコミュニティパッケージを参照)
  • Red Blob Games -- グリッド、経路探索、プロシージャル生成に関する Amit Patel のガイド(redblobgames.com)
  • ゲームデザインにおけるプロシージャル生成(Tanya X. Short、Tarn Adams)-- 生成システムに焦点を当てたデザイン向け参考資料

コメント