feat: 支持自动寻路算法

This commit is contained in:
2025-09-21 21:31:54 +08:00
parent 35cfabb66b
commit d6aa74cb9d
10 changed files with 1003 additions and 90 deletions

View File

@@ -52,7 +52,7 @@
}, },
"autoReleaseAssets": false, "autoReleaseAssets": false,
"_globals": { "_globals": {
"__id__": 27 "__id__": 32
}, },
"_id": "58132e64-0171-4c7f-89be-a2984ca7de6b" "_id": "58132e64-0171-4c7f-89be-a2984ca7de6b"
}, },
@@ -69,28 +69,31 @@
"__id__": 3 "__id__": 3
}, },
{ {
"__id__": 9 "__id__": 10
}, },
{ {
"__id__": 18 "__id__": 20
}, },
{ {
"__id__": 6 "__id__": 6
}, },
{ {
"__id__": 21 "__id__": 23
},
{
"__id__": 26
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 24 "__id__": 29
}, },
{ {
"__id__": 25 "__id__": 30
}, },
{ {
"__id__": 26 "__id__": 31
} }
], ],
"_prefab": null, "_prefab": null,
@@ -238,6 +241,8 @@
"z": 10 "z": 10
}, },
"smoothness": 0.1, "smoothness": 0.1,
"mapWidth": 1080,
"mapHeight": 2560,
"_id": "f3ED2JS1JKurfMG53fWzmg" "_id": "f3ED2JS1JKurfMG53fWzmg"
}, },
{ {
@@ -256,6 +261,9 @@
}, },
{ {
"__id__": 8 "__id__": 8
},
{
"__id__": 9
} }
], ],
"_prefab": null, "_prefab": null,
@@ -349,6 +357,34 @@
"loop": true, "loop": true,
"_id": "bfAE5dYjNG5JfiAUJyxBbf" "_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", "__type__": "cc.Node",
"_name": "TiledMap", "_name": "TiledMap",
@@ -359,19 +395,22 @@
}, },
"_children": [ "_children": [
{ {
"__id__": 10 "__id__": 11
}, },
{ {
"__id__": 13 "__id__": 14
} }
], ],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 16 "__id__": 17
}, },
{ {
"__id__": 17 "__id__": 18
},
{
"__id__": 19
} }
], ],
"_prefab": null, "_prefab": null,
@@ -410,16 +449,16 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_parent": { "_parent": {
"__id__": 9 "__id__": 10
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 11 "__id__": 12
}, },
{ {
"__id__": 12 "__id__": 13
} }
], ],
"_prefab": null, "_prefab": null,
@@ -458,7 +497,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 10 "__id__": 11
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -480,7 +519,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 10 "__id__": 11
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -519,16 +558,16 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"_parent": { "_parent": {
"__id__": 9 "__id__": 10
}, },
"_children": [], "_children": [],
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 14 "__id__": 15
}, },
{ {
"__id__": 15 "__id__": 16
} }
], ],
"_prefab": null, "_prefab": null,
@@ -567,7 +606,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 13 "__id__": 14
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -589,7 +628,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 13 "__id__": 14
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -601,7 +640,7 @@
"r": 255, "r": 255,
"g": 255, "g": 255,
"b": 255, "b": 255,
"a": 255 "a": 0
}, },
"_id": "30V2VyJEZCsKfZxqWiSZMJ" "_id": "30V2VyJEZCsKfZxqWiSZMJ"
}, },
@@ -611,7 +650,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 9 "__id__": 10
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -633,7 +672,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 9 "__id__": 10
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -645,6 +684,23 @@
"cleanupImageCache": true, "cleanupImageCache": true,
"_id": "af4s2K0x1MH5srzPTEFKZE" "_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", "__type__": "cc.Node",
"_name": "Bg", "_name": "Bg",
@@ -657,10 +713,10 @@
"_active": false, "_active": false,
"_components": [ "_components": [
{ {
"__id__": 19 "__id__": 21
}, },
{ {
"__id__": 20 "__id__": 22
} }
], ],
"_prefab": null, "_prefab": null,
@@ -699,7 +755,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 18 "__id__": 20
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -721,7 +777,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 18 "__id__": 20
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -766,10 +822,10 @@
"_active": true, "_active": true,
"_components": [ "_components": [
{ {
"__id__": 22 "__id__": 24
}, },
{ {
"__id__": 23 "__id__": 25
} }
], ],
"_prefab": null, "_prefab": null,
@@ -808,7 +864,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 21 "__id__": 23
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -830,7 +886,7 @@
"_objFlags": 0, "_objFlags": 0,
"__editorExtras__": {}, "__editorExtras__": {},
"node": { "node": {
"__id__": 21 "__id__": 23
}, },
"_enabled": true, "_enabled": true,
"__prefab": null, "__prefab": null,
@@ -840,9 +896,102 @@
"camera": { "camera": {
"__id__": 4 "__id__": 4
}, },
"moveSpeed": 5, "pathfinder": {
"__id__": 19
},
"moveSpeed": 300,
"mapWidth": 1080,
"mapHeight": 2560,
"_id": "c1AuAU3IlKnLOzgk9vsBr4" "_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", "__type__": "cc.UITransform",
"_name": "", "_name": "",
@@ -914,29 +1063,29 @@
{ {
"__type__": "cc.SceneGlobals", "__type__": "cc.SceneGlobals",
"ambient": { "ambient": {
"__id__": 28
},
"shadows": {
"__id__": 29
},
"_skybox": {
"__id__": 30
},
"fog": {
"__id__": 31
},
"octree": {
"__id__": 32
},
"skin": {
"__id__": 33 "__id__": 33
}, },
"lightProbeInfo": { "shadows": {
"__id__": 34 "__id__": 34
}, },
"postSettings": { "_skybox": {
"__id__": 35 "__id__": 35
}, },
"fog": {
"__id__": 36
},
"octree": {
"__id__": 37
},
"skin": {
"__id__": 38
},
"lightProbeInfo": {
"__id__": 39
},
"postSettings": {
"__id__": 40
},
"bakedWithStationaryMainLight": false, "bakedWithStationaryMainLight": false,
"bakedWithHighpLightmap": false "bakedWithHighpLightmap": false
}, },

