Unityでローグライクなゲームを作ってみる(3) ダンジョン作成編

# はじめに
パート2ではマスタデータの読み込みについて書かせていただきました。
パート3以降ではいよいよダンジョンの作成を行っていきます！


# 手順
ざっくりとした手順です。
1. 任意のブロックを配置する関数を用意
2. 1の関数を使って四角形の部屋を作成
3. マスタデータの通りに作成するようにして完成


# 配置するオブジェクトについて
ありきたりですが、配置するオブジェクトは2次元の配列で管理します。
床だったり壁だったりと、様々なオブジェクトを配置します。
配置するルールは以下のとおりです。
画像の例は3つの部屋を作ったときです。
![floor_image.png](https://qiita-image-store.s3.amazonaws.com/0/177910/d0658160-916b-3189-5083-d7c73ab5a54e.png)
最初にある程度の配列を確保し、なるべく全体で正方形を描くように配置します。


# オブジェクトを配置する
まずは単純なオブジェクトを生成する関数を用意します。
床や壁を指定できるようにしておくと後で拡張しやすいです。

また、ダンジョンを生成するクラスは[partial](https://docs.microsoft.com/ja-jp/dotnet/articles/csharp/language-reference/keywords/partial-type)なクラスでそれぞれ作成し、
Generator.Block（ブロックを配置）、Generator.Room（部屋を配置）
といった役割ごとにクラスを分けていこうと思います。（あとで説明します）

```Generator.Block.cs

public enum Type
{
    None,
    Floor, // 床.
    Wall,  // 壁.
    RoomFloor, // 部屋用の床.
}

/// <summary>
/// ダンジョン内に配置するオブジェクトを生成します.
/// </summary>
/// <returns>生成したオブジェクトを返します.</returns>
public static GameObject GenerateObject(Type type, float x, float y)
{
    GameObject obj = null;

    // とりあえず単純なキューブを生成します.
    obj = GameObject.CreatePrimitive( PrimitiveType.Cube );
    // :
    // レイヤーや位置などを設定します.
    // :
    // 生成したオブジェクトを返します.
    return obj;
}
```
とりあえずキューブを配置する処理を書いてます。
一番最後に種類に応じたブロックを生成するように拡張します。


# 部屋を作成する

配置するオブジェクトの情報クラスと、
それを配列で持つフロアの情報クラスを準備します。

```ObjectInformation.cs
// ダンジョンに配置するオブジェクトの参照や、
// 配置するオブジェクトの種類などを持ちます.
public class ObjectInformation
{
    public GameObject RefObject { get; set; } // GameObjectでなく、配置物のコンポーネントなどでもOK.
}
```

```FloorInformation.cs
// ObjectInformationの配列を持ちます.
public class FloorInformation
{
    /// <summary>
    /// 配置オブジェクトのパラメータを返します.
    /// </summary>
    public ObjectInformation[][] Objects
    {
        get { return objects; }
        set { objects = value; }
    }
    private ObjectInformation[][] objects;
}
```

これで配置物を格納するクラスが準備できました。

次に、部屋を作成する関数を準備します。
引数に部屋1つ1つランダムで大きさを決めるため、最小サイズと最大サイズをそれぞれ渡します。
戻り値は先程作ったフロア情報クラスです。生成された情報が格納されてます。
また、最初ですべての値が最大だったときの広さ分の配列を確保して部屋を生成していきます。

```Generator.Room.cs
public static partial class Generator
{
    public static partial class Room
    {
        /// <summary>
        /// 部屋を作ります.
        /// </summary>
        /// <param name="widthMin"></param>
        /// <param name="heightMin"></param>
        /// <param name="widthMax"></param>
        /// <param name="heightMax"></param>
        /// <param name="distanceMin"></param>
        /// <param name="distanceMax"></param>
        /// <param name="count"></param>
        /// <returns></returns>
        public static FloorInformation Generate(int widthMin, int heightMin, int widthMax, int heightMax, int distanceMin, int distanceMax, int count)
        {
            // 壁となる外枠のサイズを計算します.
            // 3マスの部屋を作ると、壁が自動的に計算されて外枠含めると5x5、移動可能は3x3の部屋ができます.
            widthMin     += additinalSize;
            heightMin    += additinalSize;
            widthMax     += additinalSize;
            heightMax    += additinalSize;

            // 部屋を何行（何列）並べるかを決定します.
            var roomCol = Mathf.RoundToInt(Mathf.Sqrt(count));
            var roomRow = roomCol;
            roomRow = (roomCol * roomRow) < count ? roomRow + 1 : roomRow; // 足りないときは増やす.

            // リスト（配列）の数を、使用する最大数のブロック数で確保します（ランダムに変化するので）.
            var widthSize  = (widthMax  + additinalSize) * roomCol  + distanceMax * (roomCol - 1);
            var heightSize = (heightMax + additinalSize) * roomRow  + distanceMax * (roomRow - 1);

            FloorInformation data = CreateFloor(widthSize, heightSize); // 上記で計算したサイズでフロア情報を生成します.

            // 部屋ごとにサイズをきめてランダム生成します.
            int startX = 0;
            int startY = 0;
            for (int i = 0; i < count; ++i)
            {
                var rw = Random.Range(widthMin,    widthMax);
                var rh = Random.Range(heightMin,   heightMax);
                var rd = Random.Range(distanceMin, distanceMax);

                SetBlocks(data, new Vector2(startX, startY), rw, rh); // 部屋1つ分を配置する関数(後述).

                startX += rw + rd;

                if ((i + 1) % roomCol == 0) // 次の行へ進めます.
                {
                    startX = 0;
                    startY += rh + rd;
                }
            }

            return data;
        }
    }
}
```

上記のようなまず部屋の規模を計算する関数を用意し、
その中で部屋1つについて配置していく関数を呼んでいます。

部屋を1つ分だけ配置する関数を用意します。
先ほどの関数で使われていたもので、外枠なら壁を、それ以外はとりあえず床を配置していきます。

```Generator.Room.cs
/// <summary>
/// ある地点から床と壁を配置していき、部屋を生成します.
/// </summary>
private static FloorInformation SetBlocks(FloorInformation data, int x, int y, int width, int height)
{
    // 部屋の大きさに合わせて壁や床を配置していきます.
    int hMax = y + height;
    int wMax = x + width;
    for (int i = y; i < hMax; ++i)
    {
        for (int n = x; n < wMax; ++n)
        {
            // この座標に新しくオブジェクトを生成します.
            int generatePosX = n - data.Objects[i].Length / 2;
            int generatePosY = (data.Objects.Length / 2) - i;

            Constant.Dungeon.BlockType generateType = Constant.Dungeon.BlockType.None;

            // 壁となる位置（外枠）なら壁を生成します.
            if (i == y || i == (hMax - 1) || n == x || n == (wMax - 1))
            {
                generateType = Constant.Dungeon.BlockType.Wall;
            }
            else
            {
                generateType = Constant.Dungeon.BlockType.RoomFloor; // 部屋の床としておくことで、普通の床と区別します.
            }

            data.Objects[i][n] = Generator.Block.Generate(
                generateType,
                generatePosX,
                generatePosY
                );
        }
    }
    return data;
}
```

Generator.Room.Generate を呼んでみます。
部屋の大きさや、数を渡してあげればランダムな大きさの部屋がいくつか生成できると思います。

下の図のようにいくつものブロックが配置されていると思います。
![floor_block.png](https://qiita-image-store.s3.amazonaws.com/0/177910/67a2699e-f2b5-d69f-2a6b-5712f526d34b.png)

最後にブロックの見た目を変えましょう。

```Generator.Block.cs
GameObject.CreatePrimitive( PrimitiveType.Cube );
```

↓変更例

```Generator.Block.cs
private static GameObject CreateBlock(Dungeon.ObjectInformation target)
{
    string resourceName = string.Empty;

    switch (target.BlockType)
    {
        case Constant.Dungeon.BlockType.Wall:
            // 壁オブジェクトを生成.
            break;

        default:
            // 床オブジェクトを生成.
            break;
    }
}

```

![floor_block2.png](https://qiita-image-store.s3.amazonaws.com/0/177910/a8dcdf46-6042-3888-9a36-edcf5c536ff4.png)
※素材は[こちら](https://www.assetstore.unity3d.com/jp/#!/content/65665)を使用しております。

以上で部屋の作成が完了しました！
次回は部屋と部屋をつなぐ仕組みを考えてみる予定です。
お疲れ様でした。