遊戲AI之路徑規劃(3)
路徑規劃是尋路的重要優化思想,在了解路徑規劃之前必須先了解基本的尋路算法
可參考:https://www.cnblogs.com/KillerAery/p/9231511.html
使用路徑點(Way Point)作為節點
實際上A尋路算法,對於圖也是適用的,實現只要稍微改一下。
大部分討論A算法使用的是網格點(也就是簡單的二維網格),但是這種內存開銷往往比較大。
而預先設好路徑點,而不是使用網格來當作節點,則可以減少節點數量,順帶也就減少了尋路的運算速度開銷。
(如圖,使用了路徑點作為節點)
此外路徑點的使用,也能使路徑相比網格路徑更加平滑。
洪水填充算法創建路徑點
倘若一個地圖過大,開發人員手動預設好路徑點+路徑連接的工作就比較繁瑣,而且很容易有錯漏。
這時可以使用洪水填充算法來自動生成路徑點,並為它們鏈接。
算法步驟:
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); } } } }
自動生成的導航圖可以調整擴展的距離,從而得到合適的節點和邊的數量。
使用導航網(Navigation Mesh)作為節點
導航網將地圖劃分成若幹個凸多邊形,每個凸多邊形就是一個節點。
(使用凸多邊形,是因為凸多邊形邊上的一個點走到另外一點,不管怎麽走都不會走出這個多邊形。而凹多邊形可能走的出外面。)
使用導航網更加可以大大減少路徑點和搜尋所需的計算量,同時也使路徑更加自然。
區域分割
區域分割有點類似於導航網,但是它屬於更高層次的分割。
即完整地圖分割成若幹個區域,一個區域又可以分割成若幹個導航網的凸多邊形。
區域分割也不僅可以使用在路徑規劃上,也可以利用記錄區域信息達到很多目的。
例如:AI在感知敵人的時候,可以通過區域分割,過濾掉本區域外的所有敵人,只對本區域的所有敵人作感知測試。
常用的區域分割方法有手動分割區域和使用四叉樹(或八叉樹)來分割區域。
四叉樹
四叉樹索引的基本思想是將地理空間遞歸劃分為不同層次的樹結構。
它將已知範圍的空間等分成四個相等的子空間,如此遞歸下去,直至樹的層次達到一定深度或者滿足某種要求後停止分割。
//示例:一個四叉樹節點的簡單結構
struct QuadtreeNode {
Data data;
QuadtreeNode* children[2][2];
int diliverX; //表示這個區域的劃分長度
};
//示例:找到x,y位置對應的四叉樹節點(共2層)
//通過diliver來將x,y歸納為0或1的值,從而索引到對應的子節點。
int diliver = root.diliver;
int diliverX = x / diliver;
int diliverY = y / diliver;
QuadtreeNode* n1 = root.children[diliverX][diliverY];
//如果歸納為1的值,還需要減去該劃分長度,以便進一步劃分
x -= diliverX ? diliver : 0;
y -= diliverY ? diliver : 0;
//再執行類似上一操作
diliver = n1.diliver;
diliverX = x / diliver;
diliverY = y / diliver;
QuadtreeNode* n2 = n1.children[x / diliver][y / diliver];
x -= diliverX ? diliver : 0;
y -= diliverY ? diliver : 0;
四叉樹的結構比較簡單,並且當空間數據對象分布比較均勻時,具有比較高的空間數據插入和查詢效率(復雜度O(logN))
八叉樹
八叉樹類似四叉樹,適用於三維空間的分割(一個立方體可被分成8個小立方體)。
預計算
主要方式是通過預先計算好的數據,然後運行時使用這些數據減少運算量。
可以根據自己的項目權衡運行速度和內存空間來選擇預計算。
(首先以這副圖為示例)
路徑查詢表
借助預先計算好的路徑查詢表,可以以O(|v|)的時間復雜度極快完成尋路,但是占用空間為O(|v|2)。
(|v|為頂點數量)
實現:對每個頂點使用Dijkstra算法,求出該頂點到各頂點的路徑,再通過對路徑回溯得到前一個經過的點。
路徑成本查詢表
有時候,遊戲AI需要考慮路徑的成本來決定行為,
則可以預先計算好路徑成本查詢表,以O(1)的時間復雜度獲取路徑成本,但是占用空間為O(|v|2)。
實現:類似路徑查詢表,只不過記錄的是路徑成本開銷,而不是路徑點。
遊戲AI之路徑規劃(3)