@esengine/pathfinding 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/{bin/modules/astar.js → dist/pathfinding.cjs} +18 -27
- package/dist/pathfinding.cjs.map +1 -0
- package/{bin → dist}/pathfinding.d.ts +24 -41
- package/dist/pathfinding.js +556 -0
- package/dist/pathfinding.js.map +1 -0
- package/{bin/modules/breadth-first.js → dist/pathfinding.mjs} +11 -28
- package/dist/pathfinding.mjs.map +1 -0
- package/package.json +23 -21
- package/.idea/misc.xml +0 -6
- package/.idea/modules.xml +0 -8
- package/.idea/source.iml +0 -12
- package/.idea/vcs.xml +0 -6
- package/.vscode/tasks.json +0 -13
- package/.wing/settings.json +0 -3
- package/bin/README.md +0 -245
- package/bin/modules/astar.min.js +0 -1
- package/bin/modules/breadth-first.min.js +0 -1
- package/bin/package.json +0 -54
- package/bin/pathfinding.js +0 -559
- package/bin/pathfinding.min.js +0 -1
- package/gulpfile.js +0 -137
- package/lib/wxgame.d.ts +0 -3945
- package/src/AI/Pathfinding/AStar/AStarPathfinder.ts +0 -244
- package/src/AI/Pathfinding/AStar/AstarGridGraph.ts +0 -183
- package/src/AI/Pathfinding/AStar/IAstarGraph.ts +0 -30
- package/src/AI/Pathfinding/BreadthFirst/BreadthFirstPathfinder.ts +0 -109
- package/src/AI/Pathfinding/BreadthFirst/IUnweightedGraph.ts +0 -14
- package/src/AI/Pathfinding/BreadthFirst/UnweightedGraph.ts +0 -29
- package/src/AI/Pathfinding/BreadthFirst/UnweightedGridGraph.ts +0 -81
- package/src/Types/IVector2.ts +0 -102
- package/src/Utils/PriorityQueue.ts +0 -121
- package/src/index.ts +0 -49
- package/tsconfig.json +0 -34
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { IVector2, Vector2Utils } from '../../../Types/IVector2';
|
|
2
|
-
import { IAstarGraph } from './IAstarGraph';
|
|
3
|
-
import { PriorityQueue, IPriorityQueueNode } from '../../../Utils/PriorityQueue';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* A*算法节点
|
|
7
|
-
*/
|
|
8
|
-
class AStarNode implements IPriorityQueueNode {
|
|
9
|
-
public node: IVector2;
|
|
10
|
-
public priority: number = 0;
|
|
11
|
-
public gCost: number = 0;
|
|
12
|
-
public hCost: number = 0;
|
|
13
|
-
public parent: AStarNode | null = null;
|
|
14
|
-
public hash: number = 0; // 缓存哈希值,避免重复计算
|
|
15
|
-
|
|
16
|
-
constructor(node: IVector2, gCost: number = 0, hCost: number = 0, parent: AStarNode | null = null) {
|
|
17
|
-
this.node = node;
|
|
18
|
-
this.gCost = gCost;
|
|
19
|
-
this.hCost = hCost;
|
|
20
|
-
this.priority = gCost + hCost;
|
|
21
|
-
this.parent = parent;
|
|
22
|
-
this.hash = Vector2Utils.toHash(node); // 预计算哈希值
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
updateCosts(gCost: number, hCost: number, parent: AStarNode | null = null): void {
|
|
26
|
-
this.gCost = gCost;
|
|
27
|
-
this.hCost = hCost;
|
|
28
|
-
this.priority = gCost + hCost;
|
|
29
|
-
this.parent = parent;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
updateNode(node: IVector2, gCost: number = 0, hCost: number = 0, parent: AStarNode | null = null): void {
|
|
33
|
-
this.node = node;
|
|
34
|
-
this.gCost = gCost;
|
|
35
|
-
this.hCost = hCost;
|
|
36
|
-
this.priority = gCost + hCost;
|
|
37
|
-
this.parent = parent;
|
|
38
|
-
this.hash = Vector2Utils.toHash(node); // 更新哈希值
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
reset(): void {
|
|
42
|
-
this.node = null as any;
|
|
43
|
-
this.priority = 0;
|
|
44
|
-
this.gCost = 0;
|
|
45
|
-
this.hCost = 0;
|
|
46
|
-
this.parent = null;
|
|
47
|
-
this.hash = 0;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export class AStarPathfinder {
|
|
52
|
-
private static _nodePool: AStarNode[] = [];
|
|
53
|
-
private static _tempPath: IVector2[] = [];
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 从对象池获取节点
|
|
57
|
-
*/
|
|
58
|
-
private static _getNode(node: IVector2, gCost: number = 0, hCost: number = 0, parent: AStarNode | null = null): AStarNode {
|
|
59
|
-
let astarNode = this._nodePool.pop();
|
|
60
|
-
if (!astarNode) {
|
|
61
|
-
astarNode = new AStarNode(node, gCost, hCost, parent);
|
|
62
|
-
} else {
|
|
63
|
-
astarNode.updateNode(node, gCost, hCost, parent);
|
|
64
|
-
}
|
|
65
|
-
return astarNode;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 回收节点到对象池
|
|
70
|
-
*/
|
|
71
|
-
private static _recycleNode(node: AStarNode): void {
|
|
72
|
-
if (this._nodePool.length < 1000) { // 限制池大小
|
|
73
|
-
node.reset();
|
|
74
|
-
this._nodePool.push(node);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 使用A*算法搜索路径
|
|
80
|
-
* @param graph 图对象
|
|
81
|
-
* @param start 起始节点
|
|
82
|
-
* @param goal 目标节点
|
|
83
|
-
* @returns 搜索结果
|
|
84
|
-
*/
|
|
85
|
-
static search<T extends IVector2>(
|
|
86
|
-
graph: IAstarGraph<T>,
|
|
87
|
-
start: T,
|
|
88
|
-
goal: T
|
|
89
|
-
): { found: boolean; goalNode?: AStarNode } {
|
|
90
|
-
const openSet = new PriorityQueue<AStarNode>();
|
|
91
|
-
const closedSet = new Set<number>(); // 使用数值哈希
|
|
92
|
-
const openSetMap = new Map<number, AStarNode>(); // 使用数值哈希
|
|
93
|
-
|
|
94
|
-
const startHash = Vector2Utils.toHash(start);
|
|
95
|
-
const goalHash = Vector2Utils.toHash(goal);
|
|
96
|
-
|
|
97
|
-
if (startHash === goalHash) {
|
|
98
|
-
return { found: true, goalNode: this._getNode(start, 0, 0) };
|
|
99
|
-
}
|
|
100
|
-
const startNode = this._getNode(start, 0, graph.heuristic(start, goal));
|
|
101
|
-
openSet.enqueue(startNode);
|
|
102
|
-
openSetMap.set(startHash, startNode);
|
|
103
|
-
|
|
104
|
-
let goalNode: AStarNode | undefined;
|
|
105
|
-
|
|
106
|
-
while (!openSet.isEmpty) {
|
|
107
|
-
const current = openSet.dequeue()!;
|
|
108
|
-
const currentHash = current.hash;
|
|
109
|
-
|
|
110
|
-
openSetMap.delete(currentHash);
|
|
111
|
-
|
|
112
|
-
if (currentHash === goalHash) {
|
|
113
|
-
goalNode = current;
|
|
114
|
-
break;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
closedSet.add(currentHash);
|
|
118
|
-
for (const neighbor of graph.getNeighbors(current.node as T)) {
|
|
119
|
-
const neighborHash = Vector2Utils.toHash(neighbor);
|
|
120
|
-
|
|
121
|
-
if (closedSet.has(neighborHash)) {
|
|
122
|
-
continue;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const tentativeGScore = current.gCost + graph.cost(current.node as T, neighbor);
|
|
126
|
-
const existingNode = openSetMap.get(neighborHash);
|
|
127
|
-
|
|
128
|
-
if (existingNode) {
|
|
129
|
-
if (tentativeGScore < existingNode.gCost) {
|
|
130
|
-
const hCost = existingNode.hCost;
|
|
131
|
-
existingNode.updateCosts(tentativeGScore, hCost, current);
|
|
132
|
-
}
|
|
133
|
-
} else {
|
|
134
|
-
const hCost = graph.heuristic(neighbor, goal);
|
|
135
|
-
const neighborNode = this._getNode(neighbor, tentativeGScore, hCost, current);
|
|
136
|
-
openSet.enqueue(neighborNode);
|
|
137
|
-
openSetMap.set(neighborHash, neighborNode);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
if (current !== goalNode) {
|
|
142
|
-
this._recycleNode(current);
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
while (!openSet.isEmpty) {
|
|
146
|
-
const node = openSet.dequeue()!;
|
|
147
|
-
if (node !== goalNode) {
|
|
148
|
-
this._recycleNode(node);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return { found: !!goalNode, goalNode };
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
/**
|
|
156
|
-
* 搜索并返回完整路径
|
|
157
|
-
* @param graph 图对象
|
|
158
|
-
* @param start 起始节点
|
|
159
|
-
* @param goal 目标节点
|
|
160
|
-
* @returns 路径数组,如果没找到则返回空数组
|
|
161
|
-
*/
|
|
162
|
-
static searchPath<T extends IVector2>(
|
|
163
|
-
graph: IAstarGraph<T>,
|
|
164
|
-
start: T,
|
|
165
|
-
goal: T
|
|
166
|
-
): T[] {
|
|
167
|
-
const result = this.search(graph, start, goal);
|
|
168
|
-
|
|
169
|
-
if (!result.found || !result.goalNode) {
|
|
170
|
-
return [];
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return this.reconstructPathFromNode(result.goalNode, start);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/**
|
|
177
|
-
* 从目标节点重构路径
|
|
178
|
-
* @param goalNode 目标节点
|
|
179
|
-
* @param start 起始节点
|
|
180
|
-
* @returns 完整路径
|
|
181
|
-
*/
|
|
182
|
-
private static reconstructPathFromNode<T extends IVector2>(
|
|
183
|
-
goalNode: AStarNode,
|
|
184
|
-
start: T
|
|
185
|
-
): T[] {
|
|
186
|
-
this._tempPath.length = 0;
|
|
187
|
-
|
|
188
|
-
let current: AStarNode | null = goalNode;
|
|
189
|
-
const startHash = Vector2Utils.toHash(start);
|
|
190
|
-
|
|
191
|
-
while (current) {
|
|
192
|
-
this._tempPath.unshift(current.node as T);
|
|
193
|
-
|
|
194
|
-
const currentHash = current.hash;
|
|
195
|
-
const parent = current.parent;
|
|
196
|
-
|
|
197
|
-
if (currentHash !== startHash) {
|
|
198
|
-
this._recycleNode(current);
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
current = parent;
|
|
202
|
-
}
|
|
203
|
-
return [...this._tempPath] as T[];
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
/**
|
|
207
|
-
* 检查路径是否存在
|
|
208
|
-
* @param graph 图对象
|
|
209
|
-
* @param start 起始节点
|
|
210
|
-
* @param goal 目标节点
|
|
211
|
-
* @returns 是否存在路径
|
|
212
|
-
*/
|
|
213
|
-
static hasPath<T extends IVector2>(
|
|
214
|
-
graph: IAstarGraph<T>,
|
|
215
|
-
start: T,
|
|
216
|
-
goal: T
|
|
217
|
-
): boolean {
|
|
218
|
-
const result = this.search(graph, start, goal);
|
|
219
|
-
|
|
220
|
-
if (result.goalNode) {
|
|
221
|
-
this._recycleNode(result.goalNode);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return result.found;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
/**
|
|
228
|
-
* 清理对象池(可选调用,用于内存管理)
|
|
229
|
-
*/
|
|
230
|
-
static clearPool(): void {
|
|
231
|
-
this._nodePool.length = 0;
|
|
232
|
-
this._tempPath.length = 0;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* 获取对象池统计信息
|
|
237
|
-
*/
|
|
238
|
-
static getPoolStats(): { poolSize: number; maxPoolSize: number } {
|
|
239
|
-
return {
|
|
240
|
-
poolSize: this._nodePool.length,
|
|
241
|
-
maxPoolSize: 1000
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
}
|
|
@@ -1,183 +0,0 @@
|
|
|
1
|
-
import { IVector2, Vector2Utils } from '../../../Types/IVector2';
|
|
2
|
-
import { IAstarGraph } from './IAstarGraph';
|
|
3
|
-
import { AStarPathfinder } from './AStarPathfinder';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 基本静态网格图与A*一起使用
|
|
7
|
-
* 将walls添加到walls数组,并将加权节点添加到weightedNodes数组
|
|
8
|
-
*/
|
|
9
|
-
export class AstarGridGraph implements IAstarGraph<IVector2> {
|
|
10
|
-
public dirs: IVector2[] = [
|
|
11
|
-
Vector2Utils.create(1, 0),
|
|
12
|
-
Vector2Utils.create(0, -1),
|
|
13
|
-
Vector2Utils.create(-1, 0),
|
|
14
|
-
Vector2Utils.create(0, 1)
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
public walls: IVector2[] = [];
|
|
18
|
-
public weightedNodes: IVector2[] = [];
|
|
19
|
-
public defaultWeight: number = 1;
|
|
20
|
-
public weightedNodeWeight = 5;
|
|
21
|
-
|
|
22
|
-
private _width: number;
|
|
23
|
-
private _height: number;
|
|
24
|
-
private _neighbors: IVector2[] = new Array(4);
|
|
25
|
-
|
|
26
|
-
private _wallsSet: Set<number> = new Set();
|
|
27
|
-
private _weightedNodesSet: Set<number> = new Set();
|
|
28
|
-
private _wallsDirty: boolean = true;
|
|
29
|
-
private _weightedNodesDirty: boolean = true;
|
|
30
|
-
|
|
31
|
-
constructor(width: number, height: number) {
|
|
32
|
-
this._width = width;
|
|
33
|
-
this._height = height;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* 添加障碍物
|
|
38
|
-
*/
|
|
39
|
-
addWall(wall: IVector2): void {
|
|
40
|
-
this.walls.push(wall);
|
|
41
|
-
this._wallsDirty = true;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* 批量添加障碍物
|
|
46
|
-
*/
|
|
47
|
-
addWalls(walls: IVector2[]): void {
|
|
48
|
-
this.walls.push(...walls);
|
|
49
|
-
this._wallsDirty = true;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* 清空障碍物
|
|
54
|
-
*/
|
|
55
|
-
clearWalls(): void {
|
|
56
|
-
this.walls.length = 0;
|
|
57
|
-
this._wallsSet.clear();
|
|
58
|
-
this._wallsDirty = false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 添加加权节点
|
|
63
|
-
*/
|
|
64
|
-
addWeightedNode(node: IVector2): void {
|
|
65
|
-
this.weightedNodes.push(node);
|
|
66
|
-
this._weightedNodesDirty = true;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/**
|
|
70
|
-
* 批量添加加权节点
|
|
71
|
-
*/
|
|
72
|
-
addWeightedNodes(nodes: IVector2[]): void {
|
|
73
|
-
this.weightedNodes.push(...nodes);
|
|
74
|
-
this._weightedNodesDirty = true;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 清空加权节点
|
|
79
|
-
*/
|
|
80
|
-
clearWeightedNodes(): void {
|
|
81
|
-
this.weightedNodes.length = 0;
|
|
82
|
-
this._weightedNodesSet.clear();
|
|
83
|
-
this._weightedNodesDirty = false;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* 更新内部哈希集合
|
|
88
|
-
*/
|
|
89
|
-
private _updateHashSets(): void {
|
|
90
|
-
if (this._wallsDirty) {
|
|
91
|
-
this._wallsSet.clear();
|
|
92
|
-
for (const wall of this.walls) {
|
|
93
|
-
this._wallsSet.add(Vector2Utils.toHash(wall));
|
|
94
|
-
}
|
|
95
|
-
this._wallsDirty = false;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (this._weightedNodesDirty) {
|
|
99
|
-
this._weightedNodesSet.clear();
|
|
100
|
-
for (const node of this.weightedNodes) {
|
|
101
|
-
this._weightedNodesSet.add(Vector2Utils.toHash(node));
|
|
102
|
-
}
|
|
103
|
-
this._weightedNodesDirty = false;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* 确保节点在网格图的边界内
|
|
109
|
-
* @param node
|
|
110
|
-
*/
|
|
111
|
-
public isNodeInBounds(node: IVector2): boolean {
|
|
112
|
-
return 0 <= node.x && node.x < this._width && 0 <= node.y && node.y < this._height;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* 检查节点是否可以通过。walls是不可逾越的。
|
|
117
|
-
* @param node
|
|
118
|
-
*/
|
|
119
|
-
public isNodePassable(node: IVector2): boolean {
|
|
120
|
-
this._updateHashSets();
|
|
121
|
-
return !this._wallsSet.has(Vector2Utils.toHash(node));
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* 检查是否存在路径
|
|
126
|
-
* @param start 起始位置
|
|
127
|
-
* @param goal 目标位置
|
|
128
|
-
*/
|
|
129
|
-
public search(start: IVector2, goal: IVector2): boolean {
|
|
130
|
-
return AStarPathfinder.hasPath(this, start, goal);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
/**
|
|
134
|
-
* 搜索并返回完整路径
|
|
135
|
-
* @param start 起始位置
|
|
136
|
-
* @param goal 目标位置
|
|
137
|
-
*/
|
|
138
|
-
public searchPath(start: IVector2, goal: IVector2): IVector2[] {
|
|
139
|
-
return AStarPathfinder.searchPath(this, start, goal);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
public getNeighbors(node: IVector2): IVector2[] {
|
|
143
|
-
this._neighbors.length = 0;
|
|
144
|
-
|
|
145
|
-
for (const dir of this.dirs) {
|
|
146
|
-
const next = Vector2Utils.add(node, dir);
|
|
147
|
-
if (this.isNodeInBounds(next) && this.isNodePassable(next)) {
|
|
148
|
-
this._neighbors.push(next);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
return this._neighbors;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
public cost(from: IVector2, to: IVector2): number {
|
|
156
|
-
this._updateHashSets();
|
|
157
|
-
return this._weightedNodesSet.has(Vector2Utils.toHash(to)) ? this.weightedNodeWeight : this.defaultWeight;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
public heuristic(node: IVector2, goal: IVector2): number {
|
|
161
|
-
return Vector2Utils.manhattanDistance(node, goal);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* 获取统计信息
|
|
166
|
-
*/
|
|
167
|
-
public getStats(): {
|
|
168
|
-
walls: number;
|
|
169
|
-
weightedNodes: number;
|
|
170
|
-
gridSize: string;
|
|
171
|
-
wallsSetSize: number;
|
|
172
|
-
weightedNodesSetSize: number;
|
|
173
|
-
} {
|
|
174
|
-
this._updateHashSets();
|
|
175
|
-
return {
|
|
176
|
-
walls: this.walls.length,
|
|
177
|
-
weightedNodes: this.weightedNodes.length,
|
|
178
|
-
gridSize: `${this._width}x${this._height}`,
|
|
179
|
-
wallsSetSize: this._wallsSet.size,
|
|
180
|
-
weightedNodesSetSize: this._weightedNodesSet.size
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { IVector2 } from '../../../Types/IVector2';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A*算法图接口
|
|
5
|
-
* @template T 节点类型,通常是实现了IVector2的类型
|
|
6
|
-
*/
|
|
7
|
-
export interface IAstarGraph<T extends IVector2> {
|
|
8
|
-
/**
|
|
9
|
-
* 获取指定节点的邻居节点
|
|
10
|
-
* @param node 当前节点
|
|
11
|
-
* @returns 邻居节点数组
|
|
12
|
-
*/
|
|
13
|
-
getNeighbors(node: T): T[];
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* 计算从一个节点到另一个节点的移动成本
|
|
17
|
-
* @param from 起始节点
|
|
18
|
-
* @param to 目标节点
|
|
19
|
-
* @returns 移动成本
|
|
20
|
-
*/
|
|
21
|
-
cost(from: T, to: T): number;
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 计算启发式函数值(估算从当前节点到目标的成本)
|
|
25
|
-
* @param node 当前节点
|
|
26
|
-
* @param goal 目标节点
|
|
27
|
-
* @returns 启发式成本
|
|
28
|
-
*/
|
|
29
|
-
heuristic(node: T, goal: T): number;
|
|
30
|
-
}
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { IVector2, Vector2Utils } from '../../../Types/IVector2';
|
|
2
|
-
import { IUnweightedGraph } from './IUnweightedGraph';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 广度优先搜索算法实现
|
|
6
|
-
* 适用于无权图,保证找到最短路径(步数最少)
|
|
7
|
-
*/
|
|
8
|
-
export class BreadthFirstPathfinder {
|
|
9
|
-
/**
|
|
10
|
-
* 使用广度优先搜索算法搜索路径
|
|
11
|
-
* @param graph 图对象
|
|
12
|
-
* @param start 起始节点
|
|
13
|
-
* @param goal 目标节点
|
|
14
|
-
* @param cameFrom 可选的路径记录Map
|
|
15
|
-
* @returns 是否找到路径
|
|
16
|
-
*/
|
|
17
|
-
static search<T extends IVector2>(
|
|
18
|
-
graph: IUnweightedGraph<T>,
|
|
19
|
-
start: T,
|
|
20
|
-
goal: T,
|
|
21
|
-
cameFrom?: Map<number, T>
|
|
22
|
-
): boolean {
|
|
23
|
-
const frontier: T[] = [];
|
|
24
|
-
const visited = new Set<number>();
|
|
25
|
-
const pathMap = cameFrom || new Map<number, T>();
|
|
26
|
-
|
|
27
|
-
const startHash = Vector2Utils.toHash(start);
|
|
28
|
-
const goalHash = Vector2Utils.toHash(goal);
|
|
29
|
-
|
|
30
|
-
if (startHash === goalHash) {
|
|
31
|
-
return true;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
frontier.push(start);
|
|
35
|
-
visited.add(startHash);
|
|
36
|
-
|
|
37
|
-
while (frontier.length > 0) {
|
|
38
|
-
const current = frontier.shift()!;
|
|
39
|
-
const currentHash = Vector2Utils.toHash(current);
|
|
40
|
-
|
|
41
|
-
if (currentHash === goalHash) {
|
|
42
|
-
return true;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
for (const neighbor of graph.getNeighbors(current)) {
|
|
46
|
-
const neighborHash = Vector2Utils.toHash(neighbor);
|
|
47
|
-
|
|
48
|
-
if (visited.has(neighborHash)) {
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
visited.add(neighborHash);
|
|
53
|
-
pathMap.set(neighborHash, current);
|
|
54
|
-
frontier.push(neighbor);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* 搜索并返回完整路径
|
|
63
|
-
* @param graph 图对象
|
|
64
|
-
* @param start 起始节点
|
|
65
|
-
* @param goal 目标节点
|
|
66
|
-
* @returns 路径数组,如果没找到则返回空数组
|
|
67
|
-
*/
|
|
68
|
-
static searchPath<T extends IVector2>(
|
|
69
|
-
graph: IUnweightedGraph<T>,
|
|
70
|
-
start: T,
|
|
71
|
-
goal: T
|
|
72
|
-
): T[] {
|
|
73
|
-
const cameFrom = new Map<number, T>();
|
|
74
|
-
|
|
75
|
-
if (this.search(graph, start, goal, cameFrom)) {
|
|
76
|
-
return this.reconstructPath(cameFrom, start, goal);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
return [];
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* 重构路径
|
|
84
|
-
* @param cameFrom 路径记录Map
|
|
85
|
-
* @param start 起始节点
|
|
86
|
-
* @param goal 目标节点
|
|
87
|
-
* @returns 完整路径
|
|
88
|
-
*/
|
|
89
|
-
static reconstructPath<T extends IVector2>(
|
|
90
|
-
cameFrom: Map<number, T>,
|
|
91
|
-
start: T,
|
|
92
|
-
goal: T
|
|
93
|
-
): T[] {
|
|
94
|
-
const path: T[] = [];
|
|
95
|
-
let current = goal;
|
|
96
|
-
const startHash = Vector2Utils.toHash(start);
|
|
97
|
-
|
|
98
|
-
while (Vector2Utils.toHash(current) !== startHash) {
|
|
99
|
-
path.unshift(current);
|
|
100
|
-
const currentHash = Vector2Utils.toHash(current);
|
|
101
|
-
const parent = cameFrom.get(currentHash);
|
|
102
|
-
if (!parent) break;
|
|
103
|
-
current = parent;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
path.unshift(start);
|
|
107
|
-
return path;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { IVector2 } from '../../../Types/IVector2';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* 无权图接口,用于广度优先搜索
|
|
5
|
-
* @template T 节点类型,通常是实现了IVector2的类型
|
|
6
|
-
*/
|
|
7
|
-
export interface IUnweightedGraph<T extends IVector2> {
|
|
8
|
-
/**
|
|
9
|
-
* 获取指定节点的邻居节点
|
|
10
|
-
* @param node 当前节点
|
|
11
|
-
* @returns 邻居节点数组
|
|
12
|
-
*/
|
|
13
|
-
getNeighbors(node: T): T[];
|
|
14
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { IVector2 } from '../../../Types/IVector2';
|
|
2
|
-
import { IUnweightedGraph } from './IUnweightedGraph';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* 一个未加权图的基本实现。所有的边都被缓存。这种类型的图最适合于非基于网格的图。
|
|
6
|
-
* 作为边添加的任何节点都必须在边字典中有一个条目作为键。
|
|
7
|
-
*/
|
|
8
|
-
export class UnweightedGraph<T extends IVector2> implements IUnweightedGraph<T> {
|
|
9
|
-
public edges: Map<T, T[]> = new Map<T, T[]>();
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* 为指定节点添加边
|
|
13
|
-
* @param node 节点
|
|
14
|
-
* @param neighbors 邻居节点数组
|
|
15
|
-
*/
|
|
16
|
-
addEdgesForNode(node: T, neighbors: T[]): this {
|
|
17
|
-
this.edges.set(node, neighbors);
|
|
18
|
-
return this;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 获取指定节点的邻居节点
|
|
23
|
-
* @param node 节点
|
|
24
|
-
* @returns 邻居节点数组
|
|
25
|
-
*/
|
|
26
|
-
getNeighbors(node: T): T[] {
|
|
27
|
-
return this.edges.get(node) || [];
|
|
28
|
-
}
|
|
29
|
-
}
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { IVector2, Vector2Utils } from '../../../Types/IVector2';
|
|
2
|
-
import { IUnweightedGraph } from './IUnweightedGraph';
|
|
3
|
-
import { BreadthFirstPathfinder } from './BreadthFirstPathfinder';
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 基本的无权网格图,用于广度优先搜索
|
|
7
|
-
* 适用于简单的网格寻路,如迷宫、推箱子等游戏
|
|
8
|
-
*/
|
|
9
|
-
export class UnweightedGridGraph implements IUnweightedGraph<IVector2> {
|
|
10
|
-
private static readonly CARDINAL_DIRS: IVector2[] = [
|
|
11
|
-
Vector2Utils.create(1, 0),
|
|
12
|
-
Vector2Utils.create(0, -1),
|
|
13
|
-
Vector2Utils.create(-1, 0),
|
|
14
|
-
Vector2Utils.create(0, 1)
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
private static readonly COMPASS_DIRS: IVector2[] = [
|
|
18
|
-
Vector2Utils.create(1, 0),
|
|
19
|
-
Vector2Utils.create(1, -1),
|
|
20
|
-
Vector2Utils.create(0, -1),
|
|
21
|
-
Vector2Utils.create(-1, -1),
|
|
22
|
-
Vector2Utils.create(-1, 0),
|
|
23
|
-
Vector2Utils.create(-1, 1),
|
|
24
|
-
Vector2Utils.create(0, 1),
|
|
25
|
-
Vector2Utils.create(1, 1),
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
public walls: IVector2[] = [];
|
|
29
|
-
|
|
30
|
-
private _width: number;
|
|
31
|
-
private _height: number;
|
|
32
|
-
private _dirs: IVector2[];
|
|
33
|
-
private _neighbors: IVector2[] = [];
|
|
34
|
-
|
|
35
|
-
constructor(width: number, height: number, allowDiagonalSearch: boolean = false) {
|
|
36
|
-
this._width = width;
|
|
37
|
-
this._height = height;
|
|
38
|
-
this._dirs = allowDiagonalSearch ? UnweightedGridGraph.COMPASS_DIRS : UnweightedGridGraph.CARDINAL_DIRS;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
public isNodeInBounds(node: IVector2): boolean {
|
|
42
|
-
return 0 <= node.x && node.x < this._width && 0 <= node.y && node.y < this._height;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
public isNodePassable(node: IVector2): boolean {
|
|
46
|
-
return !this.walls.find(wall => Vector2Utils.equals(wall, node));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
public getNeighbors(node: IVector2): IVector2[] {
|
|
50
|
-
this._neighbors.length = 0;
|
|
51
|
-
|
|
52
|
-
for (const dir of this._dirs) {
|
|
53
|
-
const next = Vector2Utils.add(node, dir);
|
|
54
|
-
if (this.isNodeInBounds(next) && this.isNodePassable(next)) {
|
|
55
|
-
this._neighbors.push(next);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
return this._neighbors;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* 搜索路径的便捷方法
|
|
64
|
-
* @param start 起始位置
|
|
65
|
-
* @param goal 目标位置
|
|
66
|
-
* @returns 路径数组,如果没找到则返回空数组
|
|
67
|
-
*/
|
|
68
|
-
public searchPath(start: IVector2, goal: IVector2): IVector2[] {
|
|
69
|
-
return BreadthFirstPathfinder.searchPath(this, start, goal);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* 检查是否存在路径
|
|
74
|
-
* @param start 起始位置
|
|
75
|
-
* @param goal 目标位置
|
|
76
|
-
* @returns 是否存在路径
|
|
77
|
-
*/
|
|
78
|
-
public hasPath(start: IVector2, goal: IVector2): boolean {
|
|
79
|
-
return BreadthFirstPathfinder.search(this, start, goal);
|
|
80
|
-
}
|
|
81
|
-
}
|