本文發布於游戲程序員劉宇的個人博客,歡迎轉載,請注明來源https://www.cnblogs.com/xiaohutu/p/10504586.html
某個神秘的時間,我接到了一項神秘的任務,最核心的難度是要求實現:引擎是Unity3D,在手機端可以流暢運行為前提,在一個實時戰斗的過程里,地圖有地形(而且是會被動態改變的地形),數百個單位獨立AI尋路、要實現忽略掉部分單位的篩選尋路、動態避障、滿足幀同步需求並可以被服務器驗證。可以說這個需求是集合了各種難點於一身。在這個任務的過程里,發現網上這樣的文章比較少,所以想總結分享一下。着重於算法和思路這一塊,不涉及圖形上的問題。
一. 通常怎么做
需要尋路,又需要避障,先說一些常規的解決思路:
1.1 尋路
尋路就是基於既有的數據尋找到符合條件的一條路線:
1. 拿來主義類:用unity3d自己的NavMesh、自己的A* project,包含了尋路數據的生成和計算。
2. 進階類:格子尋路可以用:
自己寫A* 算法,進行常見的優化(二叉堆優化、HOT優化等等等等), 分層A*
JPS以及各種優化(位運算,剪枝,預處理等)
Dijkstra(擴展Dynamic A*)
DFS, BFS
。。。
(后續開文詳解)
1.2 避障部分
合理的通過改變自己的行為(速度,方向)來避免穿插:
0. 真實物理
1. 在引擎里使用射線判定是否碰撞,並等待/重新尋路(耗時)
2. 根據距離判斷是否碰撞,並等待/重新尋路,最簡單的是直接用距離計算(耗時)
3. 在2的基礎上使用算法優化距離判斷,減少計算量,一般來說可以:
3.1 九宮格,分區查找計算目標
3.2 根據需求四叉樹/八叉樹來對目標列表進行分區分塊,提高查詢的速度
3.3 十字鏈表來存儲格子對應的單位,提高查詢速度
3.4 雙向鏈表的視野管理思路
4. 其他思路:
4.1 Steer類、使用作用力思路計算
4.2 VO、RVO、ORCA等避障算法、通過速度與距離整體計算
4.3 其他群體行為算法
4.4 路徑沖突、管理類算法
這一部分的很多思路和做法和技能AI里的目標篩選類似,可以公用數據結構來快速獲得計算結果。
二. 項目的特點分析
在我的例子里:
1.單位要可以實時尋找看起來合理的路線行走,路線要避開行人和地形障礙
2.動態避障,不能穿人、地形
3.要實現有篩選的尋路,尋路時要忽略掉部分單位
4.有地形玩法,且會被技能動態改變
5.單位數量巨大
6.滿足幀同步需求
7.可以被服務器驗證
三. 具體思路
冷靜思考,發現幾百個這個數量級有點大,必須從底層到上層都很優化才可以,不然光是一些算法乘以單位數量運行的時間都是天文數字。
而且封裝好的東西就沒辦法考慮了:沒有辦法在服務器同步運行校驗、浮點數沒有辦法滿足幀同步的要求、動態改變障礙數據的支持不夠。
所以必須得自定義的去實現這些東西,而且首先的有一定的原則:
1. 必須有簡單、可靠、快速、通用的數據結構來存儲尋路數據和障礙數據
2. 簡單可靠的服務器客戶端都可以運行的算法代碼
3. 必須用有損服務/分層服務的思想來剪裁算法的時間
接下來我們一步步的來分析:
3.1 數據結構部分
避免使用大量的容器,避免容器操作的消耗,盡量使用數組、變量、常量。提前緩存各種開方、定點數三角函數、距離計算等的查表值。
3.1.1 地圖數據
我們這個項目大量的戰斗地圖可以抽象成數組,但是地表類型不一樣,高度也不一樣,首先寫了一個unity3d的插件,一鍵導出戰斗地圖的數據,包含N個2的冪為寬高的數組,包含了一些基本信息:
地形的障礙數據、地形數據(用來計算尋路和保存地形玩法),高度數據(避免其他更耗時的方式計算單位的y高度)。
3.1.2 角色數據
我們這個項目並不是一個角色使用一個格子,而是多個格子(邏輯比較復雜),所以緩存了很多數組,包含角色的半徑圖(每一個格子里都存儲了最大可以允許占位的角色半徑,這樣可以快速判斷一個格子是否可以站人)、格子與多個單位的映射關系等。
如下如,無窮代表誰都可以站,其他人在靠近時根據黃色的半徑值來判自己的半徑是否低於數值。如果大家的游戲沒那么復雜,那么更好了,根據游戲情況來,甚至最簡單一個格子存儲一個人都可以。
3.1.2 運行時數據
存儲了兩層數據
第一層,N份內容不同的,可以直接在尋路中被使用的障礙數據數組。(每個游戲情況不一樣)
第二層,根據邏輯存儲的,可以二次生成第一層數據的邏輯數據。
* 在單位行走時,會實時更新各種地圖的緩存數據
* 在尋路需要檢索時,大部分情況直接使用特定的數組緩存。
* 當特殊尋路需求時(如篩選),使用數組基於第二層數據進行修改的特殊數據。
在這個轉換的過程中,通過對每一個玩家的半徑圖的匯總,形成新的全地圖半徑圖。
既然是大規模單位的算法,那么這個計算難免最終必須優化成為位操作,於是這里涉及到一個通過位操作快速計算最低位的方法。
1. 位運算 v & -v,直接保留最低位,足夠進行通過性判斷。
2. DeBruijn序列的移位寄存器算法,理論上一個2的冪都可以乘以神奇數字來獲得在DeBruijn序列里的值,可以直接獲得可以站的半徑。
unsigned int v;
int r;
static const int MultiplyDeBruijnBitPosition[32] =
{
0, 1, 28, 2, 29, 14, 24, 3, 30, 22, 20, 15, 25, 17, 4, 8,
31, 27, 13, 23, 21, 19, 16, 7, 26, 12, 18, 6, 11, 5, 10, 9
};
r = MultiplyDeBruijnBitPosition[((uint32_t)((v & -v) * 0x077CB531U)) >> 27];
理論上2的冪數位都可以有神奇數字,詳情可以看這里https://en.wikipedia.org/wiki/De_Bruijn_sequence
3.1.3 數據結構其他
其他還林林總總存儲了各種緩存數據,這一塊建議大家結合自己的項目仔細思考,目的是提高尋路系統和避障系統的各種效率:
1. 自定義尋路格子數據讀取的效率
2. 避障系統檢索碰撞單位的效率
3. 避障系統和尋路系統檢索可站點的效率
這些東西大家可能細節都不太一樣,總體就是這些。
3.2 尋路算法的選擇
尋路數據基於半徑圖、地圖數據、技能地形數據等形成了可以快速讀取應用的障礙數據。算法方面,根據情況最后采用了自定義的格子算法。優點是純邏輯,服務器也可以運行來校驗。在這個過程里,算法的耗時是結結實實的跑不掉的,所以我們是不可能采用單一耗時的算法去計算所有情況了。我使用了多種算法的組合來應對性能問題,並且根據情況逐步使用更耗時的算法:
1. 根據障礙數據快速檢索直線阻擋,采用直線尋路。(這就是為什么障礙數據必須設計的很好,可以快速的計算直線是否有阻擋)
2. 在很短距離范圍內使用一些非最優解路線的算法, 類似貪心算法等,因為距離短,所以耗時會很少,這種使用次數最多
3. 實現最優版的類A*尋路算法(JPS等),在必須使用時使用用它,首先是大多數情況下根據邏輯都使用部分路徑搜索,先逼近,這樣路徑很短,耗時低。
4. 部分情況下使用完全的路徑搜索。
1234步步遞進,這一部分大家肯定是根據自己項目的需求來選擇使用,鍵是一定要有不同耗時的方案來選擇。
一些優化:
1. 算法本身的優化,必須是非常極限,到每一個細節。這點C/C++語言的優勢凸顯,這個項目的引擎是unity3d,也可以寫然后用dll調用
2. 對使用頻度進行優化,尋路失敗的單位進行等待,耗時尋路的操作每一幀次數上限進行控制
3. 游戲邏輯層面對AI的耗時進行錯峰
4. 幀同步層面對邏輯幀層面的頻率進行控制
3.3 避障算法1
大家要根據項目的需求來進行,在我們這個例子里不可避免的是要自己實現算法,最終我采取了下面的方式,主要是可以結合前面的數據結構來使用:
1.使用與尋路公用的部分數據,通過直接使用大圖應用實現了快速嚴格的障礙數據判定,即不能走的地方絕對不能走
2.部分自定義的VO(某些情況下使用)來控制是否停止,Steer里的SlowRadius來控制擠碰和松散程度,要碰撞的單位停下來,在AI層選擇行為是等待一會還是重新尋找目標。因為我們還是要很多單位執行AI,所以這一部分實現的時候非常簡單,實現一小部分最需要的邏輯。同時還是注意這類算法的采樣數量和計算的次數。
最后整體的效果還可以,基本上(走-->等待(避讓)-->繼續走-->停) 這樣的一個流程基本滿足需求,可以實現類似物理擠碰的效果。
3.4 避障算法2
還有一個也很好但是我沒有使用的避障方法,大家可以學習探討。這個方法的普通版適用於不使用半徑圖或者不需要到物理級表現擠碰邏輯、但是需要避免穿插的情況。請看下圖:
假設一個人從A走到B點(為了表達方便,簡化成直線),那么從A到B的路線可以根據A的速度計算到達的時間來標記出優先級等級,也可以稱之為灰度等級。
那么:
1. 在設定在時間片內(即誤差允許的時間片內)可以到達的路徑上,標記好需要占用的路徑的優先級等級。
2. 不同路徑交叉時,根據時間的這個灰度等級來計算當前是否可以穿過,還是應該等待。
3. 移動時,通過設計高效的數據結構來維護更新整個數據結構
這個思路有很多東西可以進階:
1. 當速度不同時,要根據速度計算灰度等級級。
2. 當半徑不同時,需要更復雜的系數來表達我在某個時間是不是阻擋了一個格子的通過性,即占用了很多個格子的灰度等級。
比如灰色系的朋友,速度變快了,t1時間可以到達遙遠的X了,那么他相對的在更短的時間片里可以飛速通過。
藍色系的朋友,變胖了,還是從C到D,但是占用灰度等級的格子變多了。
通過設計高效的算法,這個方法一樣可以較好的實現等待避讓。
3.5 單位搜索相關
除了尋路和避障外,游戲AI里最耗時的莫過於各種技能和攻擊的范圍搜索了,在幾百個單位的時候,這種搜索需求的消耗也非常可觀。基於上面的數據結構,可以設計出一些很有意思的,快速搜索制定范圍內單位列表的算法。
比如放技能使用的,我們項目里使用了一些數據和鏈表的結合,可以較高效率的搜索到指定格子周圍的單位列表。還有可以基於障礙數據,等到很多地方是否有人這樣的信息,這一塊大家可以根據自己項目的情況來看。
四. 小結
先看一下之前提出的項目要求:
1.單位要可以實時尋找看起來合理的路線行走,路線要避開行人和地形障礙
2.動態避障過程,不能穿人、地形
3.要實現有篩選的尋路,尋路時要忽略掉部分單位
4.有地形玩法,且會被技能動態改變
5.單位數量巨大
6.滿足幀同步需求
7.可以被服務器驗證
其中1-5都已經滿足需求,6通過整個實現過程中全部采用自定義的定點數來完成,7因為我們全部使用平台無關的邏輯代碼,不使用插件,所以也滿足需求。
通過以上的設計,AI運算已經占用很低,基本上達到了性能需求。經過項目實際運行,在3D角色200個左右的情況下,AI的運算依舊非常低。現在ECS逐漸開始流行,結合引擎層面的ECS管理,使用類似的思路來構建極限的AI和算法,我相信還有很大的潛力可以挖掘。
總體來說,這類問題最關鍵的地方還是在於前期數據結構的設計,然后根據性能和項目功能需求來設計、實現算法。根據項目的實際需求,當性能需求沒那么高時,可以采取一些表現更好的方式,如真實物理等。
本文提到的一些算法等,后續陸續開文詳解。
謝謝大家費時閱讀。