本系列的教程文章基於 A*Pathfinding Project 4.2.8的官網教程翻譯,每一章節的原文地址都會在教程最下方給出。

介紹一些提升性能的小技巧。

優化性能方式很多,摘取一些介紹一下:

  • Profile!在進行任何優化之前,先通過profiler找出代碼中運行效率較低的地方。要注意的是,如果你使用了多線程的話,那麼尋路的性能就不會在profiler里看到了,因為它沒運行在主線程。
  • 到現在為止,最簡單的優化方案就是啟用多線程(A* Inspector -> Settings)。這會讓尋路運行在Unity主線程之外,這很有用,因為現在大部分計算機或者手機都是多核的。
  • 確保"Show Graphs"(A* inspector 最下面)選項沒有開啟,對於一個大型的Graph來說,每幀都渲染會拖慢遊戲的性能表現。(只在Editor模式下)
  • 路徑日誌對性能也有很大的影響,最多的時候可以減少50%!如果你不需要就關掉。(A* Inspector -> Settings -> Path Log Mode 設置為None)
  • 另外一個需要考慮的事情是Graph的類型。Grid graphs是最簡單的,但是它不能支持非常巨大的世界(千米及以上級別的)。recast graph可以做這樣的使用,並且它在小型的地形上的速度也優於Grid graph,因為它的節點更少。確定就是掃描速度太慢了,運行時更新代價太大。所以,如果你是一個靜態的地形,最好的方式就是使用recast 類型。
  • 不同的移動腳本也有不同的性能。AILerp速度最快,但是還是要根據你遊戲類型去選擇合適的內置移動腳本。
  • 如果你有很多個單位,盡量不要用CharacterControlller,用一個簡單的raycast查到地面之後,它的運行速度大部分是都是優於CharacterControlller的。
  • 盡量減少新路徑的計算頻率,控制欄位是repathRate。你可以禁用自動路徑計算,或者只在agent運行開始的時候計算一次?或者你自定義一個計算腳本,在遠離目標點的時候減少計算頻率,靠近的時候再增加。
  • 在修飾腳本上使用較低的質量設定。比如減少SimpleSmoothModifier平滑路徑的次數。AIPath腳本其實已經做的比較好了。
  • 如果你使用AIPath 或者 RichAI 腳本: 當你有大量的AI,那麼打開AiBase腳本,然後把Update和FixedUpdate移除,如果你不使用rigidbodies的話。原因是因為,即使是空方法,只要你載入腳本里,Unity就會調用。

啟發函數優化(Heuristic Optimization)

(譯註:本篇在原文里是另外一個章節了 由於本篇內容較少,且相關性比較大 所以放在一個章節里,後面的兩個部分也是,不再闡述。)

這裡講怎麼使用啟發來顯著增加遊戲性能。

通常尋路的時候,我們都會使用啟發式的形式。一個啟發式的含義就是粗略估計某點離目標點的距離。一般都是使用歐幾里得距離來表述,這是最快的並且結果相對較好的。那麼這種方式在非開放性世界,並且周圍存在很多小型障礙物的時候並不是很好。這個時候如果優化啟發式會比單純減少節點搜索收益要高。

pro專有功能

結果

這種技術其實就是通過優化啟發來達到減少搜索的方式。這會切切實實的增加性能表現。看下下面的兩張圖,第一張是沒有開啟優化的,而第二張是開了優化的。可以看到第二張的搜索直接規避了某些方向上無用的搜索。

沒有啟用啟發優化
啟用了啟發優化

背景

如果你的世界是靜態的,或者變動非常少,那麼有一種技術可以預先計算節點之間的距離,並獲得更好的啟發式。在某些情況它甚至能達到10倍的性能提升。

當然最優的啟發式其實就是,我們能知道graph上任意一點到其他任意一點的距離。這顯然不切實際,不僅要做巨量的提前計算,還要把結果存儲起來以供查詢,消耗大量的內存。同樣數據量巨大的時候,查詢本身也是非常損耗的事情。

那麼我們其實可以選取幾個關鍵點(軸點)然後計算他們到每個其他點的距離,然後這就可以平衡2個極端,帶來較好的啟發式。

