見出し画像

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

前回の記事はこちら
前回はアイテムの種類を増やしました。

ミニマップが表示されない?

再起動後、何故かミニマップが表示されなくなったので、修正します。

private void Start()
{
   RectTransform rect = roadImage.GetComponent<RectTransform>();
   pw = rect.sizeDelta.x;
   ph = rect.sizeDelta.y;
   Reset(map.width, map.height);
}

最後にResetメソッドを呼ぶことで改善されます。

ダンジョン生成の改善

そろそろ現在のダンジョン生成に不満が出てきたので、改善したいと思います。
主に下記の記事の最後の方に掲載されている改良版に沿って改善します。

ちょっと変更が多いのでLoadFieldMapスクリプト全体を載せます。

using System.Collections.Generic;
using UnityEngine;
using System.Xml.Linq;

public class LoadFieldMap : MonoBehaviour
{
   public string mapName;
   public DungeonInfo dungeonInfo;
   public Field field;

   private RandomDungeon dungeon;

   // Start is called before the first frame update
   void Start()
   {
       dungeon = new RandomDungeon();
       Load();
   }

   /**
   * マップを読み込む
   */
   public void Load()
   {
       field.Reset();
       Array2D mapdata = ReadMapFile(mapName);
       if (mapdata != null)
       {
           field.Create(mapdata);
       }
   }

   /**
   * Z軸に対して反対の値を返す
   */
   private int ToMirrorX(int xgrid, int mapWidth)
   {
       return mapWidth - xgrid - 1;
   }

   /**
   * TMXファイルからマップデータを取得する
   */
   private Array2D ReadMapFile(string path)
   {
       try
       {
           XDocument xml = XDocument.Load(path);
           XElement map = xml.Element("map");
           XElement group = null;
           foreach (var gp in map.Elements("group"))
           {
               if (gp.Attribute("name").Value.Equals(Field.floorNum + "F"))
               {
                   group = gp;
                   break;
               }
           }
           if (group == null) return null;
           int w = int.Parse(map.Attribute("width").Value);
           int h = int.Parse(map.Attribute("height").Value);
           int pw = int.Parse(map.Attribute("tilewidth").Value);
           int ph = int.Parse(map.Attribute("tileheight").Value);
           Array2D data = null;
           foreach (var layer in group.Elements("layer"))
           {
               switch (layer.Attribute("name").Value)
               {
                   case "Tile Layer":
                       string[] sdata = (layer.Element("data").Value).Split(',');
                       w = int.Parse(layer.Attribute("width").Value);
                       h = int.Parse(layer.Attribute("height").Value);
                       data = new Array2D(w, h);
                       for (int z = 0; z < h; z++)
                       {
                           for (int x = 0; x < w; x++)
                           {
                               data.Set(x, z, int.Parse(sdata[ToMirrorX(x, w) + z * w]) - 1);
                           }
                       }

                       break;
               }
           }
           foreach (var objgp in group.Elements("objectgroup"))
           {
               switch (objgp.Attribute("name").Value)
               {
                   case "Object Layer":
                       foreach (var obj in objgp.Elements("object"))
                       {
                           int x = int.Parse(obj.Attribute("x").Value);
                           int z = int.Parse(obj.Attribute("y").Value);
                           int rw = int.Parse(obj.Attribute("width").Value);
                           int rh = int.Parse(obj.Attribute("height").Value);
                           string name = obj.Attribute("name").Value;
                           string type = "";
                           foreach (var prop in obj.Element("properties").Elements("property"))
                           {
                               switch (prop.Attribute("name").Value)
                               {
                                   case "Type":
                                       type = prop.Attribute("value").Value;
                                       break;
                               }
                           }
                           field.SetObject(name, type, ToMirrorX(x / pw, w) - (rw / pw - 1), z / ph, rw / pw, rh / ph);
                       }
                       break;
               }
           }
           if (data == null)
           {
               foreach (var prop in group.Element("properties").Elements("property"))
               {
                   int num = int.Parse(prop.Attribute("value").Value);
                   switch (prop.Attribute("name").Value)
                   {
                       case "RoomSizeMin":
                           dungeonInfo.roomSizeMin = num;
                           break;
                       case "RoomSizeMax":
                           dungeonInfo.roomSizeMax = num;
                           break;
                       case "OuterMargin":
                           dungeonInfo.outerMargin = num;
                           break;
                       case "PosMargin":
                           dungeonInfo.posMargin = num;
                           break;
                       case "ConnectRate":
                           dungeonInfo.connectRate = num;
                           break;
                       case "EnemyNumMin":
                           dungeonInfo.enemyNumMin = num;
                           break;
                       case "EnemyNumMax":
                           dungeonInfo.enemyNumMax = num;
                           break;
                       case "ItemNumMin":
                           dungeonInfo.itemNumMin = num;
                           break;
                       case "ItemNumMax":
                           dungeonInfo.itemNumMax = num;
                           break;
                   }
               }
               data = dungeon.Create(w, h, field, dungeonInfo);
           }
           return data;
       }
       catch (System.Exception i_exception)
       {
           Debug.LogErrorFormat("{0}", i_exception);
       }
       return null;
   }

