ダンジョンゲーム向け ランダムマップ生成設計
目的
プレイヤーに「毎回異なる攻略体験」を提供しつつ、生成失敗や進行不能を発生させない、安定したランダムマップ生成を実現する。
また、単純な処理の軽量化を目的とするのではなく、ゲームとしての面白さを維持した上で、生成アルゴリズムを最適化することを設計方針とする。
設計思想
一般的には、
- バグのないマップ生成
- 処理負荷の軽減
を優先するあまり、生成ルールを単純化し、結果として似たようなマップや単調な攻略体験になりやすい。
本設計では、その考え方を採用しない。
目指すべきは、
「面白いマップを生成すること」を前提とし、その実現に必要なルールを整理・制限することで、処理負荷を抑えることである。
つまり、
面白いマップ生成 + 処理の軽量化
= 必要なルールのみを定義した、複雑さを制御できる生成アルゴリズム
という考え方を基本方針とする。
生成の自由度を無制限に広げるのではなく、「ゲームとして面白い」と判断できる条件をルール化し、その範囲内でランダム性を持たせることで、毎回異なる体験と安定した生成を両立する。
マップ記号
記号内容
□
未使用マス
▲
スタートルーム
■
メインルート
◆
サブルート
▼
ゴールルーム
◯
次階層接続ポイント
※「◯」は部屋ではなく、次階層・次マップへ接続するための予約領域である。
ボード構成
マップサイズは常に 8×8 とする。
各マスには固定のボードIDを割り当てる。
例
- A1 = X0 Y0
- C4 = X2 Y3
- H8 = X7 Y7
ボードIDは生成内容に関係なく常に固定であり、変更されない。
マップ生成フロー
生成は以下の順序で実行する。
- 既存マップデータの初期化
- スタート地点の決定
- メインルート長の決定
- メインルート生成
- ゴールルームの決定
- 次階層接続ポイント(◯)の配置
- サブルート生成
- 部屋同士の接続情報生成
- 扉情報生成
- デバッグマップ出力
スタート地点生成ルール
スタート地点は StartArea の設定に応じて生成する。
選択可能な範囲
- 全域
- 外周
- 中央付近
- 外周または中央
初期設定は 「外周または中央」 とする。
完全なランダム配置では単調なマップになりやすいため、スタート位置をある程度制限することで、より探索性の高いダンジョンを生成しやすくする。
メインルート生成ルール
メインルートはスタート地点(▲)から開始する。
移動可能方向は以下の4方向。
- 上
- 下
- 左
- 右
以下の条件は禁止とする。
- マップ外への移動
- 自身との重複
- ゴール横に配置される接続ポイント(◯)を配置できなくなる形
ルート終端の部屋はゴール(▼)となる。
メインルート生成アルゴリズム
ルート生成は経路探索方式で行う。
各ステップでは隣接マスを評価し、それぞれにスコアを付与する。
評価対象
- 直進よりも適度な方向転換
- 周囲に十分な空きスペースが存在すること
- 一本道になり過ぎない構造
- 適度なランダム性
評価を下げる条件
- 長時間の直進
- 早期の行き止まり
- 既存ルートとの重複
選択した経路が条件を満たせなくなった場合はバックトラックを行い、別ルートを探索する。
これにより、一本道ではないダンジョンらしい構造を生成する。
サブルート生成ルール
サブルートはメインルート生成後に配置する。
生成条件
- 必ず既存の部屋へ接続すること
- サブルート同士の接続は許可する
- 他の部屋と重複しないこと
- ゴールへ接続しないこと
- ゴール隣接マスへ生成しないこと
- 次階層接続ポイント(◯)を使用しないこと
これにより、ゴール周辺の構造を維持しながら探索用の分岐を追加する。
private bool IsValidSideRoomCandidate(Vector2Int candidate)
{
if (!IsInsideBoard(candidate)) return false;
if (board[candidate.x, candidate.y] != null) return false;
if (IsNextPoint(candidate)) return false;
if (IsNextToRoomType(candidate, RoomType.End)) return false;
return true;
}ゴールルーム生成ルール
ゴールルームはメインルート最後の部屋とする。
生成条件
- 直前のメインルート部屋と接続すること
- サブルートとは接続しないこと
- 通常部屋との接続数を最大2本までとする
- 必ず次階層接続ポイント(◯)への扉を持つこと
結果としてゴールルームは
- 通常接続 1〜2本
- 次階層接続用の特殊扉 1本
を持つ。
次階層接続ポイント(◯)
次階層接続ポイントはゴールルームに隣接して配置する。
このポイントは部屋ではない。
役割
部屋の数に含めない- 次階層生成用の予約領域
- ゴール専用の遷移ポイント
フロア1→フロア2のような階層型ダンジョン生成へ対応するための設計である。
private void OpenEndDoorToNextPoint()
{
if (!hasNextPoint)
{
return;
}
Vector2Int endPosition = mainRoutePositions[mainRoutePositions.Count - 1];
RoomData endRoom = board[endPosition.x, endPosition.y];
endRoom.OpenDoor(nextPointDirection);
}扉情報生成
部屋配置完了後、全ての部屋について隣接マスを確認する。
隣接部屋が存在する方向の扉を開放する。
対象方向
- 上
- 下
- 左
- 右
その後、ゴールルームのみ追加処理を行う。
- サブルート接続を除外
- 接続数を制限
- 次階層接続ポイントへの扉を生成
ボードID管理
各マスには、チェス盤と同様の命名規則を採用し、一意のボードIDを割り当てる。
例
- A1
- B4
- H8
ボードIDは、内部で使用する座標(X,Y)とは別に、人が認識しやすい識別子として利用する。
この方式を採用することで、
- デバッグログの可読性向上
- マップの状態確認の容易化
- セーブデータやイベント管理での識別
- 開発者同士の共通認識の確立
といった利点が得られる。
例えば、「X=3, Y=5」よりも「D6」の方が直感的に位置を把握しやすく、デバッグや仕様書においても理解しやすい表現となる。
ボードIDはマップ生成後も変更されず、各部屋の固定識別子として扱う。
public static string GetBoardId(Vector2Int boardPosition)
{
char column = (char)('A' + boardPosition.x);
int row = boardPosition.y + 1;
return column.ToString() + row;
}最適化方針
8×8固定マップであるため、生成処理自体の負荷は非常に小さい。
採用している最適化
- 配列による高速アクセス
- データ生成を先に行う構成
- レイアウト生成中はプレハブを生成しない
- シンプルな部屋データ構造
- 隣接部屋への直接参照
本設計で重視するのは処理速度ではなく、扱いやすく整合性の高いマップデータを構築することである。
将来的な拡張
生成されたマップデータは以下の用途へ利用可能である。
- プレハブ生成
- 扉生成
- セーブ・ロード
- デバッグ表示
- 次階層生成
- マップイベント制御
- 階層間接続
各部屋は以下の情報を保持する。
- ボードID
- 座標
- 部屋種別
- 隣接部屋情報
- 扉情報
これらの情報を基盤とすることで、将来的な機能追加やシステム拡張にも柔軟に対応できる。