View File

@@ -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;
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8e8e7d9a-2a3b-4c5d-8f9e-1a2b3c4d5e6f",
"files": [],
"subMetas": {},
"userData": {}
}

63
assets/scripts/Manager.ts Normal file
View File

@@ -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) {
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "8a6e8f34-f238-4dd9-8247-c6ece27ea4c2",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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);
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "a1a1a2a3-4b5c-6d7e-8f9a-3c4d5e6f7g8h",
"files": [],
"subMetas": {},
"userData": {}
}

View File

@@ -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; const { ccclass, property } = _decorator;
@ccclass('PlayerController') @ccclass('PlayerController')
@@ -10,8 +11,11 @@ export class PlayerController extends Component {
@property(Camera) @property(Camera)
camera: Camera | null = null; // 主摄像机 camera: Camera | null = null; // 主摄像机
@property({ range: [1, 20] }) @property(TiledMapPathfinder)
moveSpeed: number = 5; // 移动速度 pathfinder: TiledMapPathfinder | null = null; // 寻路组件
@property({ range: [1, 300] })
moveSpeed: number = 300; // 移动速度(像素/秒)
@property @property
mapWidth: number = 1080; // 地图宽度 mapWidth: number = 1080; // 地图宽度
@@ -20,7 +24,8 @@ export class PlayerController extends Component {
mapHeight: number = 2560; // 地图高度 mapHeight: number = 2560; // 地图高度
private isMoving: boolean = false; private isMoving: boolean = false;
private targetPosition: Vec3 = new Vec3(); private currentPath: Vec3[] = [];
private currentPathIndex: number = 0;
private originalPosition: Vec3 = new Vec3(); private originalPosition: Vec3 = new Vec3();
onLoad() { onLoad() {
@@ -40,7 +45,7 @@ export class PlayerController extends Component {
} }
private onTouchStart(event: EventTouch) { private onTouchStart(event: EventTouch) {
if (!this.player || !this.camera) return; if (!this.player || !this.camera || !this.pathfinder) return;
// 获取触摸点的UI坐标 // 获取触摸点的UI坐标
const touchLocation = event.getUILocation(); const touchLocation = event.getUILocation();
@@ -51,7 +56,7 @@ export class PlayerController extends Component {
console.log(`触摸UI坐标: (${touchLocation.x}, ${touchLocation.y})`); console.log(`触摸UI坐标: (${touchLocation.x}, ${touchLocation.y})`);
console.log(`转换后世界坐标: (${worldPos.x.toFixed(2)}, ${worldPos.y.toFixed(2)})`); console.log(`转换后世界坐标: (${worldPos.x.toFixed(2)}, ${worldPos.y.toFixed(2)})`);
this.moveToPosition(worldPos); this.moveToPositionWithPathfinding(worldPos);
} }
private screenToWorldPoint(screenPos: { x: number, y: number }): Vec3 { private screenToWorldPoint(screenPos: { x: number, y: number }): Vec3 {
@@ -82,17 +87,41 @@ export class PlayerController extends Component {
} }
private moveToPosition(worldPos: Vec3) { private moveToPositionWithPathfinding(worldPos: Vec3) {
if (!this.player) return; if (!this.player || !this.pathfinder) return;
// 停止当前移动
this.stopMovement();
// 限制目标位置在地图边界内 // 限制目标位置在地图边界内
const clampedPos = this.clampPlayerPosition(worldPos); const clampedPos = this.clampPlayerPosition(worldPos);
// 设置目标位置保持Z轴不变 // 检查目标位置是否可行走
this.targetPosition.set(clampedPos.x, clampedPos.y, this.player.position.z); if (!this.pathfinder.isWorldPositionWalkable(clampedPos)) {
this.isMoving = true; 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; return clampedPosition;
} }
update(deltaTime: number) { /**
if (!this.isMoving || !this.player) return; * 移动到路径中的下一个路径点
*/
const currentPos = this.player.position; private moveToNextWaypoint() {
const distance = Vec3.distance(currentPos, this.targetPosition); if (!this.player || this.currentPath.length === 0 || this.currentPathIndex >= this.currentPath.length) {
// 如果距离很小,直接到达目标位置
if (distance < 0.1) {
this.player.position = this.targetPosition.clone();
this.isMoving = false; this.isMoving = false;
console.log('到达目标位置'); console.log('路径移动完成');
return; return;
} }
// 计算移动方向 const targetPos = this.currentPath[this.currentPathIndex];
const direction = new Vec3(); const currentPos = this.player.position;
Vec3.subtract(direction, this.targetPosition, currentPos);
direction.normalize();
// 计算这一帧应该移动距离 // 计算移动距离和时间
const moveDistance = this.moveSpeed * deltaTime * 100; // 增加移动速度倍数 const distance = Vec3.distance(currentPos, targetPos);
const moveTime = distance / this.moveSpeed;
// 如果剩余距离小于这一帧要移动的距离,直接到达目标 console.log(`移动到路径点${this.currentPathIndex}: (${targetPos.x.toFixed(2)}, ${targetPos.y.toFixed(2)})`);
if (distance <= moveDistance) {
this.player.position = this.targetPosition.clone(); // 使用缓动移动到目标位置
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.isMoving = false;
console.log('到达目标位置'); this.currentPath = [];
} else { this.currentPathIndex = 0;
// 正常移动 }
const newPosition = new Vec3();
Vec3.scaleAndAdd(newPosition, currentPos, direction, moveDistance);
// 确保新位置在地图边界内 update(deltaTime: number) {
const clampedNewPosition = this.clampPlayerPosition(newPosition); // 更新逻辑现在主要由缓动系统处理
this.player.position = clampedNewPosition; // 这里可以添加其他需要每帧更新的逻辑
}
} }
} }

View File

@@ -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()
};
}
}

View File

@@ -0,0 +1,9 @@
{
"ver": "4.0.24",
"importer": "typescript",
"imported": true,
"uuid": "9f9f8e8d-3b4c-5d6e-9f0e-2b3c4d5e6f7g",
"files": [],
"subMetas": {},
"userData": {}
}