游戏开发中的寻路算法:从A*到导航网格
游戏开发中的寻路算法:从A*到导航网格
引言
在游戏开发中,寻路算法是实现NPC智能移动的核心技术。无论是RTS游戏中的单位移动,还是RPG中的敌人追逐,高效的寻路算法都是不可或缺的。本文将介绍游戏中常用的寻路算法,从经典的A*算法到现代的导航网格技术。
寻路算法基础
寻路算法的目标是在给定的地图上找到从起点到终点的最佳路径。评判路径优劣的标准通常包括:
- 路径长度:更短的路径通常更优
- 计算效率:算法需要在有限的时间内完成
- 路径平滑度:避免不自然的锯齿状路径
A*算法详解
A*算法是游戏开发中最常用的寻路算法之一,它结合了Dijkstra算法的完备性和贪心最佳优先搜索的效率。
算法原理
A*算法使用一个评估函数
是从起点到当前节点的实际代价 是从当前节点到目标的估计代价(启发式函数)
伪代码实现
function A_Star(start, goal)
// 初始化开放列表和关闭列表
openSet := {start} // 待探索的节点集合
closedSet := {} // 已探索的节点集合
// 记录从起点到每个节点的最佳路径
cameFrom := an empty map
// g(n):从起点到n的代价
gScore := map with default value of Infinity
gScore[start] := 0
// f(n):从起点经过n到终点的估计总代价
fScore := map with default value of Infinity
fScore[start] := heuristic_cost_estimate(start, goal)
while openSet is not empty
// 获取f值最小的节点
current := node in openSet with the lowest fScore value
if current = goal
return reconstruct_path(cameFrom, current)
openSet.Remove(current)
closedSet.Add(current)
for each neighbor of current
if neighbor in closedSet
continue
// 计算经过current到达neighbor的代价
tentative_gScore := gScore[current] + dist_between(current, neighbor)
if neighbor not in openSet
openSet.Add(neighbor)
else if tentative_gScore >= gScore[neighbor]
continue // 已找到更好的路径
// 找到了更好的路径
cameFrom[neighbor] := current
gScore[neighbor] := tentative_gScore
fScore[neighbor] := gScore[neighbor] + heuristic_cost_estimate(neighbor, goal)
// 没有找到路径
return failure
C++实现示例
#include <vector>
#include <queue>
#include <unordered_map>
#include <cmath>
struct Node {
int x, y;
float g; // 从起点到该点的代价
float h; // 启发式函数值
Node(int _x, int _y) : x(_x), y(_y), g(0), h(0) {}
float f() const { return g + h; }
bool operator==(const Node& other) const {
return x == other.x && y == other.y;
}
};
// 自定义哈希函数,用于unordered_map
struct NodeHash {
std::size_t operator()(const Node& node) const {
return std::hash<int>()(node.x) ^ std::hash<int>()(node.y);
}
};
// 自定义比较函数,用于优先队列
struct CompareNode {
bool operator()(const Node* a, const Node* b) const {
return a->f() > b->f(); // 小的f值优先
}
};
class AStar {
public:
AStar(int width, int height, bool** obstacles)
: width_(width), height_(height), obstacles_(obstacles) {}
std::vector<Node> FindPath(const Node& start, const Node& goal) {
// 优先队列,按f值排序
std::priority_queue<Node*, std::vector<Node*>, CompareNode> openSet;
// 已访问节点集合
std::unordered_map<Node, bool, NodeHash> closedSet;
// 记录路径
std::unordered_map<Node, Node, NodeHash> cameFrom;
// 初始化起点
Node* startNode = new Node(start.x, start.y);
startNode->h = CalculateHeuristic(*startNode, goal);
openSet.push(startNode);
while (!openSet.empty()) {
Node* current = openSet.top();
openSet.pop();
// 找到目标
if (current->x == goal.x && current->y == goal.y) {
return ReconstructPath(cameFrom, *current);
}
Node currentKey(current->x, current->y);
closedSet[currentKey] = true;
// 检查四个方向的邻居
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
// 跳过对角线和自身
if ((dx == 0 && dy == 0) || (dx != 0 && dy != 0)) continue;
int nx = current->x + dx;
int ny = current->y + dy;
// 检查边界
if (nx < 0 || nx >= width_ || ny < 0 || ny >= height_) continue;
// 检查障碍物
if (obstacles_[nx][ny]) continue;
Node neighbor(nx, ny);
// 如果已经访问过,跳过
if (closedSet.find(neighbor) != closedSet.end()) continue;
// 计算新的g值
float tentativeG = current->g + 1.0f; // 假设每步代价为1
// 检查是否是更好的路径
bool inOpenSet = false;
for (auto it = openSet.c.begin(); it != openSet.c.end(); ++it) {
if ((*it)->x == nx && (*it)->y == ny) {
inOpenSet = true;
if (tentativeG < (*it)->g) {
(*it)->g = tentativeG;
cameFrom[neighbor] = currentKey;
}
break;
}
}
if (!inOpenSet) {
Node* newNode = new Node(nx, ny);
newNode->g = tentativeG;
newNode->h = CalculateHeuristic(*newNode, goal);
openSet.push(newNode);
cameFrom[neighbor] = currentKey;
}
}
}
delete current; // 释放内存
}
// 没有找到路径
return std::vector<Node>();
}
private:
int width_, height_;
bool** obstacles_;
// 曼哈顿距离作为启发式函数
float CalculateHeuristic(const Node& a, const Node& b) {
return std::abs(a.x - b.x) + std::abs(a.y - b.y);
}
// 重建路径
std::vector<Node> ReconstructPath(
const std::unordered_map<Node, Node, NodeHash>& cameFrom,
const Node& current) {
std::vector<Node> path;
Node currentNode = current;
path.push_back(currentNode);
while (cameFrom.find(currentNode) != cameFrom.end()) {
currentNode = cameFrom.at(currentNode);
path.push_back(currentNode);
}
std::reverse(path.begin(), path.end());
return path;
}
};
启发式函数选择
启发式函数的选择对A*算法的性能有重大影响。常用的启发式函数包括:
- 曼哈顿距离:
- 适用于只能沿四个基本方向移动的网格
- 欧几里得距离:
- 适用于可以向任意方向移动的情况
- 对角线距离:
- 适用于允许对角线移动的网格
导航网格(NavMesh)
随着游戏场景的复杂化,基于网格的寻路算法面临着效率和内存消耗的挑战。导航网格(NavMesh)技术应运而生,它将可行走区域表示为一系列凸多边形,大大减少了需要处理的节点数量。
导航网格的优势
- 内存效率:相比于网格,导航网格需要存储的数据更少
- 性能优化:搜索空间大幅减少,提高寻路效率
- 路径平滑:生成的路径更自然,减少了锯齿状移动
- 适应复杂地形:可以更好地表示三维环境中的可行走区域
导航网格的构建过程
- 场景分析:识别可行走区域和障碍物
- 三角剖分:将可行走区域分解为三角形
- 网格优化:合并相邻三角形形成凸多边形
- 连接信息:建立多边形之间的邻接关系
使用导航网格的寻路流程
- 定位:确定起点和终点所在的多边形
- 高层寻路:在多边形网络中使用A*算法找到多边形序列
- 路径优化:使用漏斗算法等技术在多边形序列中找到最短路径
- 平滑处理:应用贝塞尔曲线等技术使路径更自然
动态寻路与路径平滑
在实际游戏中,环境可能会动态变化,角色也需要平滑自然地移动。以下是一些常用技术:
动态寻路
- 增量更新:只重新计算受环境变化影响的区域
- 分层寻路:结合全局路径规划和局部避障
- D* Lite算法:专为动态环境设计的寻路算法
路径平滑
// 简单的路径平滑算法
std::vector<Vector2> SmoothPath(const std::vector<Vector2>& path, float weight = 0.5f) {
if (path.size() <= 2) return path;
std::vector<Vector2> smoothPath = path;
for (int i = 0; i < 10; i++) { // 迭代多次以获得更平滑的结果
for (int j = 1; j < smoothPath.size() - 1; j++) {
smoothPath[j] = smoothPath[j] * (1 - weight) +
(smoothPath[j-1] + smoothPath[j+1]) * 0.5f * weight;
}
}
return smoothPath;
}
实际应用案例
RTS游戏中的群体寻路
RTS游戏中,多个单位需要同时寻路并避免相互碰撞。常用技术包括:
- 流场寻路(Flow Field Pathfinding):为整个地图生成方向场
- 局部避障:使用速度障碍(Velocity Obstacles)或互斥
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 游戏技术博客!
