feat: 寻路优化
This commit is contained in:
@@ -6,245 +6,355 @@ const { ccclass, property } = _decorator;
|
||||
@ccclass('TiledMapPathfinder')
|
||||
export class TiledMapPathfinder extends Component {
|
||||
|
||||
@property(TiledMap)
|
||||
tiledMap: TiledMap | null = null;
|
||||
@property(TiledMap)
|
||||
tiledMap: TiledMap | null = null;
|
||||
|
||||
@property({ displayName: '可行走图层名称' })
|
||||
walkableLayerName: string = 'WalkableLayer';
|
||||
@property({ displayName: '可行走图层名称' })
|
||||
walkableLayerName: string = 'WalkableLayer';
|
||||
|
||||
@property({ displayName: '瓦片尺寸' })
|
||||
tileSize: number = 32;
|
||||
@property({ displayName: '瓦片尺寸' })
|
||||
tileSize: number = 32;
|
||||
|
||||
private pathfinding: AStarPathfinding | null = null;
|
||||
private mapSize: Size = new Size(0, 0);
|
||||
private walkableData: number[][] = [];
|
||||
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);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.tiledMap) {
|
||||
this.initializePathfinding();
|
||||
// 获取地图尺寸
|
||||
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 [];
|
||||
}
|
||||
|
||||
// 将世界坐标转换为瓦片坐标
|
||||
const startTilePos = this.worldToTileCoordinate(startWorldPos);
|
||||
const targetTilePos = this.worldToTileCoordinate(targetWorldPos);
|
||||
|
||||
console.log(`寻路: 起点瓦片坐标(${startTilePos.x}, ${startTilePos.y}) -> 终点瓦片坐标(${targetTilePos.x}, ${targetTilePos.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 {
|
||||
console.error('TiledMapPathfinder: TiledMap组件未设置');
|
||||
break; // 一旦发现不可达的点,停止搜索
|
||||
}
|
||||
}
|
||||
|
||||
// 如果找到了更远的可达点,跳过中间点
|
||||
if (farthestReachable > targetIndex) {
|
||||
currentIndex = farthestReachable;
|
||||
smoothedPath.push(path[currentIndex]);
|
||||
targetIndex = currentIndex + 1;
|
||||
} else {
|
||||
// 否则,添加当前目标点
|
||||
smoothedPath.push(path[targetIndex]);
|
||||
currentIndex = targetIndex;
|
||||
targetIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化寻路系统
|
||||
*/
|
||||
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('寻路系统初始化完成');
|
||||
}
|
||||
// 确保终点被添加
|
||||
if (smoothedPath[smoothedPath.length - 1] !== path[path.length - 1]) {
|
||||
smoothedPath.push(path[path.length - 1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从TiledLayer提取可行走数据
|
||||
*/
|
||||
private extractWalkableData(layer: TiledLayer) {
|
||||
this.walkableData = [];
|
||||
return smoothedPath;
|
||||
}
|
||||
|
||||
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 hasLineOfSight(start: Vec3, end: Vec3): boolean {
|
||||
if (!this.pathfinding) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试打印可行走数据(仅打印部分数据以避免日志过长)
|
||||
*/
|
||||
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(', ')}]`);
|
||||
}
|
||||
// 将世界坐标转换为瓦片坐标
|
||||
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; // 路径上有障碍物
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 寻找路径
|
||||
* @param startWorldPos 起点世界坐标
|
||||
* @param targetWorldPos 终点世界坐标
|
||||
* @returns 路径的世界坐标数组
|
||||
*/
|
||||
findPath(startWorldPos: Vec3, targetWorldPos: Vec3): Vec3[] {
|
||||
if (!this.pathfinding || !this.tiledMap) {
|
||||
console.warn('寻路系统未初始化');
|
||||
return [];
|
||||
}
|
||||
return true; // 路径上没有障碍物
|
||||
}
|
||||
|
||||
// 将世界坐标转换为瓦片坐标
|
||||
const startTilePos = this.worldToTileCoordinate(startWorldPos);
|
||||
const targetTilePos = this.worldToTileCoordinate(targetWorldPos);
|
||||
/**
|
||||
* 使用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;
|
||||
|
||||
console.log(`寻路: 起点瓦片坐标(${startTilePos.x}, ${startTilePos.y}) -> 终点瓦片坐标(${targetTilePos.x}, ${targetTilePos.y})`);
|
||||
let x = x0;
|
||||
let y = y0;
|
||||
|
||||
// 使用A*算法寻找路径
|
||||
const tilePath = this.pathfinding.findPath(
|
||||
startTilePos.x, startTilePos.y,
|
||||
targetTilePos.x, targetTilePos.y
|
||||
);
|
||||
while (true) {
|
||||
points.push(new Vec2(x, y));
|
||||
|
||||
if (tilePath.length === 0) {
|
||||
console.warn('未找到路径');
|
||||
return [];
|
||||
}
|
||||
if (x === x1 && y === y1) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 将瓦片坐标路径转换为世界坐标路径
|
||||
const worldPath: Vec3[] = [];
|
||||
for (const tilePos of tilePath) {
|
||||
const worldPos = this.tileToWorldCoordinate(tilePos);
|
||||
worldPath.push(worldPos);
|
||||
}
|
||||
|
||||
console.log(`找到路径,包含${worldPath.length}个点`);
|
||||
return worldPath;
|
||||
const e2 = 2 * err;
|
||||
if (e2 > -dy) {
|
||||
err -= dy;
|
||||
x += sx;
|
||||
}
|
||||
if (e2 < dx) {
|
||||
err += dx;
|
||||
y += sy;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 世界坐标转换为瓦片坐标
|
||||
*/
|
||||
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()
|
||||
};
|
||||
}
|
||||
return points;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user