   private class RandomDungeon
   {
       private DungeonInfo info;
       private Array2D data;
       private List<Area2D> areas;

       /**
       * ダンジョンを作成する
       */
       public Array2D Create(int w, int h, Field field, DungeonInfo i)
       {
           info = i;
           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(field);
           SetObjects(field);
           CreateRoads(field);
           return data;
       }

       /**
       * 区画を分割
       */
       private void Split(Area2D baseArea, bool isVertical)
       {
           Rect2D rect1, rect2;
           int minArea = (info.roomSizeMin + info.outerMargin) * 2 + 1;
           if (isVertical)
           {
               if (baseArea.outLine.height < minArea)
               {
                   areas.Add(baseArea);
                   return;
               }
               int a = baseArea.outLine.top + info.roomSizeMin + info.outerMargin;
               int b = baseArea.outLine.bottom - info.roomSizeMin - info.outerMargin;
               int ab = b - a;
               ab = Mathf.Min(ab, info.roomSizeMax);
               int p = a + Random.Range(0, ab + 1);
               rect1 = new Rect2D(baseArea.outLine.left, baseArea.outLine.top, baseArea.outLine.right, p);
               rect2 = new Rect2D(baseArea.outLine.left, p, 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;
               }
           }
           else
           {
               if (baseArea.outLine.width < minArea)
               {
                   areas.Add(baseArea);
                   return;
               }
               int a = baseArea.outLine.left + info.roomSizeMin + info.outerMargin;
               int b = baseArea.outLine.right - info.roomSizeMin - info.outerMargin;
               int ab = b - a;
               ab = Mathf.Min(ab, info.roomSizeMax);
               int p = a + Random.Range(0, ab + 1);
               rect1 = new Rect2D(baseArea.outLine.left, baseArea.outLine.top, p, baseArea.outLine.bottom);
               rect2 = new Rect2D(p, 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;
               }
           }
           Area2D area = new Area2D();
           area.outLine = rect2;
           areas.Add(area);
           baseArea.outLine = rect1;
           Split(baseArea, !isVertical);
       }

       /**
       * 部屋を作成する
       */
       private void CreateRooms(Field field)
       {
           foreach (var area in areas)
           {
               int aw = area.outLine.width - info.outerMargin;
               int ah = area.outLine.height - info.outerMargin;
               int width = Random.Range(info.roomSizeMin, aw);
               int height = Random.Range(info.roomSizeMin, ah);
               width = Mathf.Min(width, info.roomSizeMax);
               height = Mathf.Min(height, info.roomSizeMax);
               int rw = aw - width;
               int rh = ah - height;
               int rx = Random.Range(0, rw) + info.posMargin;
               int ry = Random.Range(0, rh) + info.posMargin;
               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);
               field.SetObject("Room", "Room", left, top, width + 1, height + 1);
           }
       }

       /**
       * マップ配列に部屋を作る
       */
       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 bool CreateHorizontalRoad(Area2D area1, Area2D area2, Field field, bool isC)
       {
           if (area1.outLine.right != area2.outLine.left) return false;
           int y1 = Random.Range(area1.room.top, area1.room.bottom);
           int y2 = Random.Range(area2.room.top, area2.room.bottom);
           int x = area2.outLine.left;
           if (isC)
           {
               if (area1.road != null) y1 = area1.road.bottom;
               if (area2.road != null) y2 = area2.road.bottom;
           }
           Rect2D rect1 = new Rect2D(area1.room.right, y1, x, y1);
           Rect2D rect2 = new Rect2D(x, y2, area2.room.left, y2);
           area1.road = rect1;
           area2.road = rect2;
           FillRoom(rect1);
           FillRoom(rect2);
           for (int y = Mathf.Min(y1, y2), end = Mathf.Max(y1, y2); y <= end; y++)
               data.Set(x, y, 0);
           field.SetObject("Connection", "Connection", area1.road.left, y1, 1, 1);
           field.SetObject("Connection", "Connection", area1.road.right, y1, 1, 1);
           field.SetObject("Connection", "Connection", area2.road.left, y2, 1, 1);
           field.SetObject("Connection", "Connection", area2.road.right, y2, 1, 1);
           return true;
       }

       /**
       * 垂直方向に道をのばす
       */
       private bool CreateVerticalRoad(Area2D area1, Area2D area2, Field field, bool isC)
       {
           if (area1.outLine.top != area2.outLine.bottom) return false;
           int x1 = Random.Range(area1.room.left, area1.room.right);
           int x2 = Random.Range(area2.room.left, area2.room.right);
           int y = area2.outLine.bottom;
           if (isC)
           {
               if (area1.road != null) x1 = area1.road.left;
               if (area2.road != null) x2 = area2.road.left;
           }
           Rect2D rect1 = new Rect2D(x1, y, x1, area1.room.top);
           Rect2D rect2 = new Rect2D(x2, area2.room.bottom, x2, y);
           area1.road = rect1;
           area2.road = rect2;
           FillRoom(rect1);
           FillRoom(rect2);
           for (int x = Mathf.Min(x1, x2), end = Mathf.Max(x1, x2); x <= end; x++)
               data.Set(x, y, 0);
           field.SetObject("Connection", "Connection", x1, area1.road.top, 1, 1);
           field.SetObject("Connection", "Connection", x1, area1.road.bottom, 1, 1);
           field.SetObject("Connection", "Connection", x2, area2.road.top, 1, 1);
           field.SetObject("Connection", "Connection", x2, area2.road.bottom, 1, 1);
           return true;
       }

       /**
       * 道を作成
       */
       private bool CreateRoad(Field field, Area2D area1, Area2D area2, bool isC = false)
       {
           if (area1.outLine.left < area2.outLine.left)
               if (CreateHorizontalRoad(area1, area2, field, isC)) return true;
           if (area2.outLine.left < area1.outLine.left)
               if (CreateHorizontalRoad(area2, area1, field, isC)) return true;
           if (area1.outLine.top > area2.outLine.top)
               if (CreateVerticalRoad(area1, area2, field, isC)) return true;
           if (area2.outLine.top > area1.outLine.top)
               if (CreateVerticalRoad(area2, area1, field, isC)) return true;
           return false;
       }

       /**
       * 全ての道を作成
       */
       private void CreateRoads(Field field)
       {
           for (int i = 0; i < areas.Count - 1; i++)
           {
               CreateRoad(field, areas[i], areas[i + 1]);
               if (Random.Range(0, 100) >= info.connectRate) continue;
               for (int j = i + 2; j < areas.Count; j++)
               {
                   if (CreateRoad(field, areas[i], areas[j], true)) break;
               }
           }
       }

