#1024程序员节|征文#
一.前言
在游戏开发领域,算法是不可或缺的重要组成部分。无论是基础的数据处理还是复杂的游戏AI,都依赖各种经典算法的应用,如排序算法、路径规划算法等。尤其在大规模多人在线游戏(MMORPG)中,路径寻路算法显得尤为关键。Theta算法作为A算法的改进,因其高效的路径优化能力,广泛应用于游戏中的NPC行为模拟和玩家角色移动优化。本文将深入探讨Theta*算法的原理及其在游戏开发中的应用,带你更好地理解其在实际场景中的价值。
二.广告
欢迎喜欢或者从事CocosCreator开发的小伙伴请加入我的大家庭CocosCreator游戏开发Q群:26855530
三.什么是Theta*
Theta*(Theta Star)是A星算法的优化版本,它完全集成了A星算法的所有优点,解决了A中必须沿网格边走的问题,通过允许节点之间的对角线直接相连,从而缩短路径。换句通俗易懂的话就是,他解决了A星算法的各种无意义的弯弯绕绕的路劲,以两点距离最近的直线路劲的启发式寻路算法.这里借助B站某主的视频,给大伙对比下两种算法的不同:
A*与Lazy Theta*路径规划算法对比
四.实现Theta*
接下来我就带大家,在CocosCreator里如何实现Theta*寻路.
1.定义节点类
首先,我们需要定义节点的基本结构,包括它的坐标、父节点、g值(从起点到该节点的代价)、h值(启发式估计从该节点到终点的代价)和f值(g+h,用来评估路径的总代价)。
class Node {
public x: number;
public y: number;
public parent: Node | null;
public g: number;
public h: number;
public f: number;
constructor(x: number, y: number) {
this.x = x;
this.y = y;
this.parent = null;
this.g = Infinity;
this.h = 0;
this.f = Infinity;
}
}
• x 和 y 是节点的坐标。
• parent 表示节点的父节点,用来跟踪路径。
• g 是从起点到该节点的路径代价,初始设为无穷大。
• h 是从该节点到目标节点的启发式估计。
• f = g + h 用来评估节点的优先级。
2.计算启发式函数h值
Theta*通常使用欧几里得距离(直线距离)作为启发式函数来估计节点到目标的代价。
function heuristic(node: Node, endNode: Node): number {
const dx = node.x - endNode.x;
const dy = node.y - endNode.y;
return Math.sqrt(dx * dx + dy * dy);
}
启发函数heuristic计算节点与终点之间的直线距离
3.初始化开放列表和封闭列表
Theta*需要两个列表:
• openList:存储将要处理的节点。
• closedList:存储已经处理过的节点。
const openList: Node[] = [];
const closedList: Set<Node> = new Set();
4.检查直线可见性
Theta*的核心在于检查两个节点之间是否有障碍物。如果两个节点之间没有障碍,则可以直接连接而不需要经过中间节点。
function lineOfSight(grid: number[][], node1: Node, node2: Node): boolean {
const x0 = node1.x;
const y0 = node1.y;
const x1 = node2.x;
const y1 = node2.y;
const dx = Math.abs(x1 - x0);
const dy = Math.abs(y1 - y0);
const sx = (x0 < x1) ? 1 : -1;
const sy = (y0 < y1) ? 1 : -1;
let err = dx - dy;
let x = x0;
let y = y0;
while (x !== x1 || y !== y1) {
if (grid[y][x] === 1) {
return false; // 1 表示障碍物
}
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x += sx;
}
if (e2 < dx) {
err += dx;
y += sy;
}
}
return true;
}
lineOfSight函数使用Bresenham算法检查两个节点之间是否有障碍物。
5.主算法流程
接下来,实现Theta*的主要逻辑。包括从开放列表中选取代价最低的节点、检查直线可见性并更新节点的父节点等操作。
function thetaStar(grid: number[][], startNode: Node, endNode: Node): Node | null {
startNode.g = 0;
startNode.f = heuristic(startNode, endNode);
openList.push(startNode);
while (openList.length > 0) {
// 从 openList 中取出 f 值最小的节点
openList.sort((a, b) => a.f - b.f);
const currentNode = openList.shift() as Node;
if (currentNode.x === endNode.x && currentNode.y === endNode.y) {
return currentNode; // 找到终点,返回路径
}
closedList.add(currentNode);
const neighbors = getNeighbors(grid, currentNode);
for (const neighbor of neighbors) {
if (closedList.has(neighbor)) continue;
if (!openList.includes(neighbor)) {
openList.push(neighbor);
}
let tentativeG = currentNode.g + heuristic(currentNode, neighbor);
// 检查是否可以直接连接父节点与邻居
if (currentNode.parent && lineOfSight(grid, currentNode.parent, neighbor)) {
const parentG = currentNode.parent.g + heuristic(currentNode.parent, neighbor);
if (parentG < tentativeG) {
neighbor.parent = currentNode.parent;
tentativeG = parentG;
}
} else {
neighbor.parent = currentNode;
}
if (tentativeG < neighbor.g) {
neighbor.g = tentativeG;
neighbor.f = neighbor.g + heuristic(neighbor, endNode);
}
}
}
return null; // 找不到路径
}
代码解释:
1. 初始化起点 startNode 的 g 和 f 值,并将其加入 openList。
2. 循环从 openList 中取出 f 值最小的节点进行处理。
3. 对每个邻居节点,首先检查是否已经在 closedList 中,如果在就跳过。
4. 如果邻居节点可以通过父节点的直线相连,使用 lineOfSight 检查,并更新父节点和代价。
5. 如果代价比之前记录的小,更新该节点的 g 和 f 值。
6. 当终点节点被处理时,算法结束。
6.获取邻居节点
function getNeighbors(grid: number[][], node: Node): Node[] {
const neighbors: Node[] = [];
const directions = [
[0, 1], [1, 0], [0, -1], [-1, 0], // 上下左右
[1, 1], [1, -1], [-1, 1], [-1, -1] // 四个对角
];
for (const [dx, dy] of directions) {
const x = node.x + dx;
const y = node.y + dy;
if (x >= 0 && x < grid[0].length && y >= 0 && y < grid.length && grid[y][x] === 0) {
neighbors.push(new Node(x, y));
}
}
return neighbors;
}
getNeighbors函数返回相邻的非障碍物节点,包括上下左右和对角方向。
7.重建路径
当找到终点时,可以通过递归获取路径:
function reconstructPath(endNode: Node): Node[] {
const path: Node[] = [];
let currentNode: Node | null = endNode;
while (currentNode) {
path.push(currentNode);
currentNode = currentNode.parent;
}
return path.reverse(); // 从起点到终点的路径
}
通过这几步,Theta*算法能够有效缩短路径,减少不必要的转弯。
五.总结
Theta*实现起来并不难,只要理解A*的话我相信各位小伙伴都能完全掌握的,我们下期再见吧~