見出し画像

【Unity】3Dローグライクゲームの作り方〜Step12-7〜

前回の記事はこちら
前回は2階のマップを作成し、それを読み込みました。

今回参考にさせて頂いた記事

いつもお世話になっております。ありがとうございます!

ダンジョンの自動生成の手順

一般的なダンジョンは以下の手順で生成しています。
1. マップ配列(Array2D)を初期化
2. 全てを壁(=1)にする
3. マップ配列と同じサイズの区画を作成する
4. それ以上分割できないところまで区画を2等分していく
5. それぞれの区画内に部屋を作成する
6. 最後に通路を作成して、完成!
他にも実装しなければいけないものはありますが、とりあえずこれを踏まえてコードを作成していきます。

1. マップ配列を初期化する

これは簡単ですね。

Array2D data = new Array2D(w, h);

2. 全て壁にする

これも簡単です。

for (int x = 0; x < data.width; x++)
{
   for (int y = 0; y < data.height; y++)
   {
       data.Set(x, y, 1);
   }
}

3. マップ配列と同じサイズの区画を作成する

さあ、ここからが問題です。「区画」と言っても対応するクラスがありません。まあこれは新しくクラスを作成すれば問題ありませんが......。

public class Rect2D
{
   public int left;
   public int top;
   public int right;
   public int bottom;
   public int width { get{ return right - left + 1; } }
   public int height { get{ return bottom - top + 1; } }

   public Rect2D(int l, int t, int r, int b)
   {
       left = l;
       top = t;
       right = r;
       bottom = b;
   }
}

private class Area2D
{
   public Rect2D outLine;
   public Rect2D room;
}

これらを操作し、最初の区画を作成します。

areas = new List<Area2D>();
Area2D area = new Area2D();
area.outLine = new Rect2D(0, 0, w - 1, h - 1);

4. それ以上分割できないところまで区画を2等分していく

これは再帰処理を使います。

private void Split(Area2D baseArea, bool isVertical)
{
   Rect2D rect1, rect2;
   if (isVertical)
   {
       if (baseArea.outLine.left + minArea >= baseArea.outLine.right - minArea)
       {
           areas.Add(baseArea);
           return;
       }
       int p = Random.Range(baseArea.outLine.left + minArea, baseArea.outLine.right - minArea);
       rect1 = new Rect2D(baseArea.outLine.left, baseArea.outLine.top, p, baseArea.outLine.bottom);
       rect2 = new Rect2D(p + 1, baseArea.outLine.top, baseArea.outLine.right, baseArea.outLine.bottom);
       if ((rect1.width < rect2.width) ||
           (rect1.width == rect2.width && Random.Range(0, 2) == 0))
       {
           Rect2D tmp = rect1;
           rect1 = rect2;
           rect2 = tmp;
       }
   }
   else
   {
       if (baseArea.outLine.top + minArea >= baseArea.outLine.bottom - minArea)
       {
           areas.Add(baseArea);
           return;
       }
       int p = Random.Range(baseArea.outLine.top + minArea, baseArea.outLine.bottom - minArea);
       rect1 = new Rect2D(baseArea.outLine.left, baseArea.outLine.top, baseArea.outLine.right, p);
       rect2 = new Rect2D(baseArea.outLine.left, p + 1, baseArea.outLine.right, baseArea.outLine.bottom);
       if ((rect1.height < rect2.height) ||
           (rect1.height == rect2.height && Random.Range(0, 2) == 0))
       {
           Rect2D tmp = rect1;
           rect1 = rect2;
           rect2 = tmp;
       }
   }
   Area2D area1 = new Area2D();
   area1.outLine = rect1;
   Area2D area2 = new Area2D();
   area2.outLine = rect2;
   areas.Add(area2);
   Split(area1, !isVertical);
}

垂直に割る場合、以下のように横軸に分割ポイントを作ってあげる必要があります。水平の場合はその逆で、縦軸に作ります。

垂直分割

もし分割ポイントが作れない場合はそこで再帰を終了しています。
そして面積が大きい方を次の分割に使用させ、同じ大きさの時は乱数で決めることにしました。

5. それぞれの区画内に部屋を作成する

分割した区画内に部屋を作るコードです。

foreach (var area in areas)
{
   int aw = area.outLine.width - margin * 2;
   int ah = area.outLine.height - margin * 2;
   int width = Random.Range(minRoom, aw);
   int height = Random.Range(minRoom, ah);
   int rw = aw - width;
   int rh = ah - height;
   int rx = Random.Range(margin, rw - margin);
   int ry = Random.Range(margin, rh - margin);
   int left = area.outLine.left + rx;
   int top = area.outLine.top + ry;
   int right = left + width;
   int bottom = top + height;
   area.room = new Rect2D(left, top, right, bottom);
}