       /**
       * オブジェクトをフィールドに設置する
       */
       private void SetObject(string name, string type, Field field, Array2D data)
       {
           while (true)
           {
               int areaIdx = Random.Range(0, areas.Count);
               Rect2D room = areas[areaIdx].room;
               int x = Random.Range(room.left, room.right + 1);
               int y = Random.Range(room.top, room.bottom + 1);
               if (data.Get(x, y) != 0) continue;
               data.Set(x, y, 1);
               field.SetObject(name, type, x, y, 1, 1);
               break;
           }
       }

       /**
       * オブジェクト群をフィールドに設置する
       */
       private void SetObjects(Field field)
       {
           Array2D tmpData = new Array2D(data.width, data.height);
           for (int x = 0; x < data.width; x++)
           {
               for (int y = 0; y < data.height; y++)
               {
                   tmpData.Set(x, y, data.Get(x, y));
               }
           }
           SetObject("UpStairs", "Stairs", field, tmpData);
           SetObject("DownStairs", "Stairs", field, tmpData);
           int num = Random.Range(info.enemyNumMin, info.enemyNumMax + 1);
           for (int i = 0; i < num; i++)
               SetObject("Random", "Enemy", field, tmpData);
           num = Random.Range(info.itemNumMin, info.itemNumMax + 1);
           for (int i = 0; i < num; i++)
               SetObject("Random", "Item", field, tmpData);
       }

       private class Area2D
       {
           public Rect2D outLine;
           public Rect2D room;
           public Rect2D road;
       }
   }
   
   [System.Serializable]
   public class DungeonInfo
   {
       public int roomSizeMax = 5;
       public int roomSizeMin = 3;
       public int outerMargin = 3;
       public int posMargin = 2;
       public int connectRate = 50;
       public int enemyNumMin = 0;
       public int enemyNumMax = 0;
       public int itemNumMin = 0;
       public int itemNumMax = 0;
   }
}

主な変更点は、ちゃんと隣り合った孫の区画に接続するようにしたことと、ダンジョン生成の細かい設定ができるようになった点です。
設定はUnityエディタ側からも入力できますし、マップファイルのカスタムプロパティで階数ごとに設定することもできます(両方とも入力されていた場合はマップファイル優先)。
いろいろ設定を変えてみて、試してみて下さい。
また、このコードでわからない箇所があれば何でも質問して下さい。

スクリーンショット 2020-10-19 11.15.18

なお、今回の生成の変更により、AutoMappingクラスのMappingメソッドも少し変更しました。

public void Mapping(ObjectPosition room, Field field)
{
   int sx = room.grid.x + room.range.left;
   int sy = room.grid.z + room.range.top;
   if (map.Get(sx, sy) > 0) return;
   int ex = sx + room.range.width;
   int ey = sy + room.range.height;
   for (int x = sx; x < ex; x++) {
       for (int y = sy; y < ey; y++)
       {
           map.Set(x, y, 1);
           for (int i = 0; i < stairsGrid.Length; i++)
           {
               if (x == stairsGrid[i].x && y == stairsGrid[i].z)
                   stairsImages[i].SetActive(true);
           }
       }
   }
   GameObject road = Instantiate(roadImage, roads.transform);
   road.GetComponent<RectTransform>().anchoredPosition = new Vector2(pw * (ToMirrorX(sx) - room.range.width + 1), ph * -sy);
   road.GetComponent<RectTransform>().localScale = new Vector3(room.range.width, room.range.height, 1);
   for (int x = sx; x < ex; x++)
   {
       Mapping(x, sy - 1, field.IsCollide(x, sy - 1) ? 0 : 1);
       Mapping(x, ey, field.IsCollide(x, ey) ? 0 : 1);
   }
   for (int y = sy; y < ey; y++)
   {
       Mapping(sx - 1, y, field.IsCollide(sx - 1, y) ? 0 : 1);
       Mapping(ex, y, field.IsCollide(ex, y) ? 0 : 1);
   }
}

マップファイルの作成方法:固定マップ編