當然無論使用哪種啟發演算法,A*都至少要搜索最短路徑上的所有節點,所以這種技術在打的開放Grid graph上不會帶來任何的性能提高,因為開放環境下的路徑搜索總是會比最短路徑多很多。

下面的圖展示了3中不同的優化路徑,演算法會搜索下面所有綠色節點,因為他們的權值都是一樣的。所有這些節點都是最優路徑,因為啟發計算值相同。

A*Pathfinding插件會自動選取比較好的軸點,因為其實手動很難選出。另外經過多輪測試,我手動選取的軸點都沒有自動選取的好。。。

手動放置軸點

當放置軸點的時候,很重要的事情就是要把它們放在一個死胡同里。當多條路徑可以擴展到軸點,並且仍然是最短路徑的時候,性能是最好的。

當然也有一些特使的做法你可以參考。比如TD遊戲中,幾乎所有的目標都是往同一個目標點移動,所有你只需要放置一個軸點到目標點,這樣所有的路徑計算都會大幅增加,因為每個點到軸點的距離都是已知的。

自動放置軸點

兩種模式提供。一種是完全隨機,一種是提供一種演算法,讓不同點之間盡量分散。第二種方法其實給了更高質量的遺傳演算法,但是計算就會變慢,並且由於是串列的分析節點,無法啟用多線程。

下面的2個圖就是分別用第一種和第二種方式創建的。第二種看起來就是防止在各種死胡同或者角落裡,因此它的結果是最好的。

如何去選擇你要生成的軸點數量呢?最好的方式就只有慢慢試了,看幾個才適合你。軸點越多,額外的開銷也就越大,比如你用了100個軸點所減少的搜索收益甚至遠遠小於直接搜索。所以給定一個推薦值在1-100吧,你們自己試去。

你可能會注意到,當你開始了遊戲知道尋路開始工作,會有一個延遲時間。這就是在準備啟發式的搜索數據。如果你發現了這個延遲對你而言是一個比較大的問題,那麼就使用Random模式來替代Random Spread Out模式,或者減少軸點的數量。

池(Pooling)

對象池的作用是減少load產生的GC。池是一個最大限度提高性能表現的方式,主要用在低端設備或者系統大批量使用物體的時候。Mono的的垃圾收集器能夠很好的收集那些不再使用的物體,但是一個很大的問題在於,每次GC運行的時候都需要凍結主線程才能收集分析。你可能會注意到,你的遊戲每隔幾秒就會有一個小卡頓,尤其是一些內存受限或者處理器功率不夠的設備上。

那麼一個很好的解決方案就是把對象,池化。意思就是如果你不需要一個物體了,就把它放進池裡面,當你需要一個物體的時候,你再從池裡拿出來。這樣因為不會被垃圾收集器收集到,所以也盡量減少了GC觸發的概率。

路徑池

池方案主要用在路徑和列表。因為這是分配最多的部分。池邏輯其實很簡單,只要很少的代碼,但是它很容易出錯,所以要謹慎對待。

A*Pathfinding插件的池是基於一種手動引用計數實現的。如果你開始使用一條Path了,那麼你需要調用一個特殊的方法。當然,當你停止使用的時候,比如被一條新路徑取代了,你應該用特殊的release方法釋放它。如果一條路徑被釋放的時候已經沒有任何其他地方使用了,那麼就會會到池裡,供循環調用。這就表示,它的變數可能會被隨時改變,所以當你release一條路徑之後,確保不要再保留它的引用。或者至少你要保證不能再使用它了。還要注意的是 因為Path是循環的,所以它內部的vectorPath 和Path list都是循環的,所以這些變數也不要再用了額。當然如果你真的是想用,vectorPath/path變數,你可以把它們存儲為一個局部變數,然後再把Path里的設置為Null。這樣當你釋放路徑的時候,因為值為NULL,就不會被循環引用了。

Pathfinding.Path.Claim

Pathfinding.Path.Release

如果你沒有調用claim或者release,那麼這條路徑就不會被放進池裡。所以如果你不想使用池這麼麻煩的東西,你也可以不用。如果你引用了一個Path,但是又忘了Release,那麼當所有引用它的都釋放了之後,它仍然會被GC收集到,但是不會再放入池中了。如果你沒有調用Claim就調用了Release,或者調用了多次Release,那麼會產生一條錯誤日誌。

下面展示下池的正確用法:

public class SomeAI : MonoBehaviour {
ABPath path;

public IEnumerator Start () {
while (true) {
GetComponent<Seeker>().StartPath(transform.position, transform.position + transform.forward*10, OnPathComplete);
}
}

void OnPathComplete (Path p) {
//Release any previous paths
if (path != null) path.Release(this);

path = p as ABPath;

//Claim the new path
path.Claim(this);
}

void Update () {
//Draw the path in the editor
if (path != null && path.vectorPath != null) {
for (int i = 0; i < path.vectorPath.Count-1; i++) {
Debug.DrawLine(path.vectorPath[i], path.vectorPath[i+1], Color.green);
}
}
}
}

所有就跟之前說的,一個Path回到池裡之後,它的變數也會跟著成為循環引用,所有如果你持有了某些變數的引用的話,就會導致數據錯亂,記得不要持有額。

Claim 和 Release引用,主要就是減少使用者的不恰當使用。有錯誤日誌之後就比較容易檢查了。

列表池

這個可能對於很多使用者來說並不重要。但是對於一些特殊用戶還是有些作用。這個插件其實還提供了了泛型類型的列表池。它會阻止系統分配新的列表。列表池的使用也很簡單,獲取一個引用就用PathFinding.Util.ListPool.Claim,釋放的時候就用PathFinding.Util.ListPool.Release。ListPool類提供了一個type參數,所以你可以創建任何類型的List。

// Get a reference to a list
List<int> myList = Pathfinding.Util.ListPool<int>.Claim();

// Do something with it
for (int i = 0; i < 100; i++) myList.Add(i);
int fiftytwo = myList[52];

// Release it
Pathfinding.Util.ListPool<int>.Release(ref myList);
// The ref parameter in the Release call is used to set the myList variable
// to null at the same time to avoid using the list again by mistake.

調試

由於池的設置有點繁瑣,這也取決於你調用它的腳本寫的複雜程度,而且結果也不太好立即驗證,所以一個合適的調試器會比較好。嗯~我們有,用Components ->Pathfinding-> Debugger 給任何一個物件添加腳本即可。

另外一個很有用的事情就是開啟 ASTAR_POOL_DEBUG。位於Optimizations(Pro版本專有)頁簽下面。如果你是free版本,只需要打開Path.cs文件,取消最上面 關於ASTAR_POOL_DEBUG的注釋就好。

當啟用了之後,每條路徑銷毀都會提供Log信息,包括,池化了,或者不正確的使用了。

彙編準則(Compiler Directives)

在pro版本里, A* Inspector上有個頁簽叫Optimization。

這裡提供了一些編譯選項供選擇。(可以理解為宏),例如:

#define DEBUG

public void Start () {
#if DEBUG
Debug.Log ("The DEBUG compiler directive is defined");
#else
Debug.Log ("The DEBUG directive is not defined");
#endif
}

可以看到,它的工作原理就類似於 IF的語句。不同的是這個判斷是在編譯的時候,而不是運行時。有些功能可能需要根據不同的使用者進行定製,那麼就可以使用這個方式讓編譯器來幫你解決問題。

這些宏都存儲在player settings的Scripting Define Symbols區域,他們會被所有的工程代碼共享。

如果你的目標平台是移動端,你可能會想儘可能的減少文件大小。ASTAR_NO_ZIP你可以使用這個選項來移除對DotNetZip的庫引用。如果啟用了這個選項,你可以移除Assets/AstarPathfindingProject/Plugins/DotNetZip目錄下的dll文件,這大概會減少幾百K的大小吧。但是它會被cache起來,增加啟動時間。

結尾

譯註:這篇文章包含了原文的四篇。因為原來4個都比較短,並且關聯性很大所以放在一篇裡面講解。

原文地址:

Documentation?

arongranberg.com


推薦閱讀:
相关文章