幅と高さをランダムで決めて、位置をずらしています。

6. 通路を作成する

最後に通路を作成します。

private void CreateHorizontalRoad(Area2D area1, Area2D area2)
{
   int y1 = Random.Range(area1.room.top, area1.room.bottom);
   int y2 = Random.Range(area2.room.top, area2.room.bottom);
   for (int x = area1.room.right; x < area1.outLine.right; x++)
       data.Set(x, y1, 0);
   for (int x = area2.outLine.left; x < area2.room.left; x++)
       data.Set(x, y2, 0);
   for (int y = Mathf.Min(y1, y2), end = Mathf.Max(y1, y2); y <= end; y++)
       data.Set(area1.outLine.right, y, 0);
}

private void CreateVerticalRoad(Area2D area1, Area2D area2)
{
   int x1 = Random.Range(area1.room.left, area1.room.right);
   int x2 = Random.Range(area2.room.left, area2.room.right);
   for (int y = area1.room.bottom; y < area1.outLine.bottom; y++)
       data.Set(x1, y, 0);
   for (int y = area2.outLine.top; y < area2.room.top; y++)
       data.Set(x2, y, 0);
   for (int x = Mathf.Min(x1, x2), end = Mathf.Max(x1, x2); x <= end; x++)
       data.Set(x, area1.outLine.bottom, 0);
}


for (int i = 0; i < areas.Count - 1; i++)
{
   if (areas[i].outLine.right < areas[i + 1].outLine.left)
       CreateHorizontalRoad(areas[i], areas[i + 1]);
   if (areas[i + 1].outLine.right < areas[i].outLine.left)
       CreateHorizontalRoad(areas[i + 1], areas[i]);
   if (areas[i].outLine.bottom < areas[i + 1].outLine.top)
       CreateVerticalRoad(areas[i], areas[i + 1]);
   if (areas[i + 1].outLine.bottom < areas[i].outLine.top)
       CreateVerticalRoad(areas[i + 1], areas[i]);
}

隣同士の時は縦軸に通路をのばすポイントを作成し、区画の端まで横に伸ばしています。そして右にのばしたポイントと左にのばしたポイントを縦に結びます。

通路作成

位置関係が上下のときはこの逆です。

まとめのコード

以上を踏まえて書いたコードがこちら。

using System.Collections.Generic;

private class RandomDungeon
{
   private const int minArea = 3;
   private const int minRoom = 1;
   private const int margin = 1;
   private Array2D data;
   private List<Area2D> areas;

   /**
   * ダンジョンを作成する
   */
   public Array2D Create(int w, int h)
   {
       data = new Array2D(w, h);
       for (int x = 0; x < data.width; x++)
       {
           for (int y = 0; y < data.height; y++)
           {
               data.Set(x, y, 1);
           }
       }
       areas = new List<Area2D>();
       Area2D area = new Area2D();
       area.outLine = new Rect2D(0, 0, w - 1, h - 1);
       Split(area, Random.Range(0, 2) == 0);
       CreateRooms();
       CreateRoads();
       return data;
   }

   /**
   * 区画を分割
   */
   private void Split(Area2D baseArea, bool isVertical)
   {
       Rect2D rect1, rect2;
       if (isVertical)
       {
           if (baseArea.outLine.left + minArea >= baseArea.outLine.right - minArea)
           {
               areas.Add(baseArea);
               return;
           }
           int p = Random.Range(baseArea.outLine.left + minArea, baseArea.outLine.right - minArea);
           rect1 = new Rect2D(baseArea.outLine.left, baseArea.outLine.top, p, baseArea.outLine.bottom);
           rect2 = new Rect2D(p + 1, baseArea.outLine.top, baseArea.outLine.right, baseArea.outLine.bottom);
           if ((rect1.width < rect2.width) ||
               (rect1.width == rect2.width && Random.Range(0, 2) == 0))
           {
               Rect2D tmp = rect1;
               rect1 = rect2;
               rect2 = tmp;
           }
       }
       else
       {
           if (baseArea.outLine.top + minArea >= baseArea.outLine.bottom - minArea)
           {
               areas.Add(baseArea);
               return;
           }
           int p = Random.Range(baseArea.outLine.top + minArea, baseArea.outLine.bottom - minArea);
           rect1 = new Rect2D(baseArea.outLine.left, baseArea.outLine.top, baseArea.outLine.right, p);
           rect2 = new Rect2D(baseArea.outLine.left, p + 1, baseArea.outLine.right, baseArea.outLine.bottom);
           if ((rect1.height < rect2.height) ||
               (rect1.height == rect2.height && Random.Range(0, 2) == 0))
           {
               Rect2D tmp = rect1;
               rect1 = rect2;
               rect2 = tmp;
           }
       }
       Area2D area1 = new Area2D();
       area1.outLine = rect1;
       Area2D area2 = new Area2D();
       area2.outLine = rect2;
       areas.Add(area2);
       Split(area1, !isVertical);
   }