ただコードを載せて終わりだと味気ないので、新しいマップファイルの作成方法についてまとめたいと思います。
使用するソフトウェアは「Tiled Map Editor」です。
まずは固定マップから作成していきましょう。
まずタイル・レイヤーを作成します。このレイヤーの名前を「Tile Layer」にしておきます。
同様にオブジェクト・レイヤーも作成し、名前を「Object Layer」にします。
次にグループ・レイヤーを一つ作成し、名前を「○(任意の階数)F」にします(例えば1Fや20Fなど)。そしてそのグループ・レイヤーの中に先ほど作成したタイル・レイヤーとオブジェクト・レイヤーを入れます。

スクリーンショット 2020-10-19 11.47.57

そしてタイル・レイヤーにマップを書いていきます。
例として、筆者は以下のようなマップにしました。

スクリーンショット 2020-10-19 11.54.44

ここにオブジェクトを置いていきます。
まず最初に階段の位置を決めましょう。
ただし、もし1Fや最後の階層を作成している場合は、もうそれ以上下りたり上ったりできないようにするために、下り階段や上り階段を壁の中に配置する必要があります(仕様上階段の位置は両方ともに設定して下さい)。
下り階段は「DownStairs」、上り階段は「UpStairs」です。
また、カスタムプロパティ「Type」を追加し、両方とも「Stairs」と入力して下さい。

スクリーンショット 2020-10-19 12.30.16

スクリーンショット 2020-10-19 12.04.41

さらに1Fの場合はプレイヤーの開始地点「StartPoint」を設定します。
この際、下り階段を設定するのを忘れて、後から下り階段を設定すると、プレイヤーの開始地点が下り階段の位置に上書きされてしまいます。これを防ぐために、StartPointの方がDownStairsよりも後に読み込まれる必要があります(以下のような並び順であれば大丈夫です)。

スクリーンショット 2020-10-19 12.17.29

また、階段と同じようにカスタムプロパティ「Type」を追加し「StartPoint」と設定します。
筆者はここにしました。

スクリーンショット 2020-10-19 12.19.03

次に接続口「Connection(Type:Connection)」、部屋「Room(Type:Room)」を設定します。
ただし、敵を配置しない場合は接続口を設定する必要はありません。
部屋は敵を配置しない場合でもミニマップの関係上設定する必要がありますが、逆に言えばミニマップを実装しない場合は部屋も設定する必要はありません。
部屋はオブジェクトを範囲指定することで設定します。

スクリーンショット 2020-10-19 12.39.02

また、接続口の配置方法は少々ややこしいので、下記の配置例を見ながら設定するようにして下さい。

スクリーンショット 2020-10-19 12.41.31

基本は部屋の通路に接している箇所の部屋側の位置と、曲がり角に設定することが大事です。
最後に敵(Type:Enemy)とアイテム(Type:Item)を設定します。
敵オブジェクトの名前には「Random」か「キャラクターID名」を設定します。
アイテムオブジェクトの名前には「Random」か「アイテムID名」を設定します。
Randomにすると敵やアイテムがExcelAppearDataファイルに沿ってランダムに出現します。

スクリーンショット 2020-10-19 13.06.21

なお、1Fと最後の階層は仕様上固定マップにする必要があります。

マップファイルの作成方法:ランダム生成マップ編

固定マップは大変でしたが、ランダム生成マップはとても楽です。
グループ・レイヤーを作成し、名前を「○(任意の階数)F」にするだけでOKです。
ただし、タイル・レイヤーやオブジェクト・レイヤーは作成しないようにして下さい。
Dungeon Infoに値を設定することも可能です。

スクリーンショット 2020-10-19 13.24.32

それでは、完成したマップを表示してみましょう。

スクリーンショット 2020-10-19 13.28.59

スクリーンショット 2020-10-19 13.34.12

ちゃんと設定した通りになりましたか?

という訳で今回はここまでにします。
次回は状態異常の種類の増やし方について書こうかなと思います。

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