@cc-component/cc-pathfinding 1.0.1
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/.cc-ex-component.md +12 -0
- package/assets/pathfinding/Interface.ts +73 -0
- package/assets/pathfinding/Interface.ts.meta +9 -0
- package/assets/pathfinding/base/AStarPathfinder.ts +264 -0
- package/assets/pathfinding/base/AStarPathfinder.ts.meta +9 -0
- package/assets/pathfinding/base/Common.ts +342 -0
- package/assets/pathfinding/base/Common.ts.meta +9 -0
- package/assets/pathfinding/base/GridMap.ts +314 -0
- package/assets/pathfinding/base/GridMap.ts.meta +9 -0
- package/assets/pathfinding/base/GridMovementController.ts +257 -0
- package/assets/pathfinding/base/GridMovementController.ts.meta +9 -0
- package/assets/pathfinding/base/IMapData.ts +13 -0
- package/assets/pathfinding/base/IMapData.ts.meta +9 -0
- package/assets/pathfinding/base/IPlayerBuff.ts +1 -0
- package/assets/pathfinding/base/IPlayerBuff.ts.meta +9 -0
- package/assets/pathfinding/base/MapBoxView.ts +386 -0
- package/assets/pathfinding/base/MapBoxView.ts.meta +9 -0
- package/assets/pathfinding/base/MapViewModel.ts +17 -0
- package/assets/pathfinding/base/MapViewModel.ts.meta +9 -0
- package/assets/pathfinding/base/Mapitem.ts +38 -0
- package/assets/pathfinding/base/Mapitem.ts.meta +9 -0
- package/assets/pathfinding/base/PathGridStateManager.ts +94 -0
- package/assets/pathfinding/base/PathGridStateManager.ts.meta +9 -0
- package/assets/pathfinding/base/PathfindingModule.ts +161 -0
- package/assets/pathfinding/base/PathfindingModule.ts.meta +9 -0
- package/assets/pathfinding/base/prefab/map_root.prefab +16290 -0
- package/assets/pathfinding/base/prefab/map_root.prefab.meta +13 -0
- package/assets/pathfinding/base/prefab.meta +9 -0
- package/assets/pathfinding/base.meta +9 -0
- package/assets/pathfinding.meta +9 -0
- package/assets.meta +9 -0
- package/index.ts +9 -0
- package/index.ts.meta +9 -0
- package/package.json +19 -0
- package/package.json.meta +11 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { Component, Node, Tween, tween } from 'cc';
|
|
2
|
+
import { AttackDistance } from './Common';
|
|
3
|
+
import { Cell } from './GridMap';
|
|
4
|
+
import { PathGridStateManager } from './PathGridStateManager';
|
|
5
|
+
import { PathfindingModule } from './PathfindingModule';
|
|
6
|
+
|
|
7
|
+
export interface GridMovementOptions {
|
|
8
|
+
/** 每次重试前的等待时间,单位秒 */
|
|
9
|
+
retryDelay?: number;
|
|
10
|
+
/** 单格移动动画时长,单位秒 */
|
|
11
|
+
moveDuration?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 统一管理单位移动、格子占用状态、重试寻路和移动版本控制。
|
|
16
|
+
* 负责把节点位置、临时占用、动画回调串起来,避免多个单位相互污染同一张地图状态。
|
|
17
|
+
*/
|
|
18
|
+
export class GridMovementController {
|
|
19
|
+
private readonly movementVersions = new Map<Node, number>();
|
|
20
|
+
private readonly movementOwners = new Map<Node, symbol>();
|
|
21
|
+
private readonly movementRetryCallbacks = new Map<Node, () => void>();
|
|
22
|
+
private readonly positions = new Map<Node, Cell>();
|
|
23
|
+
private readonly retryDelay: number;
|
|
24
|
+
private readonly moveDuration: number;
|
|
25
|
+
private readonly pathState: PathGridStateManager;
|
|
26
|
+
|
|
27
|
+
constructor(private readonly scheduler: Component, options: GridMovementOptions = {}) {
|
|
28
|
+
// 允许外部按场景调整重试间隔和动画时长,默认值保持兼容。
|
|
29
|
+
this.retryDelay = options.retryDelay ?? 1;
|
|
30
|
+
this.moveDuration = options.moveDuration ?? 0.5;
|
|
31
|
+
this.pathState = new PathGridStateManager({
|
|
32
|
+
isOccupied: (row, col) => PathfindingModule.IsOccupied(row, col),
|
|
33
|
+
occupy: (row, col) => PathfindingModule.OccupyNode(row, col, true),
|
|
34
|
+
free: (row, col) => PathfindingModule.FreeNode(row, col),
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** 设定单位当前格子,并同步到节点世界坐标。 */
|
|
39
|
+
setPosition(playerNode: Node, position: Cell): void {
|
|
40
|
+
this.positions.set(playerNode, { row: position.row, col: position.col });
|
|
41
|
+
playerNode.worldPosition = PathfindingModule.GetMapPosition(position.row, position.col);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 获取单位当前格子,未记录时返回默认起点。 */
|
|
45
|
+
getPosition(playerNode: Node): Cell {
|
|
46
|
+
return this.positions.get(playerNode) ?? { row: 0, col: 0 };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 开始移动到目标格子。
|
|
51
|
+
* 1. 先取消旧移动和重试。
|
|
52
|
+
* 2. 计算路径,路径无效则延时重试。
|
|
53
|
+
* 3. 成功后进入逐格动画。
|
|
54
|
+
*/
|
|
55
|
+
moveTo(playerNode: Node, target: Cell, attackDistance: AttackDistance, openColor: boolean, callBack?: () => void): void {
|
|
56
|
+
const start = this.getPosition(playerNode);
|
|
57
|
+
if (start.row === target.row && start.col === target.col) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
this.cancel(playerNode, true);
|
|
62
|
+
const route = PathfindingModule.TracePath(start, target, attackDistance, openColor);
|
|
63
|
+
if (!route || route.length === 0 || this.isPathBlocked(start, target, route, attackDistance)) {
|
|
64
|
+
this.retryPathfinding(playerNode, target, attackDistance, openColor, callBack);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.clearPathfindingRetry(playerNode);
|
|
69
|
+
route.shift();
|
|
70
|
+
this.startMovement(playerNode, route, start, target, attackDistance, openColor, callBack);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** 强制中止当前移动,可选择保留当前格子的临时占用。 */
|
|
74
|
+
cancel(playerNode: Node, preserveCurrent = false): void {
|
|
75
|
+
Tween.stopAllByTarget(playerNode);
|
|
76
|
+
this.clearPathfindingRetry(playerNode);
|
|
77
|
+
this.clearMovementRetry(playerNode);
|
|
78
|
+
|
|
79
|
+
const owner = this.movementOwners.get(playerNode);
|
|
80
|
+
if (owner) {
|
|
81
|
+
this.pathState.release(owner);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (preserveCurrent) {
|
|
85
|
+
const current = this.getPosition(playerNode);
|
|
86
|
+
const currentOwner = Symbol(playerNode.name);
|
|
87
|
+
this.movementOwners.set(playerNode, currentOwner);
|
|
88
|
+
this.pathState.reserveCurrent(current, currentOwner);
|
|
89
|
+
} else {
|
|
90
|
+
this.movementOwners.delete(playerNode);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
this.bumpVersion(playerNode);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 销毁控制器,并清理所有单位的动画、重试和格子占用状态。 */
|
|
97
|
+
destroy(): void {
|
|
98
|
+
this.movementVersions.clear();
|
|
99
|
+
this.movementOwners.forEach((owner, playerNode) => {
|
|
100
|
+
this.pathState.release(owner);
|
|
101
|
+
Tween.stopAllByTarget(playerNode);
|
|
102
|
+
});
|
|
103
|
+
this.movementOwners.clear();
|
|
104
|
+
|
|
105
|
+
for (const playerNode of this.movementRetryCallbacks.keys()) {
|
|
106
|
+
this.clearPathfindingRetry(playerNode);
|
|
107
|
+
this.clearMovementRetry(playerNode);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
this.positions.clear();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 执行逐格移动。
|
|
115
|
+
* 每次移动前会尝试保留下一格,失败说明被占用,就重新规划路径或延时重试。
|
|
116
|
+
*/
|
|
117
|
+
private startMovement(playerNode: Node, route: Cell[], start: Cell, target: Cell, attackDistance: AttackDistance, openColor: boolean, callBack?: () => void): void {
|
|
118
|
+
const previousOwner = this.movementOwners.get(playerNode);
|
|
119
|
+
if (previousOwner) {
|
|
120
|
+
this.pathState.release(previousOwner);
|
|
121
|
+
}
|
|
122
|
+
const owner = Symbol(playerNode.name);
|
|
123
|
+
this.movementOwners.set(playerNode, owner);
|
|
124
|
+
this.pathState.reserveCurrent(start, owner);
|
|
125
|
+
const version = this.bumpVersion(playerNode);
|
|
126
|
+
let previous = start;
|
|
127
|
+
let index = 0;
|
|
128
|
+
|
|
129
|
+
const reachedAttackDistance = () => this.isAttackDistance(previous, target, attackDistance);
|
|
130
|
+
// 当前格子/目标格子无法继续前进时,重新尝试路径计算或等待下一轮继续。
|
|
131
|
+
const retry = () => {
|
|
132
|
+
this.clearMovementRetry(playerNode);
|
|
133
|
+
if (route.length === 0) {
|
|
134
|
+
const retryRoute = PathfindingModule.TracePath(previous, target, attackDistance, openColor);
|
|
135
|
+
if (retryRoute.length <= 1) {
|
|
136
|
+
if (reachedAttackDistance()) {
|
|
137
|
+
moveNext();
|
|
138
|
+
} else {
|
|
139
|
+
this.scheduleMovementRetry(playerNode, retry);
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
retryRoute.shift();
|
|
144
|
+
route.push(...retryRoute);
|
|
145
|
+
index = 0;
|
|
146
|
+
}
|
|
147
|
+
moveNext();
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const scheduleRetry = () => this.scheduleMovementRetry(playerNode, retry);
|
|
151
|
+
|
|
152
|
+
const moveNext = () => {
|
|
153
|
+
if (version !== this.movementVersions.get(playerNode)) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (index >= route.length) {
|
|
157
|
+
if (!reachedAttackDistance()) {
|
|
158
|
+
route.length = 0;
|
|
159
|
+
scheduleRetry();
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
this.clearMovementRetry(playerNode);
|
|
163
|
+
callBack?.();
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const next = route[index];
|
|
168
|
+
// 只要下一格被占用,就不继续移动,重新规划路线,避免穿透同一格子。
|
|
169
|
+
if (!this.pathState.tryReserve(previous, next, owner)) {
|
|
170
|
+
this.pathState.release(owner);
|
|
171
|
+
PathfindingModule.OccupyNode(previous.row, previous.col, true);
|
|
172
|
+
const newRoute = PathfindingModule.TracePath(previous, target, attackDistance, openColor);
|
|
173
|
+
if (newRoute.length > 1) {
|
|
174
|
+
newRoute.shift();
|
|
175
|
+
route.splice(0, route.length, ...newRoute);
|
|
176
|
+
index = 0;
|
|
177
|
+
} else if (reachedAttackDistance()) {
|
|
178
|
+
callBack?.();
|
|
179
|
+
return;
|
|
180
|
+
} else {
|
|
181
|
+
route.length = 0;
|
|
182
|
+
}
|
|
183
|
+
scheduleRetry();
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
tween(playerNode)
|
|
188
|
+
.to(this.moveDuration, { worldPosition: PathfindingModule.GetMapPosition(next.row, next.col) })
|
|
189
|
+
.call(() => {
|
|
190
|
+
this.pathState.completeMove(previous, next, owner);
|
|
191
|
+
this.positions.set(playerNode, { row: next.row, col: next.col });
|
|
192
|
+
previous = next;
|
|
193
|
+
index++;
|
|
194
|
+
moveNext();
|
|
195
|
+
})
|
|
196
|
+
.start();
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
moveNext();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** 路径规划失败时,做一次延迟重试,避免因为瞬时占用导致立刻失败。 */
|
|
203
|
+
private retryPathfinding(playerNode: Node, target: Cell, attackDistance: AttackDistance, openColor: boolean, callBack?: () => void): void {
|
|
204
|
+
this.clearPathfindingRetry(playerNode);
|
|
205
|
+
const retry = () => {
|
|
206
|
+
this.clearPathfindingRetry(playerNode);
|
|
207
|
+
this.moveTo(playerNode, target, attackDistance, openColor, callBack);
|
|
208
|
+
};
|
|
209
|
+
this.movementRetryCallbacks.set(playerNode, retry);
|
|
210
|
+
this.scheduler.scheduleOnce(retry, this.retryDelay);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 移动过程中的中断重试,通常用于路径被占用或目标暂时不可达。 */
|
|
214
|
+
private scheduleMovementRetry(playerNode: Node, retry: () => void): void {
|
|
215
|
+
this.clearMovementRetry(playerNode);
|
|
216
|
+
this.movementRetryCallbacks.set(playerNode, retry);
|
|
217
|
+
this.scheduler.scheduleOnce(retry, this.retryDelay);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
private clearPathfindingRetry(playerNode: Node): void {
|
|
221
|
+
const retry = this.movementRetryCallbacks.get(playerNode);
|
|
222
|
+
if (retry) {
|
|
223
|
+
this.scheduler.unschedule(retry);
|
|
224
|
+
this.movementRetryCallbacks.delete(playerNode);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private clearMovementRetry(playerNode: Node): void {
|
|
229
|
+
const retry = this.movementRetryCallbacks.get(playerNode);
|
|
230
|
+
if (retry) {
|
|
231
|
+
this.scheduler.unschedule(retry);
|
|
232
|
+
this.movementRetryCallbacks.delete(playerNode);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** 每次启动/取消移动时更新版本号,旧动画和旧重试都会被忽略。 */
|
|
237
|
+
private bumpVersion(playerNode: Node): number {
|
|
238
|
+
const version = (this.movementVersions.get(playerNode) ?? 0) + 1;
|
|
239
|
+
this.movementVersions.set(playerNode, version);
|
|
240
|
+
return version;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private isPathBlocked(start: Cell, target: Cell, route: Cell[], attackDistance: AttackDistance): boolean {
|
|
244
|
+
if (route.length !== 1) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
return !this.isAttackDistance(start, target, attackDistance);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private isAttackDistance(start: Cell, target: Cell, attackDistance: AttackDistance): boolean {
|
|
251
|
+
return PathfindingModule.IsAttackDistance(
|
|
252
|
+
PathfindingModule.GetMapModel(start.row, start.col),
|
|
253
|
+
PathfindingModule.GetMapModel(target.row, target.col),
|
|
254
|
+
attackDistance,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
import { _decorator, Component, UITransform, Vec3, v3 } from 'cc';
|
|
2
|
+
import { MapViewModel } from './MapViewModel';
|
|
3
|
+
import { Sprite } from 'cc';
|
|
4
|
+
import { Color } from 'cc';
|
|
5
|
+
import { AttackDistance } from './Common';
|
|
6
|
+
import { Mapitem } from './Mapitem';
|
|
7
|
+
import { instantiate } from 'cc';
|
|
8
|
+
import { Cell, GridMap } from './GridMap';
|
|
9
|
+
import { AStarPathfinder } from './AStarPathfinder';
|
|
10
|
+
const { ccclass, property } = _decorator;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* 地图格子视图层
|
|
14
|
+
*
|
|
15
|
+
* 三层分离架构中的视图层,只负责:
|
|
16
|
+
* 1. 生命周期与地图初始化(格子创建、蜂窝/方格布局)
|
|
17
|
+
* 2. 颜色渲染(占用红 / 路径绿 / 起点黄 / 终点蓝)
|
|
18
|
+
* 3. 对外业务编排(TracePath 等,算法与数据全部委托给逻辑层)
|
|
19
|
+
*
|
|
20
|
+
* 逻辑层:
|
|
21
|
+
* - GridMap 网格数据 / 占用 / 方向 / 距离 / 包围判断
|
|
22
|
+
* - AStarPathfinder A* 寻路算法 / 攻击位查找
|
|
23
|
+
*/
|
|
24
|
+
@ccclass('MapBoxView')
|
|
25
|
+
export class MapBoxView extends Component {
|
|
26
|
+
|
|
27
|
+
// ==================== 1. 编辑器属性 ====================
|
|
28
|
+
|
|
29
|
+
@property({ type: Mapitem })
|
|
30
|
+
mapItem: Mapitem;
|
|
31
|
+
@property()
|
|
32
|
+
row = 6;
|
|
33
|
+
@property()
|
|
34
|
+
col = 6;
|
|
35
|
+
@property({ displayName: "顶部偏移位置" })
|
|
36
|
+
topStart = 50
|
|
37
|
+
/** 六方向模式:开启后按蜂窝规则(左上/左/左下/右上/右/右下)移动、寻路、判包围 */
|
|
38
|
+
@property({ tooltip: '开启后按蜂窝(六方向)规则寻路:左上/左/左下/右上/右/右下' })
|
|
39
|
+
useSixDirection = true
|
|
40
|
+
|
|
41
|
+
// ==================== 2. 内部数据 ====================
|
|
42
|
+
|
|
43
|
+
seat_model_list: MapViewModel[][] = [];
|
|
44
|
+
item_width = 125;
|
|
45
|
+
item_height = 110;
|
|
46
|
+
content_tf: UITransform;
|
|
47
|
+
|
|
48
|
+
/** 逻辑层:网格数据 + 寻路算法 */
|
|
49
|
+
grid: GridMap;
|
|
50
|
+
pathfinder: AStarPathfinder;
|
|
51
|
+
private colorVisible = true;
|
|
52
|
+
|
|
53
|
+
protected onLoad(): void {
|
|
54
|
+
this.content_tf = this.node.getComponent(UITransform);
|
|
55
|
+
this.grid = new GridMap(this.row, this.col, this.useSixDirection);
|
|
56
|
+
this.pathfinder = new AStarPathfinder(this.grid);
|
|
57
|
+
PathfindingModule.Init(this);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
onDestroy() {
|
|
61
|
+
PathfindingModule.Destroy();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ==================== 3. 视图:地图初始化 ====================
|
|
65
|
+
|
|
66
|
+
/** 创建整张地图(六方向模式:奇数行右移半格的蜂窝交错布局) */
|
|
67
|
+
InitView() {
|
|
68
|
+
this.node.removeAllChildren();
|
|
69
|
+
this.item_width = this.mapItem.node.getComponent(UITransform).contentSize.width;
|
|
70
|
+
this.item_height = this.mapItem.node.getComponent(UITransform).contentSize.height;
|
|
71
|
+
|
|
72
|
+
const start_x = this.content_tf.contentSize.width / 2
|
|
73
|
+
const start_y = this.content_tf.contentSize.height / 2
|
|
74
|
+
const space_x = this.item_width / 2;
|
|
75
|
+
const space_y = this.item_height / 2;
|
|
76
|
+
|
|
77
|
+
const padding_left = this.item_height / 2
|
|
78
|
+
let top = 0
|
|
79
|
+
|
|
80
|
+
for (let i = 0; i < this.row; i++) {
|
|
81
|
+
this.seat_model_list[i] = [];
|
|
82
|
+
top += i > 0 ? this.topStart : 0
|
|
83
|
+
for (let j = 0; j < this.col; j++) {
|
|
84
|
+
const model = new MapViewModel();
|
|
85
|
+
model.row = i;
|
|
86
|
+
model.col = j;
|
|
87
|
+
const node = instantiate(this.mapItem.node);
|
|
88
|
+
this.node.addChild(node);
|
|
89
|
+
node.name = `${i}_${j}`
|
|
90
|
+
const mapItem = node.getComponent(Mapitem);
|
|
91
|
+
mapItem.row = i
|
|
92
|
+
mapItem.col = j
|
|
93
|
+
|
|
94
|
+
const pos = v3(
|
|
95
|
+
this.item_width * j + space_x + (i % 2 === 1 ? (this.useSixDirection ? padding_left : 0) : 0) - start_x,
|
|
96
|
+
-(this.item_height * i + space_y - (this.useSixDirection ? top : 0)) + start_y,
|
|
97
|
+
0);
|
|
98
|
+
model.position = this.content_tf.convertToWorldSpaceAR(pos);
|
|
99
|
+
model.bottom_positon = v3(model.position.x, model.position.y - this.item_height / 2, 0);
|
|
100
|
+
|
|
101
|
+
node.worldPosition = model.position;
|
|
102
|
+
|
|
103
|
+
model.node = node;
|
|
104
|
+
model.node_sp = mapItem.bg
|
|
105
|
+
model.node_chose_sp = mapItem.bgHighlight
|
|
106
|
+
this.seat_model_list[i][j] = model;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ==================== 4. 视图:地图数据查询 ====================
|
|
112
|
+
|
|
113
|
+
GetMapModel(row: number, col: number): MapViewModel {
|
|
114
|
+
return this.seat_model_list[row][col];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
GetMapPosition(row: number, col: number): Vec3 {
|
|
118
|
+
return this.seat_model_list[row][col].position;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
GetSeatModelList() {
|
|
122
|
+
return this.seat_model_list;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ==================== 5. 逻辑委托:距离 / 方向 ====================
|
|
126
|
+
|
|
127
|
+
/** 两格之间的距离(八方向:切比雪夫,命名保留兼容) */
|
|
128
|
+
getManhattanDistance(model1: MapViewModel, model2: MapViewModel): number {
|
|
129
|
+
return this.grid.getChebyshevDistance(model1, model2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** 是否在攻击距离内 */
|
|
133
|
+
IsAttackDistance(model1: MapViewModel, model2: MapViewModel, maxDistance: number): boolean {
|
|
134
|
+
return this.grid.getChebyshevDistance(model1, model2) <= maxDistance;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** 获取从 row 行出发的移动方向偏移(六方向蜂窝 / 八方向) */
|
|
138
|
+
getDirections(row: number): [number, number][] {
|
|
139
|
+
return this.grid.getDirections(row);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** 判断两个格子是否相邻(按当前模式的移动规则) */
|
|
143
|
+
isNeighborCell(a: Cell, b: Cell): boolean {
|
|
144
|
+
return this.grid.isNeighborCell(a, b);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ==================== 6. 逻辑委托:占用(含高亮渲染) ====================
|
|
148
|
+
|
|
149
|
+
isOccupied(row: number, col: number): boolean {
|
|
150
|
+
return this.grid.isOccupied(row, col);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
occupyNode(row: number, col: number, openColor?: boolean) {
|
|
154
|
+
this.grid.occupy(row, col);
|
|
155
|
+
const seatModel = this.GetMapModel(row, col);
|
|
156
|
+
if (openColor && this.colorVisible) {
|
|
157
|
+
seatModel.node_chose_sp.color = Color.RED;
|
|
158
|
+
seatModel.node_chose_sp.node.active = true
|
|
159
|
+
} else {
|
|
160
|
+
seatModel.node_chose_sp.color = Color.WHITE;
|
|
161
|
+
seatModel.node_chose_sp.node.active = false
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** row/col < 0 时释放全部 */
|
|
166
|
+
freeNode(row: number, col: number) {
|
|
167
|
+
if (row < 0 || col < 0) {
|
|
168
|
+
this.grid.freeAll();
|
|
169
|
+
this.clearColorAll();
|
|
170
|
+
} else {
|
|
171
|
+
this.grid.free(row, col);
|
|
172
|
+
this.clearColortarget(row, col);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
GetOccupyNodeNot() {
|
|
177
|
+
return this.grid.getOccupiedSet();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ==================== 7. 逻辑委托:寻路 / 攻击位 / 包围 ====================
|
|
181
|
+
|
|
182
|
+
/** A* 寻路:返回起点到终点的路径(含起终点),找不到返回空数组 */
|
|
183
|
+
findPath(startRow: number, startCol: number, endRow: number, endCol: number): Cell[] {
|
|
184
|
+
return this.pathfinder.findPath({ row: startRow, col: startCol }, { row: endRow, col: endCol });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* 找到"从 start 出发、能走到 end 身边"的路径:
|
|
189
|
+
* 动态扩大搜索半径,找 end 周围可站且 A* 可达的最近格子。
|
|
190
|
+
*/
|
|
191
|
+
FindPathPoint(start: Cell, end: Cell, maxDistance: number): Cell[] {
|
|
192
|
+
return this.pathfinder.findPathToNearby(start, end, maxDistance);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** 判断目标点是否被包围 */
|
|
196
|
+
isClosedLoop(targetRow: number, targetCol: number, maxDistance: number): boolean {
|
|
197
|
+
return this.grid.isClosedLoop(targetRow, targetCol, maxDistance);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** 检查目标周围的点(不判断占用,仅范围内所有点) */
|
|
201
|
+
checkAvailableSurrounding(targetRow: number, targetCol: number, maxDistance: number): Cell[] {
|
|
202
|
+
return this.grid.checkAvailableSurrounding(targetRow, targetCol, maxDistance);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// ==================== 8. 对外业务编排 ====================
|
|
206
|
+
|
|
207
|
+
/** 寻路并显示路径,返回路径上的 MapViewModel 列表 */
|
|
208
|
+
FindRote(start: Cell, end: Cell) {
|
|
209
|
+
const path = this.findPath(start.row, start.col, end.row, end.col);
|
|
210
|
+
const seat_model_list: MapViewModel[] = []
|
|
211
|
+
if (path.length > 0) {
|
|
212
|
+
path.forEach((node, index) => {
|
|
213
|
+
const seatModel = this.GetMapModel(node.row, node.col);
|
|
214
|
+
seatModel.node_chose_sp.color = Color.GREEN;
|
|
215
|
+
// 起点标记为黄色
|
|
216
|
+
if (index === 0) {
|
|
217
|
+
seatModel.node_chose_sp.color = Color.YELLOW;
|
|
218
|
+
}
|
|
219
|
+
seat_model_list.push(seatModel)
|
|
220
|
+
});
|
|
221
|
+
} else {
|
|
222
|
+
console.log('No path found');
|
|
223
|
+
}
|
|
224
|
+
return seat_model_list
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 计算路径:只负责寻找起点到终点/附近可站点的路径,不处理渲染 */
|
|
228
|
+
private computeTracePath(start: Cell, end: Cell, attackDis: AttackDistance): Cell[] {
|
|
229
|
+
// 起点即终点
|
|
230
|
+
if (start.row === end.row && start.col === end.col) {
|
|
231
|
+
return [start]
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let list = this.pathfinder.findPathToNearby(start, end, 1);
|
|
235
|
+
|
|
236
|
+
// 如果只有 2 个点(起点 + 终点),处理"很近"的边界情况
|
|
237
|
+
if (list.length === 2) {
|
|
238
|
+
if (this.grid.useSixDirection) {
|
|
239
|
+
// 六方向:相邻时终点保留为终点
|
|
240
|
+
if (this.grid.isNeighborCell(start, end)) {
|
|
241
|
+
list = list.slice(0, 1)
|
|
242
|
+
list.push({ row: end.row, col: end.col })
|
|
243
|
+
}
|
|
244
|
+
} else {
|
|
245
|
+
// 原逻辑:攻击距离为 0 且终点被占用时,去掉终点
|
|
246
|
+
const last = list[list.length - 1]
|
|
247
|
+
if (attackDis == AttackDistance.attack_0 && this.grid.isOccupied(last.row, last.col)) {
|
|
248
|
+
list.pop()
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 没有结束点就补上结束点
|
|
254
|
+
const hasEnd = list.some((element) => element.row == end.row && element.col == end.col);
|
|
255
|
+
if (!hasEnd) {
|
|
256
|
+
list.push({ row: end.row, col: end.col })
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return list;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** 根据攻击距离裁剪路径,只返回真实可走到的有效段,不处理渲染 */
|
|
263
|
+
private trimPathByAttackDistance(path: Cell[], attackDis: AttackDistance): Cell[] {
|
|
264
|
+
if (path.length === 0) {
|
|
265
|
+
return path;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
let index = 0;
|
|
269
|
+
if (attackDis > path.length) {
|
|
270
|
+
// 攻击距离覆盖整条路径:站在原地就能打到
|
|
271
|
+
index = 0;
|
|
272
|
+
} else {
|
|
273
|
+
// 截断到攻击距离处
|
|
274
|
+
index = path.length - 1 - attackDis
|
|
275
|
+
// 末位不可站就退后一格,站到真实可达的最近点上:
|
|
276
|
+
// 1. 末端两点不连续 → 末尾是补上去的“假终点”(目标被围住,走不到)
|
|
277
|
+
// 2. 末位被占用 → 终点被占(敌人站在终点上),不能踩上去
|
|
278
|
+
if (attackDis <= 0 && path.length >= 2) {
|
|
279
|
+
const last = path[path.length - 1];
|
|
280
|
+
const isContinuous = this.grid.isNeighborCell(path[path.length - 2], last);
|
|
281
|
+
if (!isContinuous || this.grid.isOccupied(last.row, last.col)) {
|
|
282
|
+
index = path.length - 2;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return path.filter((_, i) => i <= index);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** 只负责渲染路径颜色,不处理搜索与裁剪逻辑 */
|
|
291
|
+
private renderPath(path: Cell[], openColor?: boolean): void {
|
|
292
|
+
if (!path || path.length === 0 || !this.colorVisible) {
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (openColor) {
|
|
297
|
+
const stopCell = path[path.length - 1];
|
|
298
|
+
const stopModel = this.GetMapModel(stopCell.row, stopCell.col);
|
|
299
|
+
stopModel.node.children[1].getComponent(Sprite).color = Color.YELLOW;
|
|
300
|
+
}
|
|
301
|
+
path.forEach((model, index) => {
|
|
302
|
+
const seatModel = this.GetMapModel(model.row, model.col);
|
|
303
|
+
seatModel.node_chose_sp.color = Color.GREEN;
|
|
304
|
+
if (openColor) {
|
|
305
|
+
seatModel.node_chose_sp.node.active = true
|
|
306
|
+
}
|
|
307
|
+
if (index === path.length - 1) {
|
|
308
|
+
seatModel.node_chose_sp.color = Color.BLUE;
|
|
309
|
+
}
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* 寻路主入口:从 start 走到 end 身边的攻击位。
|
|
317
|
+
* - attackDis:攻击距离,用于截断路径(只需走到攻击范围内即可)
|
|
318
|
+
* - openColor:是否高亮显示
|
|
319
|
+
*/
|
|
320
|
+
TracePath(start: Cell, end: Cell, attackDis: AttackDistance, openColor?: boolean): Cell[] {
|
|
321
|
+
if (!openColor) {
|
|
322
|
+
this.ClearColor()
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const path = this.computeTracePath(start, end, attackDis);
|
|
326
|
+
const validPath = this.trimPathByAttackDistance(path, attackDis);
|
|
327
|
+
this.renderPath(validPath, openColor);
|
|
328
|
+
|
|
329
|
+
return validPath;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ==================== 9. 显示辅助 ====================
|
|
333
|
+
|
|
334
|
+
/** 开启或关闭所有格子的高亮颜色 */
|
|
335
|
+
SetColorVisible(visible: boolean) {
|
|
336
|
+
this.colorVisible = visible;
|
|
337
|
+
this.seat_model_list.forEach(list => {
|
|
338
|
+
list.forEach(item => {
|
|
339
|
+
if (!this.grid.isOccupied(item.row, item.col)) {
|
|
340
|
+
item.node_chose_sp.node.active = visible;
|
|
341
|
+
if (!visible) {
|
|
342
|
+
item.node_chose_sp.color = Color.WHITE;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
})
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/** 清除路径颜色(保留被占用格子的红色高亮) */
|
|
350
|
+
ClearColor() {
|
|
351
|
+
this.seat_model_list.map(list => {
|
|
352
|
+
list.map(item => {
|
|
353
|
+
if (!this.grid.isOccupied(item.row, item.col)) {
|
|
354
|
+
item.node_chose_sp.color = Color.WHITE;
|
|
355
|
+
item.node_chose_sp.node.active = false
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
})
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** 清除所有高亮 */
|
|
362
|
+
clearColorAll() {
|
|
363
|
+
this.seat_model_list.forEach(list => {
|
|
364
|
+
list.forEach(map => {
|
|
365
|
+
map.node_chose_sp.node.active = false
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** 清除所有高亮 */
|
|
371
|
+
clearColortarget(row: number, col: number) {
|
|
372
|
+
this.seat_model_list[row][col].node_chose_sp.color = Color.WHITE;
|
|
373
|
+
this.seat_model_list[row][col].node_chose_sp.node.active = false
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** 显示/隐藏下半部分地图块(row >= 3) */
|
|
377
|
+
ShowBox(show: boolean) {
|
|
378
|
+
this.seat_model_list.map(list => {
|
|
379
|
+
list.map(item => {
|
|
380
|
+
if (item.row >= 3) {
|
|
381
|
+
item.node_sp.node.active = show;
|
|
382
|
+
}
|
|
383
|
+
})
|
|
384
|
+
})
|
|
385
|
+
}
|
|
386
|
+
}
|