   /**
   * 部屋を作成する
   */
   private void CreateRooms()
   {
       foreach (var area in areas)
       {
           int aw = area.outLine.width - margin * 2;
           int ah = area.outLine.height - margin * 2;
           int width = Random.Range(minRoom, aw);
           int height = Random.Range(minRoom, ah);
           int rw = aw - width;
           int rh = ah - height;
           int rx = Random.Range(margin, rw - margin);
           int ry = Random.Range(margin, rh - margin);
           int left = area.outLine.left + rx;
           int top = area.outLine.top + ry;
           int right = left + width;
           int bottom = top + height;
           area.room = new Rect2D(left, top, right, bottom);
           FillRoom(area.room);
       }
   }

   /**
   * マップ配列に部屋を作る
   */
   private void FillRoom(Rect2D room)
   {
       for (int x = room.left; x <= room.right; x++)
       {
           for (int y = room.top; y <= room.bottom; y++)
           {
               data.Set(x, y, 0);
           }
       }
   }

   /**
   * 水平方向に道をのばす
   */
   private void CreateHorizontalRoad(Area2D area1, Area2D area2)
   {
       int y1 = Random.Range(area1.room.top, area1.room.bottom);
       int y2 = Random.Range(area2.room.top, area2.room.bottom);
       for (int x = area1.room.right; x < area1.outLine.right; x++)
           data.Set(x, y1, 0);
       for (int x = area2.outLine.left; x < area2.room.left; x++)
           data.Set(x, y2, 0);
       for (int y = Mathf.Min(y1, y2), end = Mathf.Max(y1, y2); y <= end; y++)
           data.Set(area1.outLine.right, y, 0);
   }

   /**
   * 垂直方向に道をのばす
   */
   private void CreateVerticalRoad(Area2D area1, Area2D area2)
   {
       int x1 = Random.Range(area1.room.left, area1.room.right);
       int x2 = Random.Range(area2.room.left, area2.room.right);
       for (int y = area1.room.bottom; y < area1.outLine.bottom; y++)
           data.Set(x1, y, 0);
       for (int y = area2.outLine.top; y < area2.room.top; y++)
           data.Set(x2, y, 0);
       for (int x = Mathf.Min(x1, x2), end = Mathf.Max(x1, x2); x <= end; x++)
           data.Set(x, area1.outLine.bottom, 0);
   }

   /**
   * 道を作成
   */
   private void CreateRoads()
   {
       for (int i = 0; i < areas.Count - 1; i++)
       {
           if (areas[i].outLine.right < areas[i + 1].outLine.left)
               CreateHorizontalRoad(areas[i], areas[i + 1]);
           if (areas[i + 1].outLine.right < areas[i].outLine.left)
               CreateHorizontalRoad(areas[i + 1], areas[i]);
           if (areas[i].outLine.bottom < areas[i + 1].outLine.top)
               CreateVerticalRoad(areas[i], areas[i + 1]);
           if (areas[i + 1].outLine.bottom < areas[i].outLine.top)
               CreateVerticalRoad(areas[i + 1], areas[i]);
       }
   }

   public class Rect2D
   {
       public int left;
       public int top;
       public int right;
       public int bottom;
       public int width { get { return right - left + 1; } }
       public int height { get { return bottom - top + 1; } }

       public Rect2D(int l, int t, int r, int b)
       {
           left = l;
           top = t;
           right = r;
           bottom = b;
       }
   }

   private class Area2D
   {
       public Rect2D outLine;
       public Rect2D room;
   }
}

これをLoadFieldMapクラスの中に内部クラスとして追加しておいて下さい。

実際に使ってみる

それでは実際にこのコードを使ってみたいと思います。
LoadFieldMapクラスを以下のように変更します。

// パラメーターを追加
private RandomDungeon dungeon;

// メソッドを変更
void Start()
{
   dungeon = new RandomDungeon();
   Load();
}

private Array2D ReadMapFile(string path)
{
   try
   {
       /*   省略   */
       int w = int.Parse(map.Attribute("width").Value);
       int h = int.Parse(map.Attribute("height").Value);
       if (group == null) return dungeon.Create(w, h);
       Array2D data = null;
       foreach (var layer in group.Elements("layer"))
       /*   省略   */
}

実行して、0Fや3Fに行ってみます。以下のようにランダムにダンジョンが作成されればOKです。

スクリーンショット 2020-06-02 3.00.39

何度も試して、変なダンジョンにならないか確認しておきましょう。

とても長くなってしまいましたので、今回はここまでに致します。
次回はこのダンジョンの改良を行っていきたいと思います。

この記事が気に入ったらサポートをしてみませんか?