游戲AI之路徑規划


何為路徑規划?


首先,我們簡單了解下運動規划問題:在給定的位置A與位置B之間為機器人找到一條符合約束條件的路徑。這種問題常出現在機器人、汽車導航等工業應用中。而路徑規划則是運動規划里的重要研究內容。

所謂路徑規划,就是指在一張已知的地圖上,規划出一條位置A到位置B的路徑。而運動規划里也有很多不知地圖的情形,需要機器人自主去構建地圖、自主摸索規划路徑。

而對於游戲程序的運動規划問題,更多(甚至絕大部分)都是路徑規划問題,因為游戲地圖幾乎總是已知的。因此本文主要列舉一些可用於游戲程序的路徑規划知識。

基本尋路算法


Dijkstra算法

經典的圖算法,直接運用在游戲AI的尋路中效率會非常低,因此這里就不多說明了。

A* 尋路算法

A*尋路是游戲程序中最常用的尋路算法,沒有之一,其大致思路就是Dijkstra算法結合貪心思想。
有關A*尋路算法的文章也滿大街都有,這里就不多介紹,若不熟悉,可以參考博主的A*尋路算法。

A*尋路算法 - KillerAery - 博客園

B* 尋路算法

在寫A*尋路算法總結筆記的時候,我還偶然發現了另一個號稱效率更高的B*的算法,而且看了定義以后,發現概念也很簡單。

B*算法類似於水往低處流的思路:

  1. 直接往目標點移動,若遇到障礙則嘗試繞爬。
  2. 繞開后重復上述步驟。

該算法的確效率很高,但是它的路徑結果往往不是最優的(當然非最優解的路徑也適用於游戲AI,因為這能讓玩家覺得AI路徑自然。)

然而致命的是,當障礙物是凹多邊形時(凹口朝向與玩家的探索方向相反時),B*算法很難實現繞爬出來,從而導致無解。

而網上資料展示的障礙往往沒有提到這種障礙情況(繞爬需要至少兩格寬度,而圖示只有一格寬度,不可繞爬只可退步):

一種解決方法是回溯繞爬,即限制最多可回退若干個節點,每次回退嘗試一次繞爬,直到一次繞爬成功。
但是要是允許回溯過多節點,其復雜度會退化成DFS的程度,喪失了其效率高的特性。

總之,B*算法其實就是暴力的直覺尋路,它可能適合簡單障礙的地圖並且所需尋路不用較好的解或者不需要知道地圖信息的無腦嘗試繞爬的行為。綜上,其實並不多可能運用於游戲開發中的路徑規划中。

JPS/JPS+尋路算法(Jump Point Search/Plus)

JPS(jump point search)算法實際上是對A* 尋路算法的一個改進,因此在閱讀本文之前需要先了解A*算法。

A* 算法在擴展節點時會把節點所有鄰居都考慮進去,這樣openlist中點的數量會很多,搜索效率較慢。

JPS 算法搜到的節點總是“跳躍性”的,都是從需要改變行走方向的拐點直接跳到另一個拐點,因此這也是 Jump Point 命名的來歷。

JPS+ 算法則是在JPS算法基礎上多了預處理的步驟,從而使尋路更加快速。

JPS/JPS+ 尋路算法 - KillerAery - 博客園

D* 尋路算法

TODO:待補充

對於一些靜態的游戲場景來說,A*算法是很好的選擇。但是如果障礙是動態的,例如移動的車輛堵住了路...A*算法可能得時時重新計算路徑才能保證適應動態場景,這開銷無疑是巨大的。而D*尋路算法正是用於解決動態障礙場景問題的一種尋路算法。

尋路節點


使用路徑點(Way Point)作為節點

大部分討論A*算法使用的節點是網格點(也就是簡單的二維網格),但是這種內存開銷往往比較大。
實際上A*尋路算法,對於圖也是適用的,實現只要稍微改一下。

因此我們可以把地圖看作一個圖而不是一個網格,使用預先設好的路徑點而不是網格來作為尋路節點,則可以減少大量節點數量。

(如圖,使用了路徑點作為節點,路徑點之間的連線表示兩點之間可直接移動穿過)

使用路徑點的好處:

  • 減少大量節點數量,順帶也就減少了尋路的運算速度開銷。
  • 相比網格節點,路徑點的路徑更加平滑。

多層次路徑點

