unity-procedural-gen について
Unity 開発者向けのプロシージャル生成パターンをまとめたスキルです。ノイズ、グリッド、ダンジョン、シード付き乱数、コンテンツ制約、エディターとランタイムでの生成を扱い、調整可能な設定と再現性、プレビューを重視します。
主な機能
- 01ノイズ・地形生成設定
- 02グリッドと配置ルール
- 03連結性検証付き BSP ダンジョン
- 04シードによる再現可能な乱数
- 05コンテンツ予算と制約検証
- 06エディターのベイクとランタイム生成
活用シーン
- 01バイオームごとの地形ノイズ調整
- 02通行可能な経路を持つダンジョン生成
- 03テスト結果や共有シードの再現
- 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-- ジェネレーターの出力をプレビューするカスタムインスペクター、EditorWindowunity-level-design-- 作成したコンテンツと生成コンテンツの統合unity-performance-- 非同期生成、チャンク単位の読み込み、生成メッシュの LODunity-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)-- 生成システムに焦点を当てたデザイン向け参考資料