feat: 支持自动寻路算法
This commit is contained in:
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
225
assets/scripts/AStarPathfinding.ts
Normal file
225
assets/scripts/AStarPathfinding.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
9
assets/scripts/AStarPathfinding.ts.meta
Normal file
9
assets/scripts/AStarPathfinding.ts.meta
Normal 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
63
assets/scripts/Manager.ts
Normal 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) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
9
assets/scripts/Manager.ts.meta
Normal file
9
assets/scripts/Manager.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "8a6e8f34-f238-4dd9-8247-c6ece27ea4c2",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
152
assets/scripts/PathfindingTest.ts
Normal file
152
assets/scripts/PathfindingTest.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
9
assets/scripts/PathfindingTest.ts.meta
Normal file
9
assets/scripts/PathfindingTest.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "a1a1a2a3-4b5c-6d7e-8f9a-3c4d5e6f7g8h",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
@@ -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();
|
|
||||||
this.isMoving = false;
|
|
||||||
console.log('到达目标位置');
|
|
||||||
} else {
|
|
||||||
// 正常移动
|
|
||||||
const newPosition = new Vec3();
|
|
||||||
Vec3.scaleAndAdd(newPosition, currentPos, direction, moveDistance);
|
|
||||||
|
|
||||||
// 确保新位置在地图边界内
|
// 使用缓动移动到目标位置
|
||||||
const clampedNewPosition = this.clampPlayerPosition(newPosition);
|
tween(this.player)
|
||||||
this.player.position = clampedNewPosition;
|
.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) {
|
||||||
|
// 更新逻辑现在主要由缓动系统处理
|
||||||
|
// 这里可以添加其他需要每帧更新的逻辑
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
250
assets/scripts/TiledMapPathfinder.ts
Normal file
250
assets/scripts/TiledMapPathfinder.ts
Normal 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()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
9
assets/scripts/TiledMapPathfinder.ts.meta
Normal file
9
assets/scripts/TiledMapPathfinder.ts.meta
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"ver": "4.0.24",
|
||||||
|
"importer": "typescript",
|
||||||
|
"imported": true,
|
||||||
|
"uuid": "9f9f8e8d-3b4c-5d6e-9f0e-2b3c4d5e6f7g",
|
||||||
|
"files": [],
|
||||||
|
"subMetas": {},
|
||||||
|
"userData": {}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user