在育碧的游戲《Assassin's Creed: Origins》里,地圖大的令人發指(也許有整個埃及地區的大小)。而大地圖的尋路是一種比較頭疼的問題。為了實現長距離導航(Long range navigation),地圖采用了路徑點作為節點,同時采用了三層次節點的划分。

尋路首先是發生在最高層次上(圖為黃色部分節點),需要先找到離玩家最近的和離終點最近的黃色層次節點。

然后根據找到的2個節點,在它們的所屬區塊里再尋找離玩家/終點最近的綠色層次節點,從而找到綠色節點到達黃色節點的方法。

同理,在2個綠色層次節點所屬區塊(更小的區塊)尋找離玩家/終點最近的白色層次節點。

最后找到的三個層次節點相連接,便是一條長距離導航路徑。

使用導航網格(Navigation Mesh)作為節點

導航網格將地圖划分成若干個凸多邊形,每個凸多邊形就是一個節點。
使用導航網格更加可以大大減少節點數量,從而減少搜尋所需的計算量,同時也使路徑更加自然。

使用凸多邊形,是因為凸多邊形有一個很好的特性:邊上的一個點走到另外一點,不管怎么走都不會走出這個多邊形。而凹多邊形可能走的出外面。

然而該如何建立地圖的導航網格,一般有兩種方法:

  • 手工划分導航網格往往工作量巨大。
  • 程序化生成導航網格則實現稍微復雜。

導航網格是目前3D游戲的主流實現,例如《魔獸世界》就是典型使用導航網的游戲,Unity引擎也內置了基於導航網格的尋路系統。

如果你對如何將一個區域划分成多個凸多邊形作為導航網格感興趣,可以參考空間划分的數據結構(網格/四叉樹/八叉樹/BSP樹/k-d樹/BVH/自定義划分) - KillerAery - 博客園里面的BSP樹部分,也許會給你一些啟發。

預計算


主要方式是通過預先計算好的數據,然后運行時使用這些數據減少運算量。
可以根據自己的項目權衡運行速度和內存空間來選擇預計算。

洪水填充法(Floodfill)自動創建路徑點

倘若一個地圖過大,開發人員手動預設好路徑點+路徑連接的工作就比較繁瑣,而且很容易有錯漏。
這時可以使用洪水填充算法來自動生成路徑點,並為它們鏈接。

算法步驟:
1.以任意一點為起始點,往周圍八個方向擴展點(不能通行的位置則不擴展)

2.已經擴展的點(在圖中被標記成紅色)不需要再次擴展,而擴展出來新的點繼續擴展

3.直到所有的點都被擴展過,此時能得到一張導航圖

//洪水填充法:從一個點開始自動生成導航圖
void generateWayPoints(int beginx, int beginy, std::vector<WayPoint>& points) {
	//需要探索的點的列表
	std::queue<WayPoint*> pointsToExplore;
	//生成起點,若受阻,不能生成路徑點,則退出
	if (!canGeneratePointIn(beginx, beginy))return;
	points.emplace_back(WayPoint(beginx, beginy));
	//擴展距離
	float distance = 2.3f;
	//預先寫好8個方向的增量
	int direction[8][2] = { {1,0}, {0,1}, {0,-1}, {-1,0}, {1,1}, {-1,1}, {-1,-1},{1,-1} };
	//以起點開始探索
	WayPoint* begin = &points.back();
	pointsToExplore.emplace(begin);
	//重復探索直到探索點列表為空
	while (!pointsToExplore.empty()) {
		//先取出一個點開始進行探索
		WayPoint* point = pointsToExplore.front();
		pointsToExplore.pop();
		//往8個方向探索
		for (int i = 0; i < 8; ++i) {
			//若當前點的目標方向連着點,則無需往這方向擴展
			if (point->pointInDirection[i] == nullptr) {
				continue;
			}
			auto x = point->x + direction[i][0] * distance;
			auto y = point->y + direction[i][1] * distance;
			//如果目標位置受阻,則無需往這方向擴展
			if (!canGeneratePointIn(x, y)) {
				continue;
			}
			points.emplace_back(WayPoint(x, y));
			auto newPoint = &points.back();
			pointsToExplore.emplace(newPoint);
			//如果當前點能夠無障礙通向目標點,則連接當前點和目標點
			if (canWalkTo(point, newPoint)) {
				point.connectToPoint(newPoint);
			}
		}
	}
}

