Procedural Level Generator是在Unity應用商店中發佈的一款免費的輕量級關卡生成器:html
能夠直接搜索關鍵字在應用商店中查找並下載。算法
和我以前生成關卡的想法不一樣,這個插件生成地圖的方式相似於拼積木,它將每個地圖分爲一個一個的部分,不管是房間仍是通道,都叫作Section,只是用不一樣的標籤來規定和約束這些部分,並逐一的將這些部分在空間中鏈接起來,每個部分須要本身手動定義它的預製體,形狀,碰撞盒子以及出口列表,經過出口列表來判斷下一個部分的鏈接位置和方向,用碰撞盒子的Bounds.Intersects(Bounds bounds);方法來判斷一個部分的生成是否會是一個無效的鏈接:數據結構
1 public bool IsSectionValid(Bounds newSection, Bounds sectionToIgnore) => 2 !RegisteredColliders.Except(sectionToIgnore.Colliders).Any(c => c.bounds.Intersects(newSection.Colliders.First().bounds)); 3 4 // 5 // 摘要: 6 // Does another bounding box intersect with this bounding box? 7 // 8 // 參數: 9 // bounds: 10 public bool Intersects(Bounds bounds);
利用提早製做Section預製體的方式來鏈接生成整個關卡的方式,確實避免了不少讓人頭疼的算法設計,但可能插件自己也只是爲了提供一個基本思路,所以有些地方值得優化。ide
1.缺乏門的概念優化
不少時候,進入一個地圖的房間,咱們須要門的解鎖和開關來對探索進行限制,也有可能進入一個盡是怪物的房間,這個房間的全部門會自動關閉,給玩家一種身陷敵營是時候浴血奮戰的錯覺。故而考慮在Section中給每一個類增長一個自帶Door的列表,該列表能夠沒有任何元素,例如不少通道之間是不須要門來進行鏈接的,但房間與通道之間,房間與房間之間,能夠同時建立門來執行必要的約束限制。ui
定義門的類,注意保持在插件的命名空間之下:this
1 using UnityEngine; 2 using System.Collections.Generic; 3 4 namespace LevelGenerator.Scripts 5 { 6 public class Door : MonoBehaviour 7 { 8 public List<string> Tag1s = new List<string>(); 9 public List<string> Tag2s = new List<string>(); 10 11 public Transform ExitTransdorm { get; set; } 12 public void Initialize(LevelGenerator levelGenerator) 13 { 14 transform.SetParent(levelGenerator.Container); 15 } 16 } 17 }
這裏只定義了最基礎的一些屬性和方法,主要是門鏈接的兩個Section的標籤列表,用於更爲準確的斷定該門的所屬。spa
在Section類中添加放置門的方法:插件
1 /// <summary> 2 /// initialize door datas 3 /// </summary> 4 /// <param name="exit">place transform</param> 5 /// <param name="next">next section</param> 6 public void PlaceDoor(Transform exit, Section next) 7 { 8 var t = Instantiate(LevelGenerator.Doors.PickOne(), exit); 9 t.Initialize(LevelGenerator); 10 Doors.Add(t.gameObject); 11 12 var d = t.GetComponent<Door>(); 13 d.Tag1s.AddRange(Tags); 14 d.Tag2s.AddRange(next.Tags); 15 d.ExitTransdorm = exit; 16 17 //send door initialize event 18 if (Idx > 0 || next.Idx > 0) 19 EventManager.QueueEvent(new DoorInitEvent(t.transform, Idx, next.Idx)); 20 }
而且在每個門建立後及時記錄在Section的Doors列表中,發送建立完成的事件,這裏使用的事件系統能夠詳見:設計
http://www.javashuo.com/article/p-kxxarhzu-gq.html
調用就是在成功生成每個Section以後:
1 protected void GenerateSection(Transform exit) 2 { 3 var candidate = IsAdvancedExit(exit) 4 ? BuildSectionFromExit(exit.GetComponent<AdvancedExit>()) 5 : BuildSectionFromExit(exit); 6 7 if (LevelGenerator.IsSectionValid(candidate.Bounds, Bounds)) 8 { 9 candidate.LastSections.Add(this); 10 NextSections.Add(candidate); 11 12 if (LevelGenerator.SpaceTags.Contains(candidate.Tags.First()) && LevelGenerator.CheckSpaceTags(candidate)) 13 { 14 Destroy(candidate.gameObject);
NextSection.Remove(candidate); 15 GenerateSection(exit); 16 return; 17 } 18 19 candidate.Initialize(LevelGenerator, order); 20 candidate.LastExits.Add(exit); 21 22 PlaceDoor(exit, candidate); 23 } 24 else 25 { 26 Destroy(candidate.gameObject); 27 PlaceDeadEnd(exit); 28 } 29 }
因爲通道與通道之間不須要放門,所以在全部Section生成完畢以後將一部分門刪除:(此方法位於關卡生成器這個控制類中)
1 /// <summary> 2 /// Clear the corridor doors 3 /// </summary> 4 protected void CheckDeleteDoors() 5 { 6 foreach (var s in registeredSections) 7 { 8 if (s != null) 9 { 10 var temp = new List<GameObject>(); 11 foreach (var d in s.Doors) 12 { 13 var ds = d.GetComponent<Door>(); 14 if (ds.Tag1s.Contains("corridor") && ds.Tag2s.Contains("corridor")) 15 { 16 temp.Add(d); 17 Destroy(d); 18 } 19 } 20 21 foreach(var t in temp) 22 { 23 s.Doors.Remove(t); 24 } 25 } 26 } 27 }
這裏注意一點,遍歷列表的時候不能直接對列表的元素進行移除,因此先創建了一個臨時須要移除的列表做爲替代,遍歷臨時列表以移除元素,固然了,你用通用方式for循環倒着遍歷也是可行的,我的不太喜歡用for循環而已。
說句題外話,可能有人會有疑惑,爲何不直接在建立門的時候作條件限制,非要等到最後統一再來遍歷刪除呢,其實最主要的緣由是爲了儘可能少的變更原始的代碼邏輯和結構,而更傾向於添加新的方法來對插件進行附加功能的完善,這樣能夠很大的程度上減小bug觸發的機率,畢竟別人寫的插件你極可能總有漏想的地方,隨意的改動和刪除對方已經寫過的內容並不是良策,最好是隻添加代碼而不對原始代碼進行任何的改動或刪除,僅以這樣的方式來達到完善功能的目的。調試的時候也只用關注本身添加的部分便可。
2.路徑的末尾極可能是通道
關於這一點,可能會根據遊戲的不一樣而異,由於這個插件在生成地圖的過程當中,不管是房間仍是通道,都是同一個類Section,這樣沒辦法保證路徑末尾是一個房間,仍是通道。能夠添加一個功能用於檢查和刪除端點是通道的部分。
在Section中添加如下屬性方便遍歷刪除:
1 [HideInInspector] 2 public List<GameObject> DeadEnds = new List<GameObject>(); 3 [HideInInspector] 4 public List<Transform> LastExits = new List<Transform>(); 5 [HideInInspector] 6 public List<Section> LastSections = new List<Section>(); 7 [HideInInspector] 8 public List<Section> NextSections = new List<Section>(); 9 [HideInInspector] 10 public List<GameObject> Doors = new List<GameObject>();
分別表明每個Section的死亡端點列表,上一個Section的列表,下一個Section的列表(相似於雙向鏈表),與上一個Section鏈接的位置列表,門的列表,有了這些數據結構,不管怎麼遍歷,修改和獲取數據都是會變得很是容易。添加的地方天然是生成Section的方法中,放置端點的方法中,及放置門的方法中。
開始檢查並刪除末尾的通道:(根據實際需求是否調用)
1 /// <summary> 2 /// clear end sections and update datas 3 /// </summary> 4 protected void DeleteEndSections() 5 { 6 var temp = new List<Section>(); 7 foreach (var s in registeredSections) 8 { 9 temp.Add(s); 10 DeleteEndSection(s); 11 } 12 13 foreach(var t in temp) 14 { 15 foreach (var c in t.Bounds.Colliders) 16 { 17 DeadEndColliders.Remove(c); 18 } 19 registeredSections.Remove(t); 20 } 21 } 22 23 /// <summary> 24 /// clear the end corridors and doors , place deadend prafabs' instances 25 /// </summary> 26 /// <param name="s">the check section</param> 27 protected void DeleteEndSection(Section s) 28 { 29 if (s.Tags.Contains("corridor")) 30 { 31 if (s.DeadEnds.Count == s.ExitsCount) 32 { 33 //刪除通道以及通道的端點方塊 34 Destroy(s.gameObject); 35 foreach (var e in s.DeadEnds) 36 { 37 Destroy(e); 38 } 39 40 foreach (var ls in s.LastSections) 41 { 42 //刪除末端通道後須要在上一個節點的退出點放置端點方塊(否則牆壁上就會有洞) 43 foreach (var le in s.LastExits) 44 { 45 ls.PlaceDeadEnd(le); 46 } 47 48 //一樣的,懸空的門應該刪除 49 var temp = new List<GameObject>(); 50 foreach (var d in ls.Doors) 51 { 52 var ds = d.GetComponent<Door>(); 53 if (s.LastExits.Contains(ds.ExitTransdorm)) 54 { 55 temp.Add(d); 56 Destroy(d); 57 } 58 } 59 60 foreach (var t in temp) 61 { 62 ls.Doors.Remove(t); 63 } 64 65 //遞歸遍歷,由於端點的通道可能很長,要直到遍歷到非通道爲止 66 DeleteEndSection(ls); 67 } 68 } 69 } 70 }
3.沒有間隔隨機的規則系統
在實際生成隨機地圖的過程當中,很容易發現一個嚴重的問題,在隨機的過程當中,同類型的房間接連出現,例如,玩家剛剛進入了一個商店類型的房間,後面又立刻可能再進入一個商店類型的房間,這樣顯然很很差,而爲了不這種狀況發生,就要考慮給隨機系統添加額外的隨機規則。
在生成器的控制類中添加須要間隔隨機的標籤列表:
1 /// <summary> 2 /// The tags that need space 3 /// </summary> 4 public string[] SpaceTags;
在生成具體Section的過程當中要對下一個生成的Section進行標籤檢查:
1 candidate.LastSections.Add(this); 2 NextSections.Add(candidate); 3 4 //對間隔標籤進行檢查 5 if (LevelGenerator.SpaceTags.Contains(candidate.Tags.First()) && LevelGenerator.CheckSpaceTags(candidate)) 6 { 7 Destroy(candidate.gameObject); 8 NextSections.Remove(candidate); 9 GenerateSection(exit); 10 return; 11 } 12 13 candidate.Initialize(LevelGenerator, order); 14 candidate.LastExits.Add(exit); 15 16 PlaceDoor(exit, candidate);
只有經過檢查才能繼續初始化和生成其餘數據,否則就從新隨機。具體的檢查算法以下:
1 private bool bSpace; 2 3 /// <summary> 4 /// check the space tags 5 /// </summary> 6 /// <param name="section">next creat scetion</param> 7 /// <returns>is successive tag</returns> 8 public bool CheckSpaceTags(Section section) 9 { 10 foreach (var ls in section.LastSections) 11 { 12 if (ls.Tags.Contains("corridor")) 13 { 14 //包含通道時別忘了遍歷該通道的其餘分支 15 if (OtherNextCheck(ls, section)) 16 return bSpace = true; 17 18 bSpace = false; 19 CheckSpaceTags(ls); 20 } 21 else 22 { 23 if (SpaceTags.Contains(ls.Tags.First())) 24 { 25 return bSpace = true; 26 } 27 else 28 { 29 //即便上一個房間未包含間隔標籤,但該房間的其餘分支也須要考慮 30 if (OtherNextCheck(ls, section)) 31 return bSpace = true; 32 } 33 } 34 } 35 36 return bSpace; 37 } 38 39 bool result; 40 bool OtherNextCheck(Section section,Section check) 41 { 42 foreach(var ns in section.NextSections) 43 { 44 //若是是以前的Section分支則跳過這次遍歷 45 if (ns == check) 46 continue; 47 48 if (ns.Tags.Contains("corridor")) 49 { 50 result = false; 51 OtherNextCheck(ns, check); 52 } 53 else 54 { 55 if (SpaceTags.Contains(ns.Tags.First())) 56 { 57 return result = true; 58 } 59 } 60 } 61 62 return result; 63 }
總共有三種狀況不符合要求:
1.包含間隔標籤房間的上一個房間也包含間隔標籤。(最直接的一種狀況,直接Pass)
2.雖然包含間隔標籤的房間的上一個房間不包含間隔標籤,但鏈接它們通道的某一其餘分支中的第一個房間包含間隔標籤。
3.雖然包含間隔標籤的房間的上一個房間不包含間隔標籤,且鏈接它們通道的任何一個其餘分支中的第一個房間也不包含間隔標籤,但上一個房間的其餘分支中的第一個房間包含間隔標籤。
上面三種狀況都會形成一次戰鬥結束後可能同時又多個商店房間的狀況。
隨機生成關卡的效果展現:(圖中選中的部分爲門,間隔標籤房間便是其中有內容物的小房間)
我將改動以後的插件從新進行了打包,以供下載參考:(其中有些發事件的代碼比較懶沒有刪除,遇到問題直接對應行便可)
https://files.cnblogs.com/files/koshio0219/LevelGenerator.zip
更多有關隨機地圖關卡的隨筆可見: