diff --git a/assets/scenes/main.scene b/assets/scenes/main.scene index 0ce1a83..775a060 100644 --- a/assets/scenes/main.scene +++ b/assets/scenes/main.scene @@ -52,7 +52,7 @@ }, "autoReleaseAssets": false, "_globals": { - "__id__": 27 + "__id__": 32 }, "_id": "58132e64-0171-4c7f-89be-a2984ca7de6b" }, @@ -69,28 +69,31 @@ "__id__": 3 }, { - "__id__": 9 + "__id__": 10 }, { - "__id__": 18 + "__id__": 20 }, { "__id__": 6 }, { - "__id__": 21 + "__id__": 23 + }, + { + "__id__": 26 } ], "_active": true, "_components": [ { - "__id__": 24 + "__id__": 29 }, { - "__id__": 25 + "__id__": 30 }, { - "__id__": 26 + "__id__": 31 } ], "_prefab": null, @@ -238,6 +241,8 @@ "z": 10 }, "smoothness": 0.1, + "mapWidth": 1080, + "mapHeight": 2560, "_id": "f3ED2JS1JKurfMG53fWzmg" }, { @@ -256,6 +261,9 @@ }, { "__id__": 8 + }, + { + "__id__": 9 } ], "_prefab": null, @@ -349,6 +357,34 @@ "loop": true, "_id": "bfAE5dYjNG5JfiAUJyxBbf" }, + { + "__type__": "cc.RigidBody2D", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 6 + }, + "_enabled": true, + "__prefab": null, + "enabledContactListener": false, + "bullet": false, + "awakeOnLoad": true, + "_group": 4, + "_type": 2, + "_allowSleep": true, + "_gravityScale": 0, + "_linearDamping": 0, + "_angularDamping": 0, + "_linearVelocity": { + "__type__": "cc.Vec2", + "x": 0, + "y": 0 + }, + "_angularVelocity": 0, + "_fixedRotation": true, + "_id": "f26+sa035BLpRjrovODcjh" + }, { "__type__": "cc.Node", "_name": "TiledMap", @@ -359,19 +395,22 @@ }, "_children": [ { - "__id__": 10 + "__id__": 11 }, { - "__id__": 13 + "__id__": 14 } ], "_active": true, "_components": [ { - "__id__": 16 + "__id__": 17 }, { - "__id__": 17 + "__id__": 18 + }, + { + "__id__": 19 } ], "_prefab": null, @@ -410,16 +449,16 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 9 + "__id__": 10 }, "_children": [], "_active": true, "_components": [ { - "__id__": 11 + "__id__": 12 }, { - "__id__": 12 + "__id__": 13 } ], "_prefab": null, @@ -458,7 +497,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 10 + "__id__": 11 }, "_enabled": true, "__prefab": null, @@ -480,7 +519,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 10 + "__id__": 11 }, "_enabled": true, "__prefab": null, @@ -519,16 +558,16 @@ "_objFlags": 0, "__editorExtras__": {}, "_parent": { - "__id__": 9 + "__id__": 10 }, "_children": [], "_active": true, "_components": [ { - "__id__": 14 + "__id__": 15 }, { - "__id__": 15 + "__id__": 16 } ], "_prefab": null, @@ -567,7 +606,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 13 + "__id__": 14 }, "_enabled": true, "__prefab": null, @@ -589,7 +628,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 13 + "__id__": 14 }, "_enabled": true, "__prefab": null, @@ -601,7 +640,7 @@ "r": 255, "g": 255, "b": 255, - "a": 255 + "a": 0 }, "_id": "30V2VyJEZCsKfZxqWiSZMJ" }, @@ -611,7 +650,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 9 + "__id__": 10 }, "_enabled": true, "__prefab": null, @@ -633,7 +672,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 9 + "__id__": 10 }, "_enabled": true, "__prefab": null, @@ -645,6 +684,23 @@ "cleanupImageCache": true, "_id": "af4s2K0x1MH5srzPTEFKZE" }, + { + "__type__": "9f9f8e8d-3b4c-5d6e-9f0e-2b3c4d5e6f7g", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 10 + }, + "_enabled": true, + "__prefab": null, + "tiledMap": { + "__id__": 18 + }, + "walkableLayerName": "WalkableLayer", + "tileSize": 32, + "_id": "b7cmI9hyJG85zE5AHvozc+" + }, { "__type__": "cc.Node", "_name": "Bg", @@ -657,10 +713,10 @@ "_active": false, "_components": [ { - "__id__": 19 + "__id__": 21 }, { - "__id__": 20 + "__id__": 22 } ], "_prefab": null, @@ -699,7 +755,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 18 + "__id__": 20 }, "_enabled": true, "__prefab": null, @@ -721,7 +777,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 18 + "__id__": 20 }, "_enabled": true, "__prefab": null, @@ -766,10 +822,10 @@ "_active": true, "_components": [ { - "__id__": 22 + "__id__": 24 }, { - "__id__": 23 + "__id__": 25 } ], "_prefab": null, @@ -808,7 +864,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 21 + "__id__": 23 }, "_enabled": true, "__prefab": null, @@ -830,7 +886,7 @@ "_objFlags": 0, "__editorExtras__": {}, "node": { - "__id__": 21 + "__id__": 23 }, "_enabled": true, "__prefab": null, @@ -840,9 +896,102 @@ "camera": { "__id__": 4 }, - "moveSpeed": 5, + "pathfinder": { + "__id__": 19 + }, + "moveSpeed": 300, + "mapWidth": 1080, + "mapHeight": 2560, "_id": "c1AuAU3IlKnLOzgk9vsBr4" }, + { + "__type__": "cc.Node", + "_name": "Manager", + "_objFlags": 0, + "__editorExtras__": {}, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 27 + }, + { + "__id__": 28 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_mobility": 0, + "_layer": 33554432, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "faEV0Z9e5HPJZltMJt2Zn9" + }, + { + "__type__": "cc.UITransform", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 26 + }, + "_enabled": true, + "__prefab": null, + "_contentSize": { + "__type__": "cc.Size", + "width": 100, + "height": 100 + }, + "_anchorPoint": { + "__type__": "cc.Vec2", + "x": 0.5, + "y": 0.5 + }, + "_id": "f6ZiYZJ6JClotvMwMwGhqR" + }, + { + "__type__": "8a6e8808jhN2YJHxuzifqTC", + "_name": "", + "_objFlags": 0, + "__editorExtras__": {}, + "node": { + "__id__": 26 + }, + "_enabled": true, + "__prefab": null, + "tiledMap": { + "__id__": 18 + }, + "playerNode": { + "__id__": 6 + }, + "_id": "35G3LX9BRHiJifNhcAwfoE" + }, { "__type__": "cc.UITransform", "_name": "", @@ -914,29 +1063,29 @@ { "__type__": "cc.SceneGlobals", "ambient": { - "__id__": 28 - }, - "shadows": { - "__id__": 29 - }, - "_skybox": { - "__id__": 30 - }, - "fog": { - "__id__": 31 - }, - "octree": { - "__id__": 32 - }, - "skin": { "__id__": 33 }, - "lightProbeInfo": { + "shadows": { "__id__": 34 }, - "postSettings": { + "_skybox": { "__id__": 35 }, + "fog": { + "__id__": 36 + }, + "octree": { + "__id__": 37 + }, + "skin": { + "__id__": 38 + }, + "lightProbeInfo": { + "__id__": 39 + }, + "postSettings": { + "__id__": 40 + }, "bakedWithStationaryMainLight": false, "bakedWithHighpLightmap": false }, diff --git a/assets/scripts/AStarPathfinding.ts b/assets/scripts/AStarPathfinding.ts new file mode 100644 index 0000000..ef64c7f --- /dev/null +++ b/assets/scripts/AStarPathfinding.ts @@ -0,0 +1,225 @@ +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; + + 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[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 []; + } + + /** + * 获取节点的相邻节点(4方向) + */ + 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 } // 右 + ]; + + for (const dir of directions) { + const checkX = node.x + dir.x; + const checkY = node.y + dir.y; + + if (this.isValidPosition(checkX, checkY)) { + neighbors.push(this.grid[checkX][checkY]); + } + } + + 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); + 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; + } +} \ No newline at end of file diff --git a/assets/scripts/AStarPathfinding.ts.meta b/assets/scripts/AStarPathfinding.ts.meta new file mode 100644 index 0000000..d4c455d --- /dev/null +++ b/assets/scripts/AStarPathfinding.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "8e8e7d9a-2a3b-4c5d-8f9e-1a2b3c4d5e6f", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/scripts/Manager.ts b/assets/scripts/Manager.ts new file mode 100644 index 0000000..0b10db7 --- /dev/null +++ b/assets/scripts/Manager.ts @@ -0,0 +1,63 @@ +import { _decorator, Component, Node, TiledMap } from 'cc'; +import { TiledMapPathfinder } from './TiledMapPathfinder'; +import { PlayerController } from './PlayerController'; +const { ccclass, property } = _decorator; + +@ccclass('Manager') +export class Manager extends Component { + + @property(TiledMap) + tiledMap: TiledMap | null = null; + + @property(Node) + playerNode: Node | null = null; + + private pathfinder: TiledMapPathfinder | null = null; + private playerController: PlayerController | null = null; + + start() { + this.initializeGame(); + } + + private initializeGame() { + // 初始化寻路系统 + if (this.tiledMap) { + // 为TiledMap添加寻路组件 + this.pathfinder = this.tiledMap.node.getComponent(TiledMapPathfinder); + if (!this.pathfinder) { + this.pathfinder = this.tiledMap.node.addComponent(TiledMapPathfinder); + this.pathfinder.tiledMap = this.tiledMap; + this.pathfinder.walkableLayerName = 'WalkableLayer'; + this.pathfinder.tileSize = 32; + } + } + + // 初始化玩家控制器 + if (this.playerNode) { + this.playerController = this.playerNode.getComponent(PlayerController); + if (this.playerController && this.pathfinder) { + this.playerController.pathfinder = this.pathfinder; + } + } + + console.log('游戏初始化完成'); + this.logMapInfo(); + } + + private logMapInfo() { + if (this.pathfinder) { + const mapInfo = this.pathfinder.getMapInfo(); + if (mapInfo) { + console.log('地图信息:', { + mapSize: mapInfo.mapSize, + tileSize: mapInfo.tileSize, + orientation: mapInfo.orientation + }); + } + } + } + + update(deltaTime: number) { + + } +} \ No newline at end of file diff --git a/assets/scripts/Manager.ts.meta b/assets/scripts/Manager.ts.meta new file mode 100644 index 0000000..5864308 --- /dev/null +++ b/assets/scripts/Manager.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "8a6e8f34-f238-4dd9-8247-c6ece27ea4c2", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/scripts/PathfindingTest.ts b/assets/scripts/PathfindingTest.ts new file mode 100644 index 0000000..2d6ecfd --- /dev/null +++ b/assets/scripts/PathfindingTest.ts @@ -0,0 +1,152 @@ +import { _decorator, Component, Node, TiledMap, Vec3, Label, input, Input, EventTouch } from 'cc'; +import { TiledMapPathfinder } from './TiledMapPathfinder'; +const { ccclass, property } = _decorator; + +@ccclass('PathfindingTest') +export class PathfindingTest extends Component { + + @property(TiledMap) + tiledMap: TiledMap | null = null; + + @property(Node) + startPoint: Node | null = null; + + @property(Node) + endPoint: Node | null = null; + + @property(Label) + infoLabel: Label | null = null; + + private pathfinder: TiledMapPathfinder | null = null; + private currentPath: Vec3[] = []; + + onLoad() { + input.on(Input.EventType.TOUCH_START, this.onTouch, this); + } + + onDestroy() { + input.off(Input.EventType.TOUCH_START, this.onTouch, this); + } + + start() { + this.initializePathfinder(); + this.runTests(); + } + + private initializePathfinder() { + if (!this.tiledMap) { + console.error('TiledMap未设置'); + return; + } + + this.pathfinder = this.tiledMap.node.getComponent(TiledMapPathfinder); + if (!this.pathfinder) { + this.pathfinder = this.tiledMap.node.addComponent(TiledMapPathfinder); + this.pathfinder.tiledMap = this.tiledMap; + this.pathfinder.walkableLayerName = 'WalkableLayer'; + this.pathfinder.tileSize = 32; + } + } + + private runTests() { + if (!this.pathfinder) { + console.error('寻路器未初始化'); + return; + } + + // 等待一帧让寻路器完全初始化 + this.scheduleOnce(() => { + this.performPathfindingTest(); + }, 0.1); + } + + private performPathfindingTest() { + if (!this.pathfinder || !this.startPoint || !this.endPoint) { + console.error('测试组件未完整设置'); + return; + } + + const startPos = this.startPoint.position; + const endPos = this.endPoint.position; + + console.log(`测试寻路: 从 (${startPos.x}, ${startPos.y}) 到 (${endPos.x}, ${endPos.y})`); + + // 检查位置是否可行走 + const startWalkable = this.pathfinder.isWorldPositionWalkable(startPos); + const endWalkable = this.pathfinder.isWorldPositionWalkable(endPos); + + console.log(`起点可行走: ${startWalkable}, 终点可行走: ${endWalkable}`); + + // 如果位置不可行走,寻找最近的可行走位置 + let adjustedStartPos = startPos; + let adjustedEndPos = endPos; + + if (!startWalkable) { + const closestStart = this.pathfinder.getClosestWalkablePosition(startPos); + if (closestStart) { + adjustedStartPos = closestStart; + console.log(`调整起点到: (${adjustedStartPos.x}, ${adjustedStartPos.y})`); + } + } + + if (!endWalkable) { + const closestEnd = this.pathfinder.getClosestWalkablePosition(endPos); + if (closestEnd) { + adjustedEndPos = closestEnd; + console.log(`调整终点到: (${adjustedEndPos.x}, ${adjustedEndPos.y})`); + } + } + + // 执行寻路 + this.currentPath = this.pathfinder.findPath(adjustedStartPos, adjustedEndPos); + + if (this.currentPath.length > 0) { + console.log(`找到路径,包含 ${this.currentPath.length} 个点:`); + this.currentPath.forEach((point, index) => { + console.log(` 路径点 ${index}: (${point.x.toFixed(2)}, ${point.y.toFixed(2)})`); + }); + + this.updateInfoLabel(`路径找到!包含 ${this.currentPath.length} 个点`); + } else { + console.log('未找到路径'); + this.updateInfoLabel('未找到路径'); + } + } + + private onTouch(event: EventTouch) { + // 可以通过触摸来动态测试寻路 + if (!this.pathfinder || !this.startPoint) return; + + const touchLocation = event.getUILocation(); + // 这里可以添加触摸点寻路测试的逻辑 + } + + private updateInfoLabel(text: string) { + if (this.infoLabel) { + this.infoLabel.string = text; + } + } + + // 公共方法供外部调用测试 + public testPathfinding(startWorldPos: Vec3, endWorldPos: Vec3): Vec3[] { + if (!this.pathfinder) { + console.error('寻路器未初始化'); + return []; + } + + return this.pathfinder.findPath(startWorldPos, endWorldPos); + } + + // 获取当前找到的路径 + public getCurrentPath(): Vec3[] { + return this.currentPath.slice(); // 返回副本 + } + + // 检查位置是否可行走 + public isPositionWalkable(worldPos: Vec3): boolean { + if (!this.pathfinder) { + return false; + } + return this.pathfinder.isWorldPositionWalkable(worldPos); + } +} \ No newline at end of file diff --git a/assets/scripts/PathfindingTest.ts.meta b/assets/scripts/PathfindingTest.ts.meta new file mode 100644 index 0000000..04edadb --- /dev/null +++ b/assets/scripts/PathfindingTest.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "a1a1a2a3-4b5c-6d7e-8f9a-3c4d5e6f7g8h", + "files": [], + "subMetas": {}, + "userData": {} +} diff --git a/assets/scripts/PlayerController.ts b/assets/scripts/PlayerController.ts index 582be79..dca7303 100644 --- a/assets/scripts/PlayerController.ts +++ b/assets/scripts/PlayerController.ts @@ -1,4 +1,5 @@ -import { _decorator, Component, Node, Vec3, input, Input, EventTouch, Camera, view } from 'cc'; +import { _decorator, Component, Node, Vec3, input, Input, EventTouch, Camera, view, tween } from 'cc'; +import { TiledMapPathfinder } from './TiledMapPathfinder'; const { ccclass, property } = _decorator; @ccclass('PlayerController') @@ -10,8 +11,11 @@ export class PlayerController extends Component { @property(Camera) camera: Camera | null = null; // 主摄像机 - @property({ range: [1, 20] }) - moveSpeed: number = 5; // 移动速度 + @property(TiledMapPathfinder) + pathfinder: TiledMapPathfinder | null = null; // 寻路组件 + + @property({ range: [1, 300] }) + moveSpeed: number = 300; // 移动速度(像素/秒) @property mapWidth: number = 1080; // 地图宽度 @@ -20,7 +24,8 @@ export class PlayerController extends Component { mapHeight: number = 2560; // 地图高度 private isMoving: boolean = false; - private targetPosition: Vec3 = new Vec3(); + private currentPath: Vec3[] = []; + private currentPathIndex: number = 0; private originalPosition: Vec3 = new Vec3(); onLoad() { @@ -40,7 +45,7 @@ export class PlayerController extends Component { } private onTouchStart(event: EventTouch) { - if (!this.player || !this.camera) return; + if (!this.player || !this.camera || !this.pathfinder) return; // 获取触摸点的UI坐标 const touchLocation = event.getUILocation(); @@ -51,7 +56,7 @@ export class PlayerController extends Component { console.log(`触摸UI坐标: (${touchLocation.x}, ${touchLocation.y})`); console.log(`转换后世界坐标: (${worldPos.x.toFixed(2)}, ${worldPos.y.toFixed(2)})`); - this.moveToPosition(worldPos); + this.moveToPositionWithPathfinding(worldPos); } private screenToWorldPoint(screenPos: { x: number, y: number }): Vec3 { @@ -82,17 +87,41 @@ export class PlayerController extends Component { } - private moveToPosition(worldPos: Vec3) { - if (!this.player) return; + private moveToPositionWithPathfinding(worldPos: Vec3) { + if (!this.player || !this.pathfinder) return; + + // 停止当前移动 + this.stopMovement(); // 限制目标位置在地图边界内 const clampedPos = this.clampPlayerPosition(worldPos); - // 设置目标位置(保持Z轴不变) - this.targetPosition.set(clampedPos.x, clampedPos.y, this.player.position.z); - this.isMoving = true; + // 检查目标位置是否可行走 + if (!this.pathfinder.isWorldPositionWalkable(clampedPos)) { + console.log('目标位置不可行走,寻找最近的可行走位置'); + const closestWalkable = this.pathfinder.getClosestWalkablePosition(clampedPos); + if (!closestWalkable) { + console.warn('找不到可行走的位置'); + return; + } + clampedPos.set(closestWalkable); + } - console.log(`移动目标: (${clampedPos.x.toFixed(2)}, ${clampedPos.y.toFixed(2)})`); + // 使用寻路算法计算路径 + const startPos = this.player.position; + this.currentPath = this.pathfinder.findPath(startPos, clampedPos); + + if (this.currentPath.length === 0) { + console.warn('无法找到路径'); + return; + } + + console.log(`找到路径,包含${this.currentPath.length}个点`); + + // 开始沿路径移动 + this.currentPathIndex = 0; + this.isMoving = true; + this.moveToNextWaypoint(); } // 限制玩家位置在地图边界内 @@ -109,41 +138,50 @@ export class PlayerController extends Component { return clampedPosition; } - update(deltaTime: number) { - if (!this.isMoving || !this.player) return; - - const currentPos = this.player.position; - const distance = Vec3.distance(currentPos, this.targetPosition); - - // 如果距离很小,直接到达目标位置 - if (distance < 0.1) { - this.player.position = this.targetPosition.clone(); + /** + * 移动到路径中的下一个路径点 + */ + private moveToNextWaypoint() { + if (!this.player || this.currentPath.length === 0 || this.currentPathIndex >= this.currentPath.length) { this.isMoving = false; - console.log('到达目标位置'); + console.log('路径移动完成'); return; } - // 计算移动方向 - const direction = new Vec3(); - Vec3.subtract(direction, this.targetPosition, currentPos); - direction.normalize(); + const targetPos = this.currentPath[this.currentPathIndex]; + const currentPos = this.player.position; - // 计算这一帧应该移动的距离 - const moveDistance = this.moveSpeed * deltaTime * 100; // 增加移动速度倍数 + // 计算移动距离和时间 + const distance = Vec3.distance(currentPos, targetPos); + const moveTime = distance / this.moveSpeed; - // 如果剩余距离小于这一帧要移动的距离,直接到达目标 - if (distance <= moveDistance) { - this.player.position = this.targetPosition.clone(); - this.isMoving = false; - console.log('到达目标位置'); - } else { - // 正常移动 - const newPosition = new Vec3(); - Vec3.scaleAndAdd(newPosition, currentPos, direction, moveDistance); + console.log(`移动到路径点${this.currentPathIndex}: (${targetPos.x.toFixed(2)}, ${targetPos.y.toFixed(2)})`); - // 确保新位置在地图边界内 - const clampedNewPosition = this.clampPlayerPosition(newPosition); - this.player.position = clampedNewPosition; + // 使用缓动移动到目标位置 + tween(this.player) + .to(moveTime, { position: targetPos }, { + onComplete: () => { + this.currentPathIndex++; + this.moveToNextWaypoint(); + } + }) + .start(); + } + + /** + * 停止当前移动 + */ + private stopMovement() { + if (this.player) { + tween(this.player).stop(); } + this.isMoving = false; + this.currentPath = []; + this.currentPathIndex = 0; + } + + update(deltaTime: number) { + // 更新逻辑现在主要由缓动系统处理 + // 这里可以添加其他需要每帧更新的逻辑 } } \ No newline at end of file diff --git a/assets/scripts/TiledMapPathfinder.ts b/assets/scripts/TiledMapPathfinder.ts new file mode 100644 index 0000000..28e2e48 --- /dev/null +++ b/assets/scripts/TiledMapPathfinder.ts @@ -0,0 +1,250 @@ +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 []; + } + + // 将世界坐标转换为瓦片坐标 + 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); + } + + console.log(`找到路径,包含${worldPath.length}个点`); + return worldPath; + } + + /** + * 世界坐标转换为瓦片坐标 + */ + 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() + }; + } +} \ No newline at end of file diff --git a/assets/scripts/TiledMapPathfinder.ts.meta b/assets/scripts/TiledMapPathfinder.ts.meta new file mode 100644 index 0000000..d0691c0 --- /dev/null +++ b/assets/scripts/TiledMapPathfinder.ts.meta @@ -0,0 +1,9 @@ +{ + "ver": "4.0.24", + "importer": "typescript", + "imported": true, + "uuid": "9f9f8e8d-3b4c-5d6e-9f0e-2b3c4d5e6f7g", + "files": [], + "subMetas": {}, + "userData": {} +}