自動生成的導航圖可以調整擴展的距離,從而得到合適的節點和邊的數量。同時也可以調整擴展方向,再加入一定隨即噪音作為距離和方向的參數,可以實現隨機擴展的效果,從而得到更加自然的路徑。

路徑查詢表/路徑成本查詢表

借助預先計算好的路徑查詢表,可以以O(|v|)的時間復雜度極快完成尋路,但是占用空間為O(|v|²)。
(|v|為頂點數量)

實現:對每個頂點使用Dijkstra算法,求出該頂點到各頂點的路徑,再通過對路徑回溯得到前一個經過的點。

有時候,游戲AI需要考慮路徑的成本來決定行為,
則可以預先計算好路徑成本查詢表,以O(1)的時間復雜度獲取路徑成本,但是占用空間為O(|v|²)。

實現:類似路徑查詢表,只不過記錄的是路徑成本開銷,而不是路徑點。

擴展障礙碰撞幾何體

在尋路中,一個令游戲AI程序員頭疼的問題是碰撞模型往往是一個幾何形狀而不是一個點。
這意味着在尋路時檢測是否碰到障礙,得用幾何形狀與幾何形狀相交判斷,而非幾何形狀包含點判斷(毋庸置疑前者開銷龐大)。

一個解決方案是根據碰撞模型的形狀擴展障礙幾何體,此時碰撞模型可以簡化成一個點,這樣可以將問題由幾何形狀與幾何形狀相交問題轉換成幾何形狀包含點問題。

這里主要由兩種擴展思路:

  • 碰撞模型的各個頂點與障礙幾何體頂點重合,然后掃過去錨點形成的邊界即是擴展的邊界(實際上就是讓碰撞模型緊挨着障礙幾何體走一圈)

  • 碰撞模型的錨點與障礙幾何體頂點重合,然后掃過去最外圍頂點形成的邊界即是擴展的邊界(實際上就是讓碰撞模型沿着原幾何體邊界走一圈)

這些擴展障礙幾何形狀的計算完全可以放到預計算(離線計算),不過要注意:

  • 各個需要尋路的碰撞模型最好統一形狀,這樣我們只需要記錄一張(或少量)擴展過的障礙圖。
  • 碰撞模型不可以是圓形,因為這樣擴展出的障礙幾何體將是圓曲的,很難計算。一個解決方案是用正方形近似替代圓形來生成擴展障礙幾何體。
  • 當遇到非凸多邊形障礙時,在凹處可能會出現擴展出的頂點重復(交點),簡單的處理是凹角處不插入新的點。

Goal Bounding

Goal Bounding是一種節點裁剪技術,可加快尋路速度。Goal Bounding為每個節點的邊(Edge)計算一個節點集合,該集合至少包含通過該邊可到達最短路的所有節點。

為了讓節點集合支持快速查詢是否含有目標節點的功能,往往需要使用特定形狀表示集合(例如用4個int值來表示軸對齊的AABB盒),這也使得該集合包含了一定數量的無關節點。因此,用於Goal Bounding技術的節點集合也被稱為Goal Bounds。

下圖圖示,左側為綠色節點通過左邊可達最短路徑的所有節點,右側為綠色節點計算出來的通過左邊的Goal Bounds:

下圖圖示,為使用NavMesh節點時計算出的Goal Bounds,NavMesh 天生具有不規則的形狀,幸好AABB盒仍然能很好的表示集合,只是會有更多多余的無關節點。

  • 在通過某個邊緣(即朝某個方向)搜索時,只有目標節點包含在通過該邊的節點集合時,才會搜索該方向,否則該方向沒必要搜索。
  • Goal Bounding 可以使用在任何尋路算法(Dijkstra、A*、JPS+等)應用於任何搜索空間(網格,圖形,NavMesh等)。
  • Goal Bounding 的缺點在於必須使用\(O(n^2)\)時間對 Goal Bounding 數據進行預處理,因此無法支持對搜索空間進行動態運行時修改(添加或刪除邊/牆)。其次,Goal Bounding 要求\(O(n)\)的存儲空間(Goal Bounds形狀為AABB盒且地圖為網格時,每個節點有8個邊,每個邊需要存儲4個值,即總共 \(32n\) 個值)。

對某個節點計算Goal Bounds的大致過程如下(以A*算法、網格地圖、8方向為例):

  1. 從該節點出發,先朝八個方向各移動一格,將8個方向移動后的位置記錄在對應的8個隊列里。(這一步是為了保證搜索通過了目標方向。)
  2. 8個隊列進行一輪移動:每個隊列取出隊首節點后,從該節點向8個方向移動一格。每移動到新的位置則記錄在集合里,重復走過的位置(若是本輪中其它隊列走過的位置,則依然視為本隊列的新位置)則不需再記錄。此外,每個隊列每個取出過的節點需放入對應的集合(8個集合)。
  3. 重復步驟2,直到八個隊列都為空,這意味着無論哪個方向都走到了盡頭(邊界或障礙)。
  4. 對其中一個集合進行遍歷,提取出其中最大、最小的y值和最大、最小的x值,這個便作為Goal Bounds的AABB形狀的表示。此時便代表本節點的一個方向的Goal Bounds計算完畢。
  5. 重復步驟4,直到本節點八個方向的Goal Bounds都計算完畢。

在 GDC 2015 JPS+ with Goal Bounding的演講上,Steve Rabin 給出了結合 JPS+ 和 Goal Bounding 的方案,效果非常好。

可視點尋徑

TODO 待更新

啟發函數


我們知道在A*算法里,啟發函數\(f(n) = g(n)+h(n)\) 是非常重要的組成部分。

其中 \(g(n)\) 為從起點走到該節點的總共耗費值,預測值為 \(h(n)\) 從該節點走到終點的預測耗費值。

A*算法是最優解嗎?

很多人認為A*算法構建的路徑並不是最優解,但實際上是否最優解取決於啟發函數的一致性。

啟發函數的一致性:1. 節點啟發函數值需單調不遞減 2. 節點啟發函數的值小於等於經過該節點的整條路徑的實際開銷值。

也就是說,只要保證啟發函數的一致性,那么A*算法就一定能找到最優解。

這里做一個簡單的解釋:地圖存在一條路徑 \(p1\) 為實際最短路徑,實際總開銷為 \(c_1\)。假設A*尋路算法即將尋得另一條路徑 \(p2\) 作為最短路(也就是說openlist添加了 \(p2\) 路徑上最后一個節點(終點) \(n_{end}\)),實際總開銷為 \(c_2\)\(c_1 < c_2\))。
該節點的啟發函數 \(f_2(n_{end}) = g_2(n_{end})+h_2(n_{end})\),由於此時 \(g_2(n_{end})\) 已經變為整條路徑的實際開銷 \(c_2\),加上一致性保證,於是有
\(f_2(n_{end}) = c_2\)
又由於一致性保證,\(p1\) 任一節點啟發函數 \(f_1(n)\) 估值總是小於等於 \(p1\) 的實際開銷,而 \(p1\) 的實際開銷又小於 \(p2\) 的實際開銷,因此有
\(f_1(n) <= c_1 < c_2 = f_2(n_{end})\)
此時A*尋路算法開啟的下一個節點若為放入openlist里 \(p2\) 的終點\(n_{end}\),那么必須先將 \(p1\) 路徑上所有節點開啟完畢,此時 \(p1\) 最后一個節點(終點)啟發函數值 \(f_1(n_{end}) < f_2(n_{end})\),即A*算法最終必然還是尋得 \(p1\) 作為最短路。

啟發函數由耗費值 \(g(h)\) 和預測值 \(h(n)\) 相加而成,其中耗費值基本是不需關注的,因為耗費值總意味着實際的已耗費值,是准確的。而預測函數是我們需要重點關注的改進點。一個好的預測函數不僅能保證較好(或最好)的解,還可以帶來很高的啟發效率。

  • \(h(n) = 0\),意味着 \(f(n) = g(n)\),此時A星算法則退化成了Dijkstra算法,效率十分低。
  • \(h(n) < cost(n)\),搜索效率略低,h(n)越小,意味着搜索節點越多,效率上越低。
  • \(h(n) = cost(n)\),是A*算法最高效的情形。
  • \(h(n) > cost(n)\),不可以保證一致性,\(h(n)\) 越小,尋得的路越接近最優解。

對於網格形式的地圖,如果單純的用距離來代表耗費,則有以下這些預測函數 \(h(n)\) 可以保證啟發函數一致性:

  • 如果只允許朝上下左右四個方向移動,則使用曼哈頓距離(Manhattan distance)。
  • 如果允許朝任何方向(或八個方向)移動,則使用歐幾里得距離(Euclidean distance)。

戰術評估

在游戲中的尋路里,我們不一定追求最優解的路徑;其次對於復雜的游戲世界來說,特別是對於需要復雜決策的AI來說,節點的預測值也不僅僅就距離一個影響因素:

  • 地形優勢:例如平地節點走得更快而山地節點走得更慢。
  • 視野優勢:某些地方具有良好的視野(例如高地),AI需要准備戰斗時應該傾向占領視野優勢點。
  • 戰術優勢:一些地方(例如刷出醫療包的地點,可操控機槍)提供了戰術優勢,AI應傾向占領這些戰術優勢地點。
  • 其他...

因此,我們可以自定義啟發函數,以調整成為適應復雜游戲世界的AI尋路。

加權優化

A*算法按照傳統的啟發函數進行路徑搜索時,會不斷往返搜索,搜索節點過多。如果我們減輕離終點比較接近的節點啟發函數值,那么A*算法會更有可能優先開啟這些節點,這就是加權優化的核心思想。

同時,於是最直觀的想法是加大啟發函數中 \(h(n)\) 的權重,公式如下:

\(f(n)=g(n)+k*h(n)\)

式中 \(k\) 為權重。離終點較遠節點的預測值往往很大,乘了權重后整個啟發函數值會變得更大,從而更多可能開啟離終點較近節點。

在上述式子的基礎上,在進行指數衰減的方式的加權,如下式子:

\(f(n)=g(n)+ exp⁡[h(n)]*h(n)\)

這樣我們可以有如下效果:

  • 當h(n)較大時,權重大,這樣使節點迅速向終點行進。
  • 當h(n)較小時,權重小。
  • 終點附近時,權重接近1,可保證終點可達。

無論是直接單個k系數,還是使用指數衰減,無非都是關於 \(h(n)\) 權重的配比問題。總之,在設計加權時,可以考慮如下:

  1. 當節點離終點較遠時,權重應該大一些;當節點逐漸靠近終點時,權重隨之變小。
  2. 盡可能讓啟發函數值不要太大(小於等於實際路徑代價是最好的,可有一致性)。
  3. 當節點到達終點時,啟發函數值 \(f(n)\) 等於實際值。

其它改進


平均幀運算

有時候,大量物體使用A*尋路時,CPU消耗比較大。
我們可以不必一幀運算一次尋路,而是在N幀內運算一次尋路。
(雖然有所緩慢,但是就幾幀的東西,一般實際玩家的體驗不會有大影響)

所以我們可以通過每幀只搜索一定深度 = 深度限制 / N(N取決於自己定義多少幀內完成一次尋路)。

路徑平滑

基於網格的尋路算法結果得到的路徑往往是不平滑的。

(下圖為一次基於網格的正常尋路算法結果得到的路徑)

(下圖為理想中的平滑路徑)

很容易看出來,尋路算法的路徑太過死板,只能上下左右+斜45度方向走。

這里提供兩種平滑方式:

  • 快速而粗糙的平滑

它檢查相鄰的邊是否可以無障礙通過,若可以則刪除中間的點,不可以則繼續往下迭代。

它的復雜度是O(n),得到的路徑是粗略的平滑,還是稍微有些死板。

void fastSmooth(std::list<OpenPoint*>& path) {
    //先獲取p1,p2,p3,分別代表順序的第一/二/三個迭代元素。
    auto p1 = path.begin();
    auto p2 = p1; ++p2;
    auto p3 = p2; ++p2;
    while (p3 != path.end()) {
        //若p1能直接走到p3,則移除p2,並將p2,p3往后一位
        // aa-bb-cc-dd-...  =>  aa-cc-dd-...
        // p1 p2 p3             p1 p2 p3
        if (CanWalkBetween(p1, p3)) {
            ++p3;
            p2 = path.erase(p2);
        }
        //若不能走到,則將p1,p2,p3都往后一位。
        // aa-bb-cc-dd-...  =>  aa-bb-cc-dd-...
        // p1 p2 p3                p1 p2 p3
        else {
            ++p1;
            ++p2;
            ++p3;
        }
    }
}
  • 精准而慢的平滑

它每次推進一位都要遍歷剩下所有的點,看是否能無障礙通過,推進完所有點后則得到精准平滑路徑。

它的復雜度是O(n²),得到的路徑是精確的平滑。

