大規模單位實時遊戲尋路的構建

本文發佈於遊戲程序員劉宇的我的博客,歡迎轉載,請註明來源https://www.cnblogs.com/xiaohutu/p/10504586.html 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和算法,我相信還有很大的潛力能夠挖掘。

整體來講,這類問題最關鍵的地方仍是在於前期數據結構的設計,而後根據性能和項目功能需求來設計、實現算法。根據項目的實際需求,當性能需求沒那麼高時,能夠採起一些表現更好的方式,如真實物理等。

本文提到的一些算法等,後續陸續開文詳解。

謝謝你們費時閱讀。

相關文章
相關標籤/搜索