feat: 寻路优化

This commit is contained in:
richarjiang
2025-10-10 14:40:28 +08:00
parent dbdec71d0d
commit 455cca40b0
5 changed files with 743 additions and 523 deletions

View File

@@ -1472,7 +1472,7 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 2,
"_density": 1,
"_sensor": false,
"_friction": 0.2,
@@ -1912,8 +1912,6 @@
"moveSpeed": 300,
"mapWidth": 1080,
"mapHeight": 1920,
"attackPreferredDistance": 220,
"attackDistanceTolerance": 10,
"_id": "c1AuAU3IlKnLOzgk9vsBr4"
},
{
@@ -3696,7 +3694,7 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 4,
"_density": 1,
"_sensor": false,
"_friction": 0.2,
@@ -3896,7 +3894,7 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 4.172,
"x": -4.172,
"y": 1.768,
"z": 2.116
},
@@ -4330,20 +4328,20 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 4,
"_density": 1,
"_sensor": false,
"_friction": 0.2,
"_restitution": 0,
"_offset": {
"__type__": "cc.Vec2",
"x": -16.1,
"y": 7.4
"x": -11.1,
"y": -0.2
},
"_size": {
"__type__": "cc.Size",
"width": 97.4,
"height": 121.5
"width": 86.2,
"height": 110.3
},
"_id": "3ca3euVMhBkrOMELnKR9Zg"
},
@@ -4964,20 +4962,20 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 4,
"_density": 1,
"_sensor": false,
"_friction": 0.2,
"_restitution": 0,
"_offset": {
"__type__": "cc.Vec2",
"x": 2.3,
"y": -0.3
"x": 1.7,
"y": -3.3
},
"_size": {
"__type__": "cc.Size",
"width": 74.8,
"height": 132
"width": 45.7,
"height": 119.5
},
"_id": "efpC9N9EpG+r7WSMK95lgW"
},
@@ -5371,20 +5369,20 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 4,
"_density": 1,
"_sensor": false,
"_friction": 0.2,
"_restitution": 0,
"_offset": {
"__type__": "cc.Vec2",
"x": 1.3,
"y": 7.5
"x": 6,
"y": 3.2
},
"_size": {
"__type__": "cc.Size",
"width": 88.9,
"height": 96.6
"width": 73.1,
"height": 65.6
},
"_id": "3dz7NavQFBMZ60R0LoZHEP"
},
@@ -5778,7 +5776,7 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 4,
"_density": 1,
"_sensor": false,
"_friction": 0.2,
@@ -6412,7 +6410,7 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 4,
"_density": 1,
"_sensor": false,
"_friction": 0.2,
@@ -6612,7 +6610,7 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 3.256,
"x": -3.256,
"y": 1.763,
"z": 1.763
},
@@ -7046,7 +7044,7 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 4,
"_density": 1,
"_sensor": false,
"_friction": 0.2,
@@ -7246,7 +7244,7 @@
},
"_lscale": {
"__type__": "cc.Vec3",
"x": 1.36,
"x": 1.86,
"y": 0.822,
"z": 1.154
},
@@ -7680,7 +7678,7 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 4,
"_density": 1,
"_sensor": false,
"_friction": 0.2,
@@ -8314,7 +8312,7 @@
"_enabled": true,
"__prefab": null,
"tag": 0,
"_group": 1,
"_group": 4,
"_density": 1,
"_sensor": false,
"_friction": 0.2,

View File

@@ -10,6 +10,7 @@ export class PathNode {
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;
@@ -110,7 +111,7 @@ export class AStarPathfinding extends Component {
continue;
}
const newGCost = currentNode.gCost + this.getDistance(currentNode, neighbor);
const newGCost = currentNode.gCost + neighbor.moveCost;
if (newGCost < neighbor.gCost || openSet.indexOf(neighbor) === -1) {
neighbor.gCost = newGCost;
@@ -130,17 +131,21 @@ export class AStarPathfinding extends Component {
}
/**
* 获取节点的相邻节点(4方向)
* 获取节点的相邻节点(8方向)
*/
private getNeighbors(node: PathNode): PathNode[] {
const neighbors: PathNode[] = [];
// 个方向:上、下、左、右
// 个方向:上、下、左、右、左上、右上、左下、右下
const directions = [
{ x: 0, y: 1 }, // 上
{ x: 0, y: -1 }, // 下
{ x: -1, y: 0 }, // 左
{ x: 1, y: 0 } // 右
{ 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) {
@@ -148,7 +153,18 @@ export class AStarPathfinding extends Component {
const checkY = node.y + dir.y;
if (this.isValidPosition(checkX, checkY)) {
neighbors.push(this.grid[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);
}
}
@@ -156,12 +172,19 @@ export class AStarPathfinding extends Component {
}
/**
* 获取两个节点之间的距离(曼哈顿距离
* 获取两个节点之间的距离(欧几里得距离,支持对角线移动
*/
private getDistance(nodeA: PathNode, nodeB: PathNode): number {
const distX = Math.abs(nodeA.x - nodeB.x);
const distY = Math.abs(nodeA.y - nodeB.y);
return distX + distY;
// 使用欧几里得距离,更适合对角线移动
// 对角线距离约为1.414直线距离为1
if (distX === distY) {
return distX * 1.414; // 对角线移动
} else {
return distX + distY; // 曼哈顿距离
}
}
/**

View File

@@ -41,12 +41,6 @@ export class PlayerController extends Component {
@property
mapHeight: number = 2560; // 地图高度
@property({ tooltip: '玩家与怪物进入战斗时的理想距离' })
attackPreferredDistance: number = 10;
@property({ tooltip: '允许的距离误差范围,超出后会进行位置调整' })
attackDistanceTolerance: number = 10;
private isMoving: boolean = false;
private isAttacking: boolean = false;
private currentPath: Vec3[] = [];
@@ -59,10 +53,11 @@ export class PlayerController extends Component {
private isWin: boolean = false; // 游戏是否胜利(到达终点)
private currentDirection: number = 5; // 当前玩家朝向3表示左/上5表示右/下默认为5
private hasWinTimes = 0
// 平滑移动相关变量
private moveTween: any = null; // 当前移动的tween对象
private lastPosition: Vec3 = new Vec3(); // 上一帧位置
private readonly _tempVec3A: Vec3 = new Vec3();
private readonly _tempVec3B: Vec3 = new Vec3();
private hasWinTimes = 0
// 道具列表
private props: Node[] = [];
@@ -225,7 +220,7 @@ export class PlayerController extends Component {
if (!this.player || !this.pathfinder) return;
// 停止当前移动
this.stopMovement();
// this.stopMovement();
// 限制目标位置在地图边界内
const clampedPos = this.clampPlayerPosition(worldPos);
@@ -262,7 +257,8 @@ export class PlayerController extends Component {
// 切换到对应的动画
this.switchAnimation(animationName);
this.moveToNextWaypoint();
// 使用平滑路径移动
this.startSmoothPathMovement();
}
// 限制玩家位置在地图边界内
@@ -374,12 +370,15 @@ export class PlayerController extends Component {
return;
}
// 停止当前的移动tween
if (this.moveTween) {
this.moveTween.stop();
this.moveTween = null;
}
const targetPos = this.currentPath[this.currentPathIndex];
const currentPos = this.player.position;
// 计算移动距离和时间
const distance = Vec3.distance(currentPos, targetPos);
const moveTime = distance / this.moveSpeed;
@@ -388,10 +387,16 @@ export class PlayerController extends Component {
// 记录目标位置用于方向判断
this.lastTargetPosition.set(targetPos);
this.lastPosition.set(currentPos);
// 使用缓动移动到目标位置
tween(this.player)
this.moveTween = tween(this.player)
.to(moveTime, { position: targetPos }, {
easing: 'linear', // 使用线性插值,保持匀速移动
onUpdate: (target: Node) => {
// 在移动过程中更新动画方向
this.updateMovementDirection(target.position);
},
onComplete: () => {
this.currentPathIndex++;
this.moveToNextWaypoint();
@@ -400,10 +405,153 @@ export class PlayerController extends Component {
.start();
}
/**
* 开始平滑路径移动
*/
private startSmoothPathMovement() {
if (!this.player || this.currentPath.length === 0) {
this.isMoving = false;
this.switchAnimation('stand');
return;
}
// 停止当前的移动tween
if (this.moveTween) {
this.moveTween.stop();
this.moveTween = null;
}
// 计算总路径长度
let totalDistance = 0;
for (let i = 0; i < this.currentPath.length - 1; i++) {
totalDistance += Vec3.distance(this.currentPath[i], this.currentPath[i + 1]);
}
// 计算总移动时间
const totalTime = totalDistance / this.moveSpeed;
console.log(`开始平滑路径移动,总距离: ${totalDistance.toFixed(2)}, 总时间: ${totalTime.toFixed(2)}`);
// 创建连续的路径移动
this.moveTween = tween(this.player)
.to(totalTime, { position: this.currentPath[this.currentPath.length - 1] }, {
easing: 'linear',
onUpdate: (target: Node, ratio: number) => {
// 根据进度计算当前位置
const currentPos = this.getPositionOnPath(ratio);
if (currentPos) {
target.position = currentPos;
this.updateMovementDirection(currentPos);
}
},
onComplete: () => {
this.isMoving = false;
this.switchAnimation('stand');
console.log('平滑路径移动完成');
}
})
.start();
}
/**
* 根据路径进度获取当前位置
*/
private getPositionOnPath(ratio: number): Vec3 | null {
if (!this.player || this.currentPath.length === 0) {
return null;
}
// 计算总路径长度
const segmentLengths: number[] = [];
let totalLength = 0;
for (let i = 0; i < this.currentPath.length - 1; i++) {
const length = Vec3.distance(this.currentPath[i], this.currentPath[i + 1]);
segmentLengths.push(length);
totalLength += length;
}
// 计算目标距离
const targetDistance = totalLength * ratio;
// 找到对应的路径段
let currentDistance = 0;
for (let i = 0; i < segmentLengths.length; i++) {
if (currentDistance + segmentLengths[i] >= targetDistance) {
// 在当前段内
const segmentRatio = (targetDistance - currentDistance) / segmentLengths[i];
const startPos = this.currentPath[i];
const endPos = this.currentPath[i + 1];
// 线性插值计算当前位置
return new Vec3(
startPos.x + (endPos.x - startPos.x) * segmentRatio,
startPos.y + (endPos.y - startPos.y) * segmentRatio,
startPos.z + (endPos.z - startPos.z) * segmentRatio
);
}
currentDistance += segmentLengths[i];
}
// 如果超出范围,返回终点
return this.currentPath[this.currentPath.length - 1];
}
/**
* 在移动过程中更新动画方向
*/
private updateMovementDirection(currentPos: Vec3) {
if (!this.player || this.currentPath.length === 0) {
return;
}
// 计算移动方向(基于下一路径点)
let targetPos: Vec3;
if (this.currentPathIndex < this.currentPath.length - 1) {
targetPos = this.currentPath[this.currentPathIndex + 1];
} else {
targetPos = this.currentPath[this.currentPath.length - 1];
}
const deltaX = targetPos.x - currentPos.x;
const deltaY = targetPos.y - currentPos.y;
// 如果移动距离很小,不更新动画
const distance = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
if (distance < 1) {
return;
}
// 计算主要移动方向
const absX = Math.abs(deltaX);
const absY = Math.abs(deltaY);
// 根据移动方向选择动画
let animationName = 'walk';
if (absX > absY) {
// 水平移动为主
this.currentDirection = deltaX < 0 ? 3 : 5;
animationName = deltaX < 0 ? 'walk3' : 'walk5';
} else {
// 垂直移动为主
this.currentDirection = deltaY < 0 ? 3 : 5;
animationName = deltaY < 0 ? 'walk3' : 'walk5';
}
// 切换到对应的动画
this.switchAnimation(animationName);
}
/**
* 停止当前移动
*/
private stopMovement() {
// 停止当前的移动tween
if (this.moveTween) {
this.moveTween.stop();
this.moveTween = null;
}
if (this.player) {
tween(this.player).stop();
}
@@ -447,12 +595,6 @@ export class PlayerController extends Component {
this.stopMovement();
// this.scheduleOnce(() => {
// this.adjustPositionsForAttack(otherCollider.node);
// }, 0);
// 获取玩家和怪物的生命值
const playerHpLabel = this.player.getChildByName('hp');
const monsterHpLabel = otherCollider.node.getChildByName('hp');
@@ -643,76 +785,6 @@ export class PlayerController extends Component {
}
}
private adjustPositionsForAttack(monsterNode: Node) {
if (!this.player || !monsterNode) {
return;
}
const desiredDistance = Math.max(0, this.attackPreferredDistance);
const tolerance = Math.max(0, this.attackDistanceTolerance);
if (desiredDistance === 0) {
return;
}
const playerWorldPos = this._tempVec3A;
const monsterWorldPos = this._tempVec3B;
this.player.getWorldPosition(playerWorldPos);
monsterNode.getWorldPosition(monsterWorldPos);
const playerTarget = playerWorldPos.clone();
const monsterTarget = monsterWorldPos.clone();
const deltaX = playerWorldPos.x - monsterWorldPos.x;
const horizontalDistance = Math.abs(deltaX);
const verticalDifference = Math.abs(playerWorldPos.y - monsterWorldPos.y);
const targetY = (playerWorldPos.y + monsterWorldPos.y) * 0.5;
let adjusted = false;
if (verticalDifference > 1e-3) {
playerTarget.y = targetY;
monsterTarget.y = targetY;
adjusted = true;
}
const orderCorrect = playerWorldPos.x > monsterWorldPos.x;
const distanceOutOfRange = horizontalDistance < desiredDistance - tolerance || horizontalDistance > desiredDistance + tolerance;
if (!orderCorrect || distanceOutOfRange) {
const midX = (playerWorldPos.x + monsterWorldPos.x) * 0.5;
const halfDistance = desiredDistance * 0.5;
playerTarget.x = midX + halfDistance;
monsterTarget.x = midX - halfDistance;
adjusted = true;
}
if (!adjusted) {
return;
}
// 确保最终目标仍然满足玩家在右侧的要求
if (playerTarget.x <= monsterTarget.x) {
const swapMidX = (playerTarget.x + monsterTarget.x) * 0.5;
const halfDistance = desiredDistance * 0.5;
playerTarget.x = swapMidX + halfDistance;
monsterTarget.x = swapMidX - halfDistance;
}
const clampedPlayer = this.clampPositionWithinMap(playerTarget);
const clampedMonster = this.clampPositionWithinMap(monsterTarget);
this.setNodeWorldPosition(this.player, clampedPlayer);
this.setNodeWorldPosition(monsterNode, clampedMonster);
this.player.getWorldPosition(playerWorldPos);
monsterNode.getWorldPosition(monsterWorldPos);
const finalHorizontalDistance = Math.abs(playerWorldPos.x - monsterWorldPos.x);
const finalVerticalOffset = Math.abs(playerWorldPos.y - monsterWorldPos.y);
console.log(`战斗位置已调整,水平距离: ${finalHorizontalDistance.toFixed(2)}, 垂直误差: ${finalVerticalOffset.toFixed(2)}`);
if (playerWorldPos.x >= monsterWorldPos.x) {
this.currentDirection = 3;
}
}
private setNodeWorldPosition(node: Node, worldPos: Vec3) {
const parent = node.parent;

View File

@@ -134,8 +134,11 @@ export class TiledMapPathfinder extends Component {
worldPath.push(worldPos);
}
console.log(`找到路径,包含${worldPath.length}个点`);
return worldPath;
// 应用路径平滑算法
const smoothedPath = this.smoothPath(worldPath);
console.log(`找到路径,原始${worldPath.length}个点,平滑后${smoothedPath.length}个点`);
return smoothedPath;
}
/**
@@ -247,4 +250,111 @@ export class TiledMapPathfinder extends Component {
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;
}
}

View File

@@ -5,5 +5,22 @@
"width": 720,
"height": 1334
}
},
"physics": {
"collisionGroups": [
{
"index": 1,
"name": "player"
},
{
"index": 2,
"name": "npc"
}
],
"collisionMatrix": {
"0": 1,
"1": 4,
"2": 2
}
}
}