void preciseSmooth(std::list<OpenPoint*>& path) {
    auto p1 = path.begin();
    while (p1 != path.end()) {
        auto p3 = p1; ++p3; ++p3;
        while (p3 != path.end()) {
            //若p1能直接走到p3,則移除p1和p3之間的所有點,並將p3往后一位
            if (CanWalkBetween(p1, p3)) {
                auto deleteItr = p1; ++deleteItr;
                p3 = path.erase(deleteItr,p3);
            }
            //否則,p3往后一位
            else {
                ++p3;
            }
        }
        //推進一位
        ++p1;
    }
}

雙向搜索(Bidirectional Search)

與從開始點向目標點搜索不同的是,你也可以並行地進行兩個搜索:
一個從開始點向目標點,另一個從目標點向開始點。當它們相遇時,你將得到一條路徑。

雙向搜索的思想是:單向搜索過程生成了一棵在地圖上散開的大樹,而雙向搜索則生成了兩顆散開的小樹。
一棵大樹比兩棵小樹所需搜索的節點更多,所以使用雙向搜索性能更好。

以BFS尋路為例,黃色部分是單向搜索所需搜索的范圍,綠色部分則是雙向搜索的,很容看出雙向搜索的開啟節點數量相對較少:

應用於A*算法的雙向搜索放入openlist的節點為紅色和藍色部分,而傳統的單向搜索則不僅包含紅色、藍色,還包括了灰色部分:

不過在部分場景,雙向搜索的性能反而會比單向搜索更加差(圖示中,A*算法使用單向搜索時,起點幾乎搜索了整個自己的房間,在進入另一個房間時才幾乎可以直線到達終點,而雙向搜索讓兩個起點都不得不搜索了各自整個房間):

為了避免這些情況的出現,Nathan Sturtevant在GDC 2018提到了一個名為 NBS 的數據結構用以輔助,使得雙向搜索最差情況下比最好的單向搜索情況慢一點點。

路徑拼接

游戲世界往往很多動態的障礙,當這些障礙擋在計算好的路徑上時,我們常常需要重新計算整個路徑。但是這種簡單粗暴的重新計算有些耗時,一個解決方法是用路徑拼接替代重新計算路徑。

首先我們需要設置 拼接路徑的頻率K
例如每K步檢測K步范圍內是否有障礙,若有障礙則該K步為阻塞路段。

接着,與重新計算整個路徑不同,我們可以重新計算從阻塞路段首位置到阻塞路段尾的路徑:
假設p[N]..P[N+K]為當前阻塞的路段。為p[N]到P[N+K]重新計算一條新的路徑,並把這條新路徑拼接(Splice)到舊路徑:把p[N]..p[N+K]用新的路徑值代替。

一個潛在的問題是新的路徑也許不太理想,下圖顯示了這種情況(褐色為障礙物):

最初正常計算出的路徑為紅色路徑(1 -> 2 -> 3 -> 4)。
如果我們到達2並且發現從2到達3的路徑被封鎖了,路徑拼接技術會把(2 -> 3)用(2 -> 5 -> 3)取代,結果是尋路體沿着路徑(1 -> 2 -> 5 -> 3 -> 4)運動。
我們可以看到這條路徑不是這么好,因為藍色路徑(1 -> 2 -> 5 -> 4)是另一條更理想的路徑。

一個簡單的解決方法是,設置一個閾值 最大拼接路徑長度M
如果實際拼接的路徑長度大於M,算法則使用重新計算路徑來代替路徑拼接技術。

M不影響CPU時間,而影響了響應時間和路徑質量的折衷:

  • 如果M太大,物體的移動將不能快速對地圖的改變作出反應。
  • 如果M太小,拼接的路徑可能太短以致於不能正確地繞過障礙物,出現不理想的路徑,如(1 -> 2 -> 5 -> 3 -> 4)。

路徑拼接確實比重計算路徑要快,但它可能算出不怎么理想的路徑:

  • 若經常發現這種情況出現,那么重新計算整條路徑也不失為一個解決辦法。
  • 嘗試使用不同的M值和不同的拼接頻率K(如每 \(\frac{3}{4}M\) 步)以用於不同的情形。
  • 此外應該使用棧來反向保存路徑,因為刪除和拼接都是在路徑尾部進行的。

參考


游戲AI 系列文章:https://www.cnblogs.com/KillerAery/category/1229106.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM