上一個地圖生成算法,這一次是一個地牢的生成算法,是一個國外的人寫的算法,用dart語言寫,我把它改成了unity-c#。
原作者博客地址:Rooms and Mazes: A Procedural Dungeon Generator
當然,我看英文很吃力,好不容易找了一篇翻譯后的文章,分享給英語不太好的人。
一個翻譯后的版本:房間和迷宮:一個地牢生成算法
然后原作者的算法代碼地址(dart):github
算法的原理請看原文地址或者翻譯地址,那里有各種動態演示圖,講解的也很清楚,代碼可以看原作者的代碼,因為我沒有學過dart,改寫c#的過程很有可能有錯誤,請見諒!
原作者的代碼用到了第三方的一個庫,github地址,可以參考這個看原作者代碼。
c#代碼:
using System.Collections; using System.Collections.Generic; using UnityEngine; using System.Linq; using System.Threading; class Directions{ public static Vector2 none = new Vector2 (0,0); public static Vector2 up = new Vector2 (0,1); public static Vector2 down = new Vector2 (0,-1); public static Vector2 left = new Vector2 (-1,0); public static Vector2 right = new Vector2 (1,0); public static Vector2[] all = {up,down,left,right}; } //確保地圖的長寬是奇數 public class generateDungeon2 : MonoBehaviour { //嘗試生成房間的數量 public int numRoomTries = 50; //在已經連接的房間和走廊中再次連接的機會,使得地牢不完美 public int extraConnectorChance = 20; //控制生成房間的大小 public int roomExtraSize = 0; //控制迷宮的曲折程度 public int windingPercent = 0; public int width = 51; public int height = 51; public GameObject wall, floor,connect; private Transform mapParent; //生成的有效房間 private List<Rect> rooms; //正被雕刻的區域的索引。(每個房間一個索引,每個不連通的迷宮一個索引,在連通之前) private int currentRegion = 0; //原文https://github.com/munificent/piecemeal Array2D //改成int[,] private int[,] _regions; private Tiles[,] map; void Start () { rooms = new List<Rect> (); map = new Tiles[width,height]; _regions = new int[width,height]; mapParent = GameObject.FindGameObjectWithTag ("mapParent").transform; Generate (); } void Update () { if (Input.GetKeyDown (KeyCode.Q)) { Generate (); } } public void Generate(){ if (width % 2 == 0 || height % 2 == 0) { Debug.Log ("地圖長寬不能為偶數"); return; } InitMap (); AddRooms (); FillMaze (); ConnectRegions (); RemoveDeadEnds (); InstanceMap (); } /* *生成房間 *1.隨機房間(隨機大小,奇數) *2.查看是否重疊,否則加入房間數組 */ private void AddRooms(){ for (int i = 0; i < numRoomTries; i++) { //確保房間長寬為奇數 int size = Random.Range(1,3+roomExtraSize)*2+1; int rectangularity = Random.Range (0, 1 + size / 2) * 2; int w = size, h = size; if (0 == Random.Range (0, 1)) { w += rectangularity; } else { h += rectangularity; } int x = Random.Range (0, (width - w) / 2) * 2 + 1; int y = Random.Range (0, (height - h) / 2) * 2 + 1; Rect room = new Rect (x,y,w,h); //判斷房間是否和已存在的重疊 bool overlaps = false; foreach (Rect r in rooms) { if(room.Overlaps(r)){ overlaps = true; break; } } //如果重疊,拋棄該房間 if (overlaps) continue; //如果不重疊,把房間放入rooms中 rooms.Add(room); //設置新房間索引 StartRegion(); for (int j = x; j < x + w; j++) { for (int k = y; k < y + h; k++) { Carve (new Vector2 (k, j)); } } } } /* * 填充迷宮(洪水填充) * */ private void FillMaze(){ //0處為牆 for (int x = 1; x < width; x += 2) { for (int y = 1; y < height; y += 2) { Vector2 pos = new Vector2 (x,y); //if (map [pos] == Tiles.Wall) { if (map [x,y] == Tiles.Wall) { GrowMaze (pos); } } } } /* * 生成迷宮 */ private void GrowMaze(Vector2 start){ List<Vector2> cells = new List<Vector2> (); Vector2 lastDir = Directions.none; StartRegion (); //cells添加之前需要變成Floor Carve (start); cells.Add (start); while (cells != null && cells.Count != 0) { Vector2 cell = cells [cells.Count - 1]; //可以擴展的方向的集合 List<Vector2> unmadeCells = new List<Vector2> (); //加入能擴展迷宮的方向 foreach (Vector2 dir in Directions.all) { if (CanCarve (cell, dir)) { unmadeCells.Add (dir); } } if (unmadeCells != null && unmadeCells.Count != 0) { Vector2 dir; //得到擴展方向 windingPercent用來控制是否為原方向 if (unmadeCells.Contains (lastDir) && Random.Range (0, 100) > windingPercent) { dir = lastDir; } else { dir = unmadeCells [Random.Range (0, unmadeCells.Count - 1)]; } Carve (cell + dir); Carve (cell + dir * 2); //添加第二個單元 cells.Add (cell + dir * 2); lastDir = dir; } else { //沒有相鄰可以雕刻的單元,就刪除 cells.Remove (cells[cells.Count - 1]); //置空路徑 lastDir = Directions.none; } } } /* * 連通房間和迷宮 */ private void ConnectRegions(){ //找到區域所有可連接的空間牆wall Dictionary<Vector2,List<int>> connectorRegions = new Dictionary<Vector2, List<int>> (); for (int i = 1; i < width - 1; i++) { for (int j = 1; j < height - 1; j++) { //不是牆的跳過 if (map [i, j] != Tiles.Wall) continue; List<int> regions = new List<int> (); foreach (Vector2 dir in Directions.all) { int region = _regions [i + (int)dir.x, j + (int)dir.y]; //如果周圍不是牆(牆的索引為regions的初始值為0) //去重 if (region != 0 && !regions.Contains(region)) regions.Add (region); } //如果這個牆沒有連接一個以上的區域,那就不是一個連接點 if (regions.Count < 2) continue; connectorRegions [new Vector2 (i, j)] = regions; //標志連接點 //SetConnectCube(i,j); } } //所有連接點 List<Vector2> connectors = connectorRegions.Keys.ToList<Vector2>(); //跟蹤哪些區域已合並。將區域索引映射為它已合並的區域索引。 List<int> merged = new List<int>(); List<int> openRegions = new List<int> (); for (int i = 0; i <= currentRegion; i++) { merged.Add (i); openRegions.Add (i); } //使區域連接最終只剩下一個 while (openRegions.Count > 1) { //隨機選擇一個連接點 Vector2 connector = connectors[Random.Range(0,connectors.Count-1)]; //連接 AddJunction(connector); //合並連接區域我們將選擇第一個區域(任意)和 //將所有其他區域映射到其索引。 //connectorRegions[connector] List<int> regions = connectorRegions[connector]; for (int i = 0; i < regions.Count; i++) { regions[i] = merged[regions[i]]; } int dest = regions[0]; regions.RemoveAt (0); List<int> sources = regions; //合並所有受影響的區域 for(int i=0;i<currentRegion;i++){ if (sources.Contains (merged [i])) { merged [i] = dest; } } //移除已經連接的區域 foreach (int s in sources) { openRegions.RemoveAll (value => (value==s)); } connectors.RemoveAll (index=>IsRemove(merged,connectorRegions,connector,index)); } } /* * 簡化迷宮 */ private void RemoveDeadEnds(){ bool done = false; while (!done) { done = true; for (int i = 1; i < width - 1; i++) { for (int j = 1; j < height - 1; j++) { if (map [i, j] == Tiles.Wall) continue; int exists = 0; foreach (Vector2 dir in Directions.all) { if (map [i + (int)dir.x, j + (int)dir.y] != Tiles.Wall) { exists++; } } //如果exists==1則是三面環牆 if (exists != 1) { continue; } done = false; _regions [i, j] = 0;//變成牆 map [i, j] = Tiles.Wall; } } } } /* *保存區域索引 * */ private void StartRegion() { currentRegion++; } /* * 雕塑點,設置這個點的類型,默認地板 * */ private void Carve(Vector2 pos,Tiles type=Tiles.Floor) { int x = (int)pos.x, y = (int)pos.y; map [x, y] = Tiles.Floor; _regions [x,y] = currentRegion; } //dir是方向 private bool CanCarve(Vector2 pos,Vector2 dir){ Vector2 temp = pos + 3*dir; int x = (int)temp.x, y = (int)temp.y; //判斷是否超過邊界 if (x < 0 || x > width || y < 0 || y > height) { return false; } //需要判斷方向第二個單元的原因是cells中需要添加下一個cell //所以下一個cell要變為Floor,然后需要判斷是否第二個單元是否為牆 //如果不為牆,則第一個cell被變為Floor為,和第二個單元就連通了,不可行 //判斷第二個單元主要用來判斷不能&其他房間或走廊(regions)連通 temp = pos + 2 * dir; x = (int)temp.x; y = (int)temp.y; //是牆則能雕刻迷宮 return map [x, y] == Tiles.Wall; } private void AddJunction(Vector2 pos){ map [(int)pos.x, (int)pos.y] = Tiles.Floor; } /* * 刪除不需要的連接點 */ private bool IsRemove(List<int> merged,Dictionary<Vector2,List<int>> ConnectRegions,Vector2 connector,Vector2 pos){ //不讓連接器相連(包括斜向相連) if((connector-pos).SqrMagnitude() < 2){ return true; } List<int> temp = ConnectRegions[pos]; for(int i=0;i<temp.Count;i++){ temp[i] = merged[temp[i]]; } HashSet<int> set = new HashSet<int>(temp); //判斷連接點是否和兩個區域相鄰,不然移除 if(set.Count>1){ return false; } //增加連接,使得地圖連接不是單連通的 if(Random.Range(0,extraConnectorChance)==0) AddJunction(pos); return true; } private void SetConnectCube(int i,int j){ GameObject go = Instantiate (connect, new Vector3 (i, j, 1), Quaternion.identity) as GameObject; go.transform.SetParent (mapParent); go.layer = LayerMask.NameToLayer ("wall"); } /* * 地圖全部初始化為牆 * */ private void InitMap(){ for (int x = 0; x < width; x ++) { for (int y = 0; y < height; y ++) { map [x, y] = Tiles.Wall; } } } private void InstanceMap (){ for (int i = 0; i < width; i++) { for (int j = 0; j < height; j++) { if (map [i, j] == Tiles.Floor) { GameObject go = Instantiate (floor, new Vector3 (i, j, 1), Quaternion.identity) as GameObject; go.transform.SetParent (mapParent); //設置層級 go.layer = LayerMask.NameToLayer ("floor"); } else if (map [i, j] == Tiles.Wall) { GameObject go = Instantiate (wall, new Vector3 (i, j, 1), Quaternion.identity) as GameObject; go.transform.SetParent (mapParent); go.layer = LayerMask.NameToLayer ("wall"); } } } } }
效果圖:
這個是一開始的生成房間的效果圖:
下圖是對地圖空白部分進行迷宮填充:
下圖是對迷宮進行連接點計算:
下圖是對地圖的區域進行連接:
最后是對地圖中的死胡同進行消除: