From 455cca40b058f9d5874c7ce1777ff46cf1c6169d Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 10 Oct 2025 14:40:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AF=BB=E8=B7=AF=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/scenes/main.scene | 52 ++- assets/scripts/AStarPathfinding.ts | 401 ++++++++++---------- assets/scripts/PlayerController.ts | 252 ++++++++----- assets/scripts/TiledMapPathfinder.ts | 544 ++++++++++++++++----------- settings/v2/packages/project.json | 17 + 5 files changed, 743 insertions(+), 523 deletions(-) diff --git a/assets/scenes/main.scene b/assets/scenes/main.scene index fd68805..8a0699a 100644 --- a/assets/scenes/main.scene +++ b/assets/scenes/main.scene @@ -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, diff --git a/assets/scripts/AStarPathfinding.ts b/assets/scripts/AStarPathfinding.ts index ef64c7f..fd79a05 100644 --- a/assets/scripts/AStarPathfinding.ts +++ b/assets/scripts/AStarPathfinding.ts @@ -3,223 +3,246 @@ 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; + 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; - } + constructor(x: number, y: number, walkable: boolean = true) { + this.x = x; + this.y = y; + this.walkable = walkable; + } - calculateFCost() { - this.fCost = this.gCost + this.hCost; - } + 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; + 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 = []; + /** + * 初始化寻路网格 + * @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); - } - } + 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 []; } - /** - * 使用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[startX][startY].walkable || !this.grid[targetX][targetY].walkable) { - console.warn('起点或终点不可行走'); - return []; - } - - 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 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) { - 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 + this.getDistance(currentNode, neighbor); - - 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); - } - } - } - } - - // 没有找到路径 - return []; + if (!this.grid[startX][startY].walkable || !this.grid[targetX][targetY].walkable) { + console.warn('起点或终点不可行走'); + return []; } - /** - * 获取节点的相邻节点(4方向) - */ - private getNeighbors(node: PathNode): PathNode[] { - const neighbors: PathNode[] = []; + if (startX === targetX && startY === targetY) { + return [new Vec2(startX, startY)]; + } - // 四个方向:上、下、左、右 - const directions = [ - { x: 0, y: 1 }, // 上 - { x: 0, y: -1 }, // 下 - { x: -1, y: 0 }, // 左 - { x: 1, y: 0 } // 右 - ]; + // 重置所有节点 + this.resetNodes(); - for (const dir of directions) { - const checkX = node.x + dir.x; - const checkY = node.y + dir.y; + const startNode = this.grid[startX][startY]; + const targetNode = this.grid[targetX][targetY]; - if (this.isValidPosition(checkX, checkY)) { - neighbors.push(this.grid[checkX][checkY]); - } + 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) { + return this.retracePath(startNode, targetNode); + } + + // 检查相邻节点 + const neighbors = this.getNeighbors(currentNode); + for (const neighbor of neighbors) { + if (!neighbor.walkable || closedSet.indexOf(neighbor) !== -1) { + continue; } - return neighbors; - } + const newGCost = currentNode.gCost + neighbor.moveCost; - /** - * 获取两个节点之间的距离(曼哈顿距离) - */ - 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; - } + if (newGCost < neighbor.gCost || openSet.indexOf(neighbor) === -1) { + neighbor.gCost = newGCost; + neighbor.hCost = this.getDistance(neighbor, targetNode); + neighbor.calculateFCost(); + neighbor.parent = currentNode; - /** - * 重建路径 - */ - 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!; + if (openSet.indexOf(neighbor) === -1) { + openSet.push(neighbor); + } } - - 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; - } + // 没有找到路径 + 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); + } } - /** - * 检查位置是否有效 - */ - private isValidPosition(x: number, y: number): boolean { - return x >= 0 && x < this.gridWidth && y >= 0 && y < this.gridHeight; + 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!; } - /** - * 设置节点的可行走状态 - */ - setWalkable(x: number, y: number, walkable: boolean) { - if (this.isValidPosition(x, y)) { - this.grid[x][y].walkable = walkable; - } - } + path.push(new Vec2(startNode.x, startNode.y)); + path.reverse(); - /** - * 获取节点是否可行走 - */ - isWalkable(x: number, y: number): boolean { - if (!this.isValidPosition(x, y)) { - return false; - } - return this.grid[x][y].walkable; + 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; + } } \ No newline at end of file diff --git a/assets/scripts/PlayerController.ts b/assets/scripts/PlayerController.ts index 5486032..896f50a 100644 --- a/assets/scripts/PlayerController.ts +++ b/assets/scripts/PlayerController.ts @@ -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; diff --git a/assets/scripts/TiledMapPathfinder.ts b/assets/scripts/TiledMapPathfinder.ts index 28e2e48..86894cd 100644 --- a/assets/scripts/TiledMapPathfinder.ts +++ b/assets/scripts/TiledMapPathfinder.ts @@ -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; + } } \ No newline at end of file diff --git a/settings/v2/packages/project.json b/settings/v2/packages/project.json index fe5e32a..c983dba 100644 --- a/settings/v2/packages/project.json +++ b/settings/v2/packages/project.json @@ -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 + } } }