Files
climb/assets/scripts/AStarPathfinding.ts
richarjiang 972334f786 feat(pathfinding): 支持从不可行走位置开始寻路
改进寻路系统,允许玩家从不可行走的当前位置开始寻路到可行走区域。
当玩家位于不可行走位置时,系统会自动寻找最近的可行走位置作为起点,
并临时将起点设置为可行走状态以启动A*算法。

主要变更:
- AStarPathfinding: 临时修改起点可行走状态以支持算法启动
- PlayerController: 检测玩家当前位置并自动传送到最近可行走点
- TiledMapPathfinder: 在寻路前验证起点并寻找替代位置
2025-10-20 09:23:04 +08:00

263 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { _decorator, Component, Vec2 } from 'cc';
const { ccclass, property } = _decorator;
// A*寻路节点
export class PathNode {
x: number;
y: number;
gCost: number = 0; // 从起点到当前点的实际代价
hCost: number = 0; // 从当前点到终点的启发式代价
fCost: number = 0; // 总代价 f = g + h
parent: PathNode | null = null;
walkable: boolean = true;
moveCost: number = 1.0; // 从父节点移动到此节点的代价
constructor(x: number, y: number, walkable: boolean = true) {
this.x = x;
this.y = y;
this.walkable = walkable;
}
calculateFCost() {
this.fCost = this.gCost + this.hCost;
}
}
@ccclass('AStarPathfinding')
export class AStarPathfinding extends Component {
private grid: PathNode[][] = [];
private gridWidth: number = 0;
private gridHeight: number = 0;
/**
* 初始化寻路网格
* @param width 网格宽度
* @param height 网格高度
* @param walkableData 可行走数据0表示不可行走1表示可行走
*/
initializeGrid(width: number, height: number, walkableData: number[][]) {
this.gridWidth = width;
this.gridHeight = height;
this.grid = [];
for (let x = 0; x < width; x++) {
this.grid[x] = [];
for (let y = 0; y < height; y++) {
const walkable = walkableData[y] && walkableData[y][x] === 1;
this.grid[x][y] = new PathNode(x, y, walkable);
}
}
}
/**
* 使用A*算法寻找路径
* @param startX 起点X坐标
* @param startY 起点Y坐标
* @param targetX 终点X坐标
* @param targetY 终点Y坐标
* @returns 路径点数组,如果找不到路径返回空数组
*/
findPath(startX: number, startY: number, targetX: number, targetY: number): Vec2[] {
// 验证起点和终点是否有效
if (!this.isValidPosition(startX, startY) || !this.isValidPosition(targetX, targetY)) {
console.warn('起点或终点坐标无效');
return [];
}
// 检查终点是否可行走
if (!this.grid[targetX][targetY].walkable) {
console.warn('终点不可行走');
return [];
}
// 注意起点可能不可行走我们在TiledMapPathfinder中已经处理了这种情况
// 这里不再检查起点是否可行走,允许从不可行走的起点开始寻路
if (startX === targetX && startY === targetY) {
return [new Vec2(startX, startY)];
}
// 重置所有节点
this.resetNodes();
const startNode = this.grid[startX][startY];
const targetNode = this.grid[targetX][targetY];
// 临时将起点设置为可行走,以便算法能够开始
const originalStartWalkable = startNode.walkable;
startNode.walkable = true;
const openSet: PathNode[] = [];
const closedSet: PathNode[] = [];
openSet.push(startNode);
while (openSet.length > 0) {
// 找到f值最小的节点
let currentNode = openSet[0];
for (let i = 1; i < openSet.length; i++) {
if (openSet[i].fCost < currentNode.fCost ||
(openSet[i].fCost === currentNode.fCost && openSet[i].hCost < currentNode.hCost)) {
currentNode = openSet[i];
}
}
// 从开放列表移除当前节点,添加到关闭列表
openSet.splice(openSet.indexOf(currentNode), 1);
closedSet.push(currentNode);
// 如果到达目标节点,重建路径
if (currentNode === targetNode) {
// 恢复起点的原始可行走状态
startNode.walkable = originalStartWalkable;
return this.retracePath(startNode, targetNode);
}
// 检查相邻节点
const neighbors = this.getNeighbors(currentNode);
for (const neighbor of neighbors) {
// 如果当前节点不可行走,我们仍然允许它连接到可行走的相邻节点
// 这样可以从不可行走的起点移动到可行走的区域
if (!neighbor.walkable || closedSet.indexOf(neighbor) !== -1) {
continue;
}
const newGCost = currentNode.gCost + neighbor.moveCost;
if (newGCost < neighbor.gCost || openSet.indexOf(neighbor) === -1) {
neighbor.gCost = newGCost;
neighbor.hCost = this.getDistance(neighbor, targetNode);
neighbor.calculateFCost();
neighbor.parent = currentNode;
if (openSet.indexOf(neighbor) === -1) {
openSet.push(neighbor);
}
}
}
}
// 恢复起点的原始可行走状态
startNode.walkable = originalStartWalkable;
// 没有找到路径
return [];
}
/**
* 获取节点的相邻节点8方向
*/
private getNeighbors(node: PathNode): PathNode[] {
const neighbors: PathNode[] = [];
// 八个方向:上、下、左、右、左上、右上、左下、右下
const directions = [
{ x: 0, y: 1, cost: 1.0 }, // 上
{ x: 0, y: -1, cost: 1.0 }, // 下
{ x: -1, y: 0, cost: 1.0 }, // 左
{ x: 1, y: 0, cost: 1.0 }, // 右
{ x: -1, y: 1, cost: 1.414 }, // 左上
{ x: 1, y: 1, cost: 1.414 }, // 右上
{ x: -1, y: -1, cost: 1.414 },// 左下
{ x: 1, y: -1, cost: 1.414 } // 右下
];
for (const dir of directions) {
const checkX = node.x + dir.x;
const checkY = node.y + dir.y;
if (this.isValidPosition(checkX, checkY)) {
const neighbor = this.grid[checkX][checkY];
// 为对角线移动添加额外检查,防止穿过墙角
if (dir.cost > 1.0) {
// 检查对角线移动时,相邻的两个直角方向是否可行走
const xCheck = this.grid[node.x + dir.x][node.y];
const yCheck = this.grid[node.x][node.y + dir.y];
if (!xCheck.walkable || !yCheck.walkable) {
continue; // 如果相邻的直角方向不可行走,则不能进行对角线移动
}
}
neighbor.moveCost = dir.cost;
neighbors.push(neighbor);
}
}
return neighbors;
}
/**
* 获取两个节点之间的距离(欧几里得距离,支持对角线移动)
*/
private getDistance(nodeA: PathNode, nodeB: PathNode): number {
const distX = Math.abs(nodeA.x - nodeB.x);
const distY = Math.abs(nodeA.y - nodeB.y);
// 使用欧几里得距离,更适合对角线移动
// 对角线距离约为1.414直线距离为1
if (distX === distY) {
return distX * 1.414; // 对角线移动
} else {
return distX + distY; // 曼哈顿距离
}
}
/**
* 重建路径
*/
private retracePath(startNode: PathNode, endNode: PathNode): Vec2[] {
const path: Vec2[] = [];
let currentNode = endNode;
while (currentNode !== startNode) {
path.push(new Vec2(currentNode.x, currentNode.y));
currentNode = currentNode.parent!;
}
path.push(new Vec2(startNode.x, startNode.y));
path.reverse();
return path;
}
/**
* 重置所有节点的寻路参数
*/
private resetNodes() {
for (let x = 0; x < this.gridWidth; x++) {
for (let y = 0; y < this.gridHeight; y++) {
const node = this.grid[x][y];
node.gCost = 0;
node.hCost = 0;
node.fCost = 0;
node.parent = null;
}
}
}
/**
* 检查位置是否有效
*/
private isValidPosition(x: number, y: number): boolean {
return x >= 0 && x < this.gridWidth && y >= 0 && y < this.gridHeight;
}
/**
* 设置节点的可行走状态
*/
setWalkable(x: number, y: number, walkable: boolean) {
if (this.isValidPosition(x, y)) {
this.grid[x][y].walkable = walkable;
}
}
/**
* 获取节点是否可行走
*/
isWalkable(x: number, y: number): boolean {
if (!this.isValidPosition(x, y)) {
return false;
}
return this.grid[x][y].walkable;
}
}