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

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

372 lines
11 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, Node, TiledMap, TiledLayer, Vec2, Vec3, Size } from 'cc';
import { AStarPathfinding } from './AStarPathfinding';
const { ccclass, property } = _decorator;
@ccclass('TiledMapPathfinder')
export class TiledMapPathfinder extends Component {
@property(TiledMap)
tiledMap: TiledMap | null = null;
@property({ displayName: '可行走图层名称' })
walkableLayerName: string = 'WalkableLayer';
@property({ displayName: '瓦片尺寸' })
tileSize: number = 32;
private pathfinding: AStarPathfinding | null = null;
private mapSize: Size = new Size(0, 0);
private walkableData: number[][] = [];
onLoad() {
// 获取或创建寻路组件
this.pathfinding = this.getComponent(AStarPathfinding);
if (!this.pathfinding) {
this.pathfinding = this.addComponent(AStarPathfinding);
}
}
start() {
if (this.tiledMap) {
this.initializePathfinding();
} else {
console.error('TiledMapPathfinder: TiledMap组件未设置');
}
}
/**
* 初始化寻路系统
*/
private initializePathfinding() {
if (!this.tiledMap) {
console.error('TiledMap未设置');
return;
}
// 获取地图尺寸
this.mapSize = this.tiledMap.getMapSize();
console.log(`地图尺寸: ${this.mapSize.width}x${this.mapSize.height}`);
// 获取可行走图层
const walkableLayer = this.tiledMap.getLayer(this.walkableLayerName);
if (!walkableLayer) {
console.error(`找不到图层: ${this.walkableLayerName}`);
return;
}
// 读取可行走数据
this.extractWalkableData(walkableLayer);
// 初始化A*寻路算法
if (this.pathfinding) {
this.pathfinding.initializeGrid(this.mapSize.width, this.mapSize.height, this.walkableData);
console.log('寻路系统初始化完成');
}
}
/**
* 从TiledLayer提取可行走数据
*/
private extractWalkableData(layer: TiledLayer) {
this.walkableData = [];
for (let y = 0; y < this.mapSize.height; y++) {
this.walkableData[y] = [];
for (let x = 0; x < this.mapSize.width; x++) {
// 获取指定位置的瓦片GID
const gid = layer.getTileGIDAt(x, y);
// GID > 0 表示有瓦片,表示可行走
// GID = 0 表示没有瓦片,表示不可行走
this.walkableData[y][x] = gid > 0 ? 1 : 0;
}
}
console.log('可行走数据提取完成');
this.debugPrintWalkableData();
}
/**
* 调试打印可行走数据(仅打印部分数据以避免日志过长)
*/
private debugPrintWalkableData() {
console.log('可行走数据示例前10行:');
for (let y = 0; y < Math.min(10, this.walkableData.length); y++) {
const row = this.walkableData[y].slice(0, Math.min(10, this.walkableData[y].length));
console.log(`${y}行: [${row.join(', ')}]`);
}
}
/**
* 寻找路径
* @param startWorldPos 起点世界坐标
* @param targetWorldPos 终点世界坐标
* @returns 路径的世界坐标数组
*/
findPath(startWorldPos: Vec3, targetWorldPos: Vec3): Vec3[] {
if (!this.pathfinding || !this.tiledMap) {
console.warn('寻路系统未初始化');
return [];
}
// 将世界坐标转换为瓦片坐标
let startTilePos = this.worldToTileCoordinate(startWorldPos);
const targetTilePos = this.worldToTileCoordinate(targetWorldPos);
console.log(`寻路: 起点瓦片坐标(${startTilePos.x}, ${startTilePos.y}) -> 终点瓦片坐标(${targetTilePos.x}, ${targetTilePos.y})`);
// 检查起点是否可行走,如果不可行走则寻找最近的可行走位置
if (!this.pathfinding.isWalkable(startTilePos.x, startTilePos.y)) {
console.log('起点不可行走,寻找最近的可行走位置');
const closestWalkableWorldPos = this.getClosestWalkablePosition(startWorldPos);
if (!closestWalkableWorldPos) {
console.warn('找不到起点附近的可行走位置');
return [];
}
startTilePos = this.worldToTileCoordinate(closestWalkableWorldPos);
console.log(`使用新的起点瓦片坐标(${startTilePos.x}, ${startTilePos.y})`);
}
// 使用A*算法寻找路径
const tilePath = this.pathfinding.findPath(
startTilePos.x, startTilePos.y,
targetTilePos.x, targetTilePos.y
);
if (tilePath.length === 0) {
console.warn('未找到路径');
return [];
}
// 将瓦片坐标路径转换为世界坐标路径
const worldPath: Vec3[] = [];
for (const tilePos of tilePath) {
const worldPos = this.tileToWorldCoordinate(tilePos);
worldPath.push(worldPos);
}
// 应用路径平滑算法
const smoothedPath = this.smoothPath(worldPath);
console.log(`找到路径,原始${worldPath.length}个点,平滑后${smoothedPath.length}个点`);
return smoothedPath;
}
/**
* 世界坐标转换为瓦片坐标
*/
private worldToTileCoordinate(worldPos: Vec3): Vec2 {
if (!this.tiledMap) {
return new Vec2(0, 0);
}
// 获取地图节点的位置偏移
const mapNode = this.tiledMap.node;
const mapPosition = mapNode.position;
// 计算相对于地图的坐标
const relativeX = worldPos.x - mapPosition.x;
const relativeY = worldPos.y - mapPosition.y;
// 获取地图尺寸信息
const mapSize = this.tiledMap.getMapSize();
const tileSize = this.tiledMap.getTileSize();
// 计算瓦片坐标
// 注意Cocos Creator的Y轴向上为正但瓦片地图的Y轴向下为正
const tileX = Math.floor((relativeX + mapSize.width * tileSize.width * 0.5) / tileSize.width);
const tileY = Math.floor((mapSize.height * tileSize.height * 0.5 - relativeY) / tileSize.height);
// 确保坐标在地图范围内
const clampedX = Math.max(0, Math.min(mapSize.width - 1, tileX));
const clampedY = Math.max(0, Math.min(mapSize.height - 1, tileY));
return new Vec2(clampedX, clampedY);
}
/**
* 瓦片坐标转换为世界坐标
*/
private tileToWorldCoordinate(tilePos: Vec2): Vec3 {
if (!this.tiledMap) {
return new Vec3(0, 0, 0);
}
// 获取地图节点的位置偏移
const mapNode = this.tiledMap.node;
const mapPosition = mapNode.position;
// 获取地图尺寸信息
const mapSize = this.tiledMap.getMapSize();
const tileSize = this.tiledMap.getTileSize();
// 计算世界坐标(瓦片中心点)
const worldX = (tilePos.x + 0.5) * tileSize.width - mapSize.width * tileSize.width * 0.5 + mapPosition.x;
const worldY = mapSize.height * tileSize.height * 0.5 - (tilePos.y + 0.5) * tileSize.height + mapPosition.y;
return new Vec3(worldX, worldY, 0);
}
/**
* 检查指定世界坐标是否可行走
*/
isWorldPositionWalkable(worldPos: Vec3): boolean {
if (!this.pathfinding) {
return false;
}
const tilePos = this.worldToTileCoordinate(worldPos);
return this.pathfinding.isWalkable(tilePos.x, tilePos.y);
}
/**
* 获取最近的可行走位置
*/
getClosestWalkablePosition(worldPos: Vec3, maxSearchRadius: number = 5): Vec3 | null {
const centerTilePos = this.worldToTileCoordinate(worldPos);
// 螺旋搜索最近的可行走位置
for (let radius = 0; radius <= maxSearchRadius; radius++) {
for (let x = -radius; x <= radius; x++) {
for (let y = -radius; y <= radius; y++) {
// 只检查当前半径边界上的点
if (Math.abs(x) !== radius && Math.abs(y) !== radius && radius > 0) {
continue;
}
const checkX = centerTilePos.x + x;
const checkY = centerTilePos.y + y;
if (this.pathfinding && this.pathfinding.isWalkable(checkX, checkY)) {
return this.tileToWorldCoordinate(new Vec2(checkX, checkY));
}
}
}
}
return null;
}
/**
* 获取地图信息
*/
getMapInfo() {
if (!this.tiledMap) {
return null;
}
return {
mapSize: this.tiledMap.getMapSize(),
tileSize: this.tiledMap.getTileSize(),
orientation: this.tiledMap.getMapOrientation()
};
}
/**
* 路径平滑算法,减少不必要的转折点
* 使用视线检查算法,移除可以直接到达的中间点
*/
private smoothPath(path: Vec3[]): Vec3[] {
if (path.length <= 2) {
return path; // 路径太短,不需要平滑
}
const smoothedPath: Vec3[] = [];
smoothedPath.push(path[0]); // 添加起点
let currentIndex = 0;
let targetIndex = 1;
while (targetIndex < path.length - 1) {
// 尝试找到从当前点可以直接到达的最远点
let farthestReachable = targetIndex;
for (let i = targetIndex + 1; i < path.length; i++) {
if (this.hasLineOfSight(path[currentIndex], path[i])) {
farthestReachable = i;
} else {
break; // 一旦发现不可达的点,停止搜索
}
}
// 如果找到了更远的可达点,跳过中间点
if (farthestReachable > targetIndex) {
currentIndex = farthestReachable;
smoothedPath.push(path[currentIndex]);
targetIndex = currentIndex + 1;
} else {
// 否则,添加当前目标点
smoothedPath.push(path[targetIndex]);
currentIndex = targetIndex;
targetIndex++;
}
}
// 确保终点被添加
if (smoothedPath[smoothedPath.length - 1] !== path[path.length - 1]) {
smoothedPath.push(path[path.length - 1]);
}
return smoothedPath;
}
/**
* 检查两点之间是否有直接的视线(是否可以直线移动)
*/
private hasLineOfSight(start: Vec3, end: Vec3): boolean {
if (!this.pathfinding) {
return false;
}
// 将世界坐标转换为瓦片坐标
const startTile = this.worldToTileCoordinate(start);
const endTile = this.worldToTileCoordinate(end);
// 使用Bresenham直线算法检查路径上的所有点
const points = this.getLinePoints(startTile.x, startTile.y, endTile.x, endTile.y);
for (const point of points) {
if (!this.pathfinding.isWalkable(point.x, point.y)) {
return false; // 路径上有障碍物
}
}
return true; // 路径上没有障碍物
}
/**
* 使用Bresenham算法获取两点之间的所有点
*/
private getLinePoints(x0: number, y0: number, x1: number, y1: number): Vec2[] {
const points: Vec2[] = [];
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 (true) {
points.push(new Vec2(x, y));
if (x === x1 && y === y1) {
break;
}
const e2 = 2 * err;
if (e2 > -dy) {
err -= dy;
x += sx;
}
if (e2 < dx) {
err += dx;
y += sy;
}
}
return points;
}
}