消消方块阵换皮表情
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1420 lines
54 KiB

1 week ago
import { _decorator, Component, Node, Vec2, Vec3 } from 'cc';
import { MapGrid } from './MapGrid';
import { MapObstacle, ObstacleType } from './MapObstacle';
import { BattleType } from '../../manager/ChapterDataManager';
import { GBundle, GPath } from '../../game/ConfigRes';
import { MapPosGuide } from './MapPosGuide';
import { GEvent } from 'db://assets/mx/module/event/GEvent';
const { ccclass, property } = _decorator;
/** 最小堆(优先队列):用于 A* 开放列表优化 */
class MinHeap<T> {
private items: { value: T, priority: number }[] = [];
/** 添加元素 */
push(value: T, priority: number) {
this.items.push({ value, priority });
this.bubbleUp(this.items.length - 1);
}
/** 弹出优先级最高的元素 */
pop(): T | undefined {
if (this.items.length === 0) return undefined;
if (this.items.length === 1) return this.items.pop()?.value;
const result = this.items[0].value;
this.items[0] = this.items.pop()!;
this.bubbleDown(0);
return result;
}
/** 获取堆大小 */
get length(): number {
return this.items.length;
}
/** 清空堆 */
clear() {
this.items = [];
}
/** 上浮调整 */
private bubbleUp(index: number) {
while (index > 0) {
const parent = Math.floor((index - 1) / 2);
if (this.items[index].priority >= this.items[parent].priority) break;
[this.items[index], this.items[parent]] = [this.items[parent], this.items[index]];
index = parent;
}
}
/** 下沉调整 */
private bubbleDown(index: number) {
const length = this.items.length;
while (true) {
let smallest = index;
const left = index * 2 + 1;
const right = index * 2 + 2;
if (left < length && this.items[left].priority < this.items[smallest].priority) {
smallest = left;
}
if (right < length && this.items[right].priority < this.items[smallest].priority) {
smallest = right;
}
if (smallest === index) break;
[this.items[index], this.items[smallest]] = [this.items[smallest], this.items[index]];
index = smallest;
}
}
}
/** A* 寻路内部节点 */
class PathNode {
grid: MapGrid;
g: number; // 从起点到当前格子的实际成本
h: number; // 启发式估算成本(到终点)
f: number; // 总成本 f = g + h
parent: PathNode | null;
constructor(grid: MapGrid, g: number, h: number, parent: PathNode | null = null) {
this.grid = grid;
this.g = g;
this.h = h;
this.f = g + h;
this.parent = parent;
}
/** 重置节点用于对象复用 */
reset(grid: MapGrid, g: number, h: number, parent: PathNode | null = null) {
this.grid = grid;
this.g = g;
this.h = h;
this.f = g + h;
this.parent = parent;
return this;
}
};
@ccclass('MapManager')
export class MapManager extends Component {
@property({ displayName: '地图行数' })
mapRows: number = 16;
@property({ displayName: '地图列数' })
mapCols: number = 14;
@property({ displayName: '格子大小' })
mapGridSize: number = 53;
/**地图格子父节点 */
mapGridParent: Node = null;
/**地图目标父节点 */
mapTargetParent: Node = null;
/**出怪口父节点 */
spawnPointParent: Node = null;
/**推荐点父节点 */
guidePosParent: Node = null;
/**地图格子数组 */
mapGrids: MapGrid[] = [];
/**根据行列号缓存地图格子 */
mapGridCache: Map<string, MapGrid> = new Map();
//所有出怪口的格子
allSpawnPointGrids: Map<MapGrid, Node> = new Map();
//所有怪物攻击点的格子
allAttackPointGrids: Map<MapGrid, Node> = new Map();
/**所有推荐最佳障碍物摆放点 */
allBestObstaclePlacementGrids: MapGrid[] = [];
/**所有免费推荐点 */
allFreeRecommendGrids: MapGrid[] = [];
/**所有静态障碍物格子 */
allStaticObstacleGrids: MapGrid[] = [];
// A* 寻路复用对象池(减少 GC)
private heapPool: MinHeap<PathNode> | null = null;
private closedSetPool: Map<number, number> | null = null;
private gridNodeMapPool: Map<number, PathNode> | null = null;
// 统一最短路:从终点反向 BFS 构建的最短路树(保证同距离路径唯一)
// 注意:每次寻路都会根据当前终点和实时地图状态重建,不做跨调用缓存,避免“预计算路径仍穿过已被阻塞格子”的问题
private distanceMap: Map<number, number> = new Map();
private nextStepMap: Map<number, number> = new Map();
// 版本号:当地图格子的可走状态(isCanWalk)变化时递增
// 用于使寻路/可达性预计算结果失效
private _walkStateVersion: number = 0;
/** 8方向距离场(多源 Dijkstra)已构建到哪个版本 */
private _distance8BuiltVersion: number = -1;
/** 从“最近攻击点”扩散得到的最短代价(缩放整数) */
private _dist8Cost: Int32Array | null = null;
/** 每个格子对应的最近攻击点格子索引 */
private _bestTarget8Index: Int32Array | null = null;
/** 从该格子朝最近攻击点走的“下一步格子索引” */
private _nextStep8Index: Int32Array | null = null;
/** 距离场并列规则用,与 dist 同步重建 */
private _bestOrder8: Int32Array | null = null;
/** 可走掩码,避免每次重建距离场 new Uint8Array */
private _canWalk8: Uint8Array | null = null;
/** 多源 Dijkstra 堆复用 */
private _distance8Heap: MinHeap<{ idx: number; d: number }> | null = null;
/** 通知:地图可走状态发生变化 */
public notifyMapWalkStateChanged() {
this._walkStateVersion++;
this._distance8BuiltVersion = -1;
this.distanceMap.clear();
this.nextStepMap.clear();
this.reachabilityCache?.clear();
}
protected onLoad(): void {
this.mapGridParent = this.node.getChildByName('格子');
this.mapTargetParent = this.node.getChildByName('目标点');
this.spawnPointParent = this.node.getChildByName('出怪口');
this.guidePosParent = new Node("guidePosParent");
this.guidePosParent.parent = this.node;
this.guidePosParent.setPosition(0, 0, 0);
this.mapGrids = this.mapGridParent?.children?.map((node) => node.getOrAddComponent(MapGrid)) ?? [];
}
protected start(): void {
this.initMapGrids();
}
/**初始化地图格子 */
initMapGrids() {
this.allSpawnPointGrids.clear();
this.allAttackPointGrids.clear();
this.mapGridCache.clear();
this.allBestObstaclePlacementGrids = [];
this.allStaticObstacleGrids = [];
this.allFreeRecommendGrids = [];
for (let row = 0; row < this.mapRows; row++) {
for (let col = 0; col < this.mapCols; col++) {
let index = this.getMapGridIndex(row, col);
let mg = this.getMapGridByIndex(index);
if (!mg) continue;
mg.Row = row;
mg.Col = col;
mg.Index = index;
this.mapGridCache.set(`${row}-${col}`, mg);
if (mg.isBestPlace) {
this.allBestObstaclePlacementGrids.push(mg);
this.createRecommendPoints(mg);
}
if (mg.isFreePlace) {
this.allFreeRecommendGrids.push(mg);
this.createRecommendPoints(mg);
}
if (mg.getComponent(MapObstacle)?.type == ObstacleType.) {
this.allStaticObstacleGrids.push(mg);
}
}
}
//获取所有出怪口和怪物攻击点的格子
this.spawnPointParent.children.map((n) => {
let g = this.getMapGridByWorldPos(n.worldPosition);
if (g) this.allSpawnPointGrids.set(g, n);
});
this.mapTargetParent.children.map((n) => {
console.log('怪物攻击点', n.worldPosition);
let g = this.getMapGridByWorldPos(n.worldPosition);
// console.log('怪物攻击点格子', g);
if (g) this.allAttackPointGrids.set(g, n);
});
//需要修改位置的出怪口
let needMoveSpawnPointGrids: MapGrid[] = [];
//需要修改位置的怪物攻击点
let needMoveAttackPointGrids: MapGrid[] = [];
for (let row = 0; row < this.mapRows; row++) {
for (let col = 0; col < this.mapCols; col++) {
let index = this.getMapGridIndex(row, col);
let mg = this.getMapGridByIndex(index);
//宝箱副本最后两列预留宝箱怪区域,禁止通行、放置、铲除
if (col >= this.mapCols - 2 && gg.game.CurentBattle.BattleType == BattleType.BoxMode) {
mg.isCanWalk = false;
mg.isCanPutObstacle = false;
mg.isCanRemove = false;
if (this.allSpawnPointGrids.has(mg)) {
needMoveSpawnPointGrids.push(mg);
}
if (this.allAttackPointGrids.has(mg)) {
needMoveAttackPointGrids.push(mg);
}
}
}
}
//修改怪物出生位置
for (let i = 0; i < this.mapGrids.length; i++) {
if (i < 6) continue;
if (needMoveSpawnPointGrids.length == 0) break;
let mapGrid = this.mapGrids[i];
if (mapGrid.isCanWalk) {
let g = needMoveSpawnPointGrids.shift();
if (g) {
let node = this.allSpawnPointGrids.get(g)!;
node.worldPosition = mapGrid.node.worldPosition.clone();
this.allSpawnPointGrids.set(mapGrid, node);
}
}
}
//修改怪物攻击位置
if (this.allAttackPointGrids.size == needMoveAttackPointGrids.length) {
for (let i = 0; i < needMoveAttackPointGrids.length; i++) {
let mapGrid = needMoveAttackPointGrids[i];
let n = this.allAttackPointGrids.get(mapGrid);
let mPg = this.mapGrids[188];
n.worldPosition = mPg.node.worldPosition.clone();
this.allAttackPointGrids.set(mPg, n);
n.destroyAllChildren();
}
} else {
for (let i = 0; i < needMoveAttackPointGrids.length; i++) {
let mapGrid = needMoveAttackPointGrids[i];
let n = this.allAttackPointGrids.get(mapGrid);
n.destroy();
}
}
// 地图可走性/障碍状态初始化完成,通知寻路/可达性预计算失效
this.notifyMapWalkStateChanged();
}
/**创建推荐点 */
createRecommendPoints(grid: MapGrid) {
void this._createRecommendPointsAsync(grid);
}
private async _createRecommendPointsAsync(grid: MapGrid): Promise<void> {
if (!grid?.node?.isValid || !this.guidePosParent?.isValid) return;
const point = await gg.res.getNodeAsync(GPath.ObstaclePrefab(''), 'guidePos', GBundle.BattleMap);
if (!point?.isValid || !this.guidePosParent?.isValid || !grid?.node?.isValid) {
console.warn('[MapManager] guidePos 加载失败');
return;
}
1 week ago
this.guidePosParent.addChild(point);
point.worldPosition = grid.node.worldPosition;
point.active = true;
const posGuide = point.getComponent(MapPosGuide);
if (!posGuide) {
gg.res.putNode(point);
return;
}
1 week ago
posGuide.grid = grid;
posGuide.updateShow();
}
/**根据行列号获取地图索引 */
getMapGridIndex(row: number, col: number): number {
return row * this.mapCols + col;
}
/**根据行列号获取地图格子 */
getMapGrid(row: number, col: number): MapGrid {
let grid = this.mapGridCache.get(`${row}-${col}`);
return grid;
}
/**根据地图索引获取地图格子 */
getMapGridByIndex(index: number): MapGrid {
if (!this.mapGrids?.length) return null;
if (index < 0 || index >= this.mapRows * this.mapCols) return null;
return this.mapGrids[index];
}
/**根据世界坐标获取mapGrid */
getMapGridByWorldPos(worldPos: Vec3): MapGrid {
if (!worldPos) return null;
const grids = this.mapGrids;
if (!Array.isArray(grids) || grids.length === 0) return null;
const p1 = worldPos;
const offset = this.mapGridSize * 0.5;
for (let i = 0, len = grids.length; i < len; i++) {
const mapGrid = grids[i];
if (!mapGrid || !mapGrid.node || !mapGrid.node.isValid) continue;
const p2 = mapGrid.node.worldPosition;
let minX = p2.x - offset;
let maxX = p2.x + offset;
let minY = p2.y - offset;
let maxY = p2.y + offset;
if (p1.x >= minX && p1.x <= maxX && p1.y >= minY && p1.y <= maxY) {
return mapGrid;
}
}
return null;
}
/**获取所有可通行的地图格子 */
getAllCanWalkMapGrids(): MapGrid[] {
let canWalkGrids: MapGrid[] = [];
for (let i = 0; i < this.mapGrids.length; i++) {
let mapGrid = this.mapGrids[i];
if (mapGrid.isCanWalk) {
canWalkGrids.push(mapGrid);
}
}
return canWalkGrids;
}
/**获取所有可放置障碍物的地图格子 */
getAllCanPutObstacleMapGrids(): MapGrid[] {
let canPutObstacleGrids: MapGrid[] = [];
for (let i = 0; i < this.mapGrids.length; i++) {
let mapGrid = this.mapGrids[i];
if (mapGrid.isCanPutObstacle && mapGrid.node.active) {
canPutObstacleGrids.push(mapGrid);
}
}
return canPutObstacleGrids;
}
/**获取所有可铲除的地图格子 */
getAllCanRemoveMapGrids(): MapGrid[] {
let canRemoveGrids: MapGrid[] = [];
for (let i = 0; i < this.mapGrids.length; i++) {
let mapGrid = this.mapGrids[i];
if (mapGrid.isCanRemove) {
canRemoveGrids.push(mapGrid);
}
}
return canRemoveGrids;
}
/**随机获取count个不同的可放置的地图格子 */
getRandomCanPutObstacleMapGrids(count: number): MapGrid[] {
let canPutObstacleGrids = this.getAllCanPutObstacleMapGrids();
let shuffled = canPutObstacleGrids.slice(0);
for (let i = shuffled.length - 1; i > 0; i--) {
let j = Math.floor(Math.random() * (i + 1));
[shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
}
return shuffled.slice(0, count);
}
/**根据起点索引和终点索引寻路 */
findPathByIndex(startIndex: number, endIndex: number): MapGrid[] {
let startGrid = this.getMapGridByIndex(startIndex);
let endGrid = this.getMapGridByIndex(endIndex);
if (!startGrid || !endGrid) return [];
return this.findPath8Dir(startGrid, endGrid);
}
/**根据起点grid和终点格子寻路 */
findPath(startGrid: MapGrid, endGrid: MapGrid): MapGrid[] {
// 使用从终点反向 BFS 构建的最短路树,保证同距离路径唯一,且支持垂直方向优先
if (!startGrid || !endGrid || !startGrid.isCanWalk || !endGrid.isCanWalk) {
return [];
}
if (startGrid.Index === endGrid.Index) {
return [startGrid];
}
// 每次根据当前终点和实时障碍状态重新构建最短路树
this.buildShortestPathTreeForSingleEnd(endGrid);
// 如果起点在当前终点的可达区域之外,直接返回空
if (!this.distanceMap.has(startGrid.Index)) {
return [];
}
const path: MapGrid[] = [];
let current = startGrid;
const maxStep = this.mapRows * this.mapCols;
for (let step = 0; step < maxStep; step++) {
path.push(current);
if (current.Index === endGrid.Index) {
break;
}
const nextIndex = this.nextStepMap.get(current.Index);
if (nextIndex == null) {
// 无法继续向终点前进,路径断开
return [];
}
const nextGrid = this.getMapGridByIndex(nextIndex);
if (!nextGrid || !nextGrid.isCanWalk) {
return [];
}
current = nextGrid;
}
if (path[path.length - 1].Index !== endGrid.Index) {
return [];
}
return path;
}
/**
* BFS
*
*/
private buildShortestPathTreeForSingleEnd(endGrid: MapGrid) {
this.distanceMap.clear();
this.nextStepMap.clear();
if (!endGrid || !endGrid.isCanWalk) {
return;
}
const queue: MapGrid[] = [];
const visited = new Set<number>();
const endIndex = endGrid.Index;
visited.add(endIndex);
queue.push(endGrid);
this.distanceMap.set(endIndex, 0);
// 先做一次普通 BFS,只计算每个格子到终点的最短步数
const directions = [
{ dr: -1, dc: 0 }, // 上
{ dr: 1, dc: 0 }, // 下
{ dr: 0, dc: -1 }, // 左
{ dr: 0, dc: 1 } // 右
];
while (queue.length > 0) {
const current = queue.shift()!;
const curIdx = current.Index;
const curDist = this.distanceMap.get(curIdx)!;
for (const dir of directions) {
const newRow = current.Row + dir.dr;
const newCol = current.Col + dir.dc;
if (newRow < 0 || newRow >= this.mapRows || newCol < 0 || newCol >= this.mapCols) {
continue;
}
const neighbor = this.getMapGrid(newRow, newCol);
if (!neighbor || !neighbor.isCanWalk) {
continue;
}
const nbIdx = neighbor.Index;
if (visited.has(nbIdx)) {
continue;
}
visited.add(nbIdx);
queue.push(neighbor);
this.distanceMap.set(nbIdx, curDist + 1);
}
}
// 再根据 distanceMap,为每个格子选择“朝终点走的下一步”,并加上垂直优先规则
const endRow = endGrid.Row;
const endCol = endGrid.Col;
for (let i = 0; i < this.mapGrids.length; i++) {
const grid = this.mapGrids[i];
if (!grid || !grid.isCanWalk) continue;
const idx = grid.Index;
if (idx === endIndex) continue; // 终点本身没有下一步
const dist = this.distanceMap.get(idx);
if (dist === undefined || dist <= 0) continue;
const candidates: MapGrid[] = [];
for (const dir of directions) {
const newRow = grid.Row + dir.dr;
const newCol = grid.Col + dir.dc;
if (newRow < 0 || newRow >= this.mapRows || newCol < 0 || newCol >= this.mapCols) {
continue;
}
const neighbor = this.getMapGrid(newRow, newCol);
if (!neighbor || !neighbor.isCanWalk) continue;
const nbIdx = neighbor.Index;
const nbDist = this.distanceMap.get(nbIdx);
if (nbDist !== undefined && nbDist === dist - 1) {
candidates.push(neighbor);
}
}
if (candidates.length === 0) continue;
// 垂直优先:优先选择能更快减小与终点行差的候选,其次才考虑列差
candidates.sort((a, b) => {
const aRowDiff = Math.abs(a.Row - endRow);
const bRowDiff = Math.abs(b.Row - endRow);
if (aRowDiff !== bRowDiff) {
return aRowDiff - bRowDiff;
}
const aColDiff = Math.abs(a.Col - endCol);
const bColDiff = Math.abs(b.Col - endCol);
if (aColDiff !== bColDiff) {
return aColDiff - bColDiff;
}
// 最后再用固定方向顺序做稳定的 tie-break(上、下、左、右)
const dirOrder = (g: MapGrid) => {
if (g.Row < grid.Row) return 0; // 上
if (g.Row > grid.Row) return 1; // 下
if (g.Col < grid.Col) return 2; // 左
return 3; // 右
};
return dirOrder(a) - dirOrder(b);
});
const bestNext = candidates[0];
this.nextStepMap.set(idx, bestNext.Index);
}
}
/** 可选:支持8方向移动的版本(对角线成本≈1.414) */
findPath8Dir(startGrid: MapGrid, endGrid: MapGrid): MapGrid[] {
if (!startGrid || !endGrid || !startGrid.isCanWalk || !endGrid.isCanWalk) {
return [];
}
if (startGrid.Index === endGrid.Index) {
return [startGrid];
}
if (!this.heapPool || !this.closedSetPool || !this.gridNodeMapPool) {
this.heapPool = new MinHeap<PathNode>();
this.closedSetPool = new Map<number, number>();
this.gridNodeMapPool = new Map<number, PathNode>();
}
const openSet = this.heapPool;
const closedSet = this.closedSetPool;
const gridNodeMap = this.gridNodeMapPool;
openSet.clear();
closedSet.clear();
gridNodeMap.clear();
// 8方向启发式:欧几里得距离
const heuristic = (a: MapGrid, b: MapGrid): number => {
const dr = a.Row - b.Row;
const dc = a.Col - b.Col;
return Math.sqrt(dr * dr + dc * dc);
};
const getNeighbors = (grid: MapGrid): MapGrid[] => {
const neighbors: MapGrid[] = [];
// 与距离场一致:8 向边权相同,便于与 findPath8DirFast 的并列策略一致(降级 A* 时)
const directions = [
{ dr: 1, dc: 0, cost: 1 }, // 下
{ dr: 1, dc: 1, cost: 1 }, // 右下
{ dr: 1, dc: -1, cost: 1 }, // 左下
{ dr: 0, dc: 1, cost: 1 }, // 右
{ dr: 0, dc: -1, cost: 1 }, // 左
{ dr: -1, dc: 0, cost: 1 }, // 上
{ dr: -1, dc: 1, cost: 1 }, // 右上
{ dr: -1, dc: -1, cost: 1 }, // 左上
];
for (const dir of directions) {
const newRow = grid.Row + dir.dr;
const newCol = grid.Col + dir.dc;
if (newRow < 0 || newRow >= this.mapRows ||
newCol < 0 || newCol >= this.mapCols) {
continue;
}
// 斜向移动时,禁止“穿角”:要求两个相邻的正交格子都可行走
if (Math.abs(dir.dr) === 1 && Math.abs(dir.dc) === 1) {
const r1 = grid.Row + dir.dr;
const c1 = grid.Col; // 垂直邻格
const r2 = grid.Row;
const c2 = grid.Col + dir.dc; // 水平邻格
const n1 = this.getMapGrid(r1, c1);
const n2 = this.getMapGrid(r2, c2);
if (!n1 || !n1.isCanWalk || !n2 || !n2.isCanWalk) {
continue;
}
}
const neighbor = this.getMapGrid(newRow, newCol);
if (neighbor && neighbor.isCanWalk) {
(neighbor as any)._moveCost = dir.cost; // 临时存储移动成本
neighbors.push(neighbor);
}
}
return neighbors;
};
const startNode = new PathNode(startGrid, 0, heuristic(startGrid, endGrid));
openSet.push(startNode, startNode.f);
gridNodeMap.set(startGrid.Index, startNode);
while (openSet.length > 0) {
const currentNode = openSet.pop()!;
if (currentNode.grid.Index === endGrid.Index) {
const path: MapGrid[] = [];
let node: InstanceType<typeof PathNode> | null = currentNode;
while (node) {
path.unshift(node.grid);
node = node.parent;
}
return path;
}
closedSet.set(currentNode.grid.Index, currentNode.g);
const neighbors = getNeighbors(currentNode.grid);
for (const neighbor of neighbors) {
const moveCost = (neighbor as any)._moveCost || 1;
const existingG = closedSet.get(neighbor.Index);
const tentativeG = currentNode.g + moveCost;
if (existingG !== undefined && tentativeG >= existingG) continue;
let neighborNode = gridNodeMap.get(neighbor.Index);
const oldParentIdx = neighborNode?.parent?.grid?.Index ?? -1;
const preferThisParent =
!neighborNode ||
tentativeG < neighborNode.g ||
(tentativeG === neighborNode.g &&
this.aStarTowardGoalTieBetter(
currentNode.grid.Index,
neighbor.Index,
oldParentIdx,
endGrid.Index
));
if (!neighborNode) {
neighborNode = new PathNode(
neighbor,
tentativeG,
heuristic(neighbor, endGrid),
currentNode
);
openSet.push(neighborNode, neighborNode.f);
gridNodeMap.set(neighbor.Index, neighborNode);
} else if (preferThisParent) {
neighborNode.reset(neighbor, tentativeG, neighborNode.h, currentNode);
openSet.push(neighborNode, neighborNode.f);
}
}
}
return [];
}
/**
* farIdx nearIdxfar
* /
*/
private dijkstraStepGoalAlignmentScore(farIdx: number, nearIdx: number, goalIdx: number): number {
const c = this.mapCols;
const fr = (farIdx / c) | 0;
const fc = farIdx - fr * c;
const nr = (nearIdx / c) | 0;
const nc = nearIdx - nr * c;
const gr = (goalIdx / c) | 0;
const gc = goalIdx - gr * c;
const dr = nr - fr;
const dc = nc - fc;
const toR = gr - fr;
const toC = gc - fc;
let s = dr * toR + dc * toC;
if (toR === 0 && dr !== 0) s -= 1_000_000;
if (toC === 0 && dc !== 0) s -= 1_000_000;
return s;
}
private dijkstraTowardGoalTieBetter(farIdx: number, nearNew: number, nearOld: number, goalIdx: number): boolean {
if (nearOld < 0) return true;
if (goalIdx < 0) return nearNew < nearOld;
const sN = this.dijkstraStepGoalAlignmentScore(farIdx, nearNew, goalIdx);
const sO = this.dijkstraStepGoalAlignmentScore(farIdx, nearOld, goalIdx);
if (sN !== sO) return sN > sO;
return nearNew < nearOld;
}
/**
* A* parent child child 45°
*/
private aStarEdgeGoalAlignmentScore(parentIdx: number, childIdx: number, goalIdx: number): number {
const c = this.mapCols;
const pr = (parentIdx / c) | 0;
const pc = parentIdx - pr * c;
const cr = (childIdx / c) | 0;
const cc = childIdx - cr * c;
const gr = (goalIdx / c) | 0;
const gc = goalIdx - gr * c;
const dr = cr - pr;
const dc = cc - pc;
const toR = gr - cr;
const toC = gc - cc;
let s = dr * toR + dc * toC;
if (toR === 0 && dr !== 0) s -= 1_000_000;
if (toC === 0 && dc !== 0) s -= 1_000_000;
return s;
}
private aStarTowardGoalTieBetter(parentIdx: number, childIdx: number, oldParentIdx: number, goalIdx: number): boolean {
if (oldParentIdx < 0) return true;
if (goalIdx < 0) return parentIdx < oldParentIdx;
const sN = this.aStarEdgeGoalAlignmentScore(parentIdx, childIdx, goalIdx);
const sO = this.aStarEdgeGoalAlignmentScore(oldParentIdx, childIdx, goalIdx);
if (sN !== sO) return sN > sO;
return parentIdx < oldParentIdx;
}
/**
* 8 Dijkstra
* - dist
* - bestTargetIndex
* - nextStepIndex
*/
/** 预分配/复用 8 向距离场缓冲区,减少雷击等改图时的 GC 与瞬时卡顿 */
private ensureDistance8ScratchBuffers(size: number) {
if (!this._canWalk8 || this._canWalk8.length !== size) {
this._canWalk8 = new Uint8Array(size);
}
if (!this._dist8Cost || this._dist8Cost.length !== size) {
this._dist8Cost = new Int32Array(size);
}
if (!this._bestTarget8Index || this._bestTarget8Index.length !== size) {
this._bestTarget8Index = new Int32Array(size);
}
if (!this._bestOrder8 || this._bestOrder8.length !== size) {
this._bestOrder8 = new Int32Array(size);
}
if (!this._nextStep8Index || this._nextStep8Index.length !== size) {
this._nextStep8Index = new Int32Array(size);
}
if (!this._distance8Heap) {
this._distance8Heap = new MinHeap<{ idx: number; d: number }>();
} else {
this._distance8Heap.clear();
}
}
private ensureDistance8DirBuilt() {
if (this._distance8BuiltVersion === this._walkStateVersion && this._dist8Cost && this._bestTarget8Index && this._nextStep8Index) {
return;
}
const size = this.mapRows * this.mapCols;
this.ensureDistance8ScratchBuffers(size);
const canWalk = this._canWalk8!;
for (let i = 0; i < size; i++) {
const mg = this.mapGrids[i];
canWalk[i] = mg && mg.isCanWalk ? 1 : 0;
}
const INF = 1_000_000_000;
/**
* 使 1000 vs 1414
* ****45°
* Chebyshev dijkstraTowardGoalTieBetter /
*/
const STEP = 1000;
const dist = this._dist8Cost!;
dist.fill(INF);
const bestTarget = this._bestTarget8Index!;
bestTarget.fill(-1);
const bestOrder = this._bestOrder8!;
bestOrder.fill(INF);
const nextStep = this._nextStep8Index!;
nextStep.fill(-1);
const heap = this._distance8Heap!;
// 多源初始化:每个攻击点格子作为源点
const targetNodes = this.mapTargetParent?.children ?? [];
for (let t = 0; t < targetNodes.length; t++) {
const targetNode = targetNodes[t];
const tg = this.getMapGridByWorldPos(targetNode.worldPosition);
if (!tg || !tg.isCanWalk) continue;
const idx = tg.Index;
if (dist[idx] > 0) {
dist[idx] = 0;
bestTarget[idx] = idx;
bestOrder[idx] = t;
nextStep[idx] = -1;
heap.push({ idx, d: 0 }, 0);
}
}
const popAndSkipStale = () => {
while (heap.length > 0) {
const item = heap.pop()!;
const curIdx = item.idx;
// 如果堆里是旧条目,跳过
if (item.d !== dist[curIdx]) continue;
return item;
}
return null;
};
// 方向定义:先正交后斜向,斜向需禁穿角(边权均为 STEP)
const orthDirs = [
{ dr: -1, dc: 0, cost: STEP },
{ dr: 1, dc: 0, cost: STEP },
{ dr: 0, dc: -1, cost: STEP },
{ dr: 0, dc: 1, cost: STEP },
];
const diagDirs = [
{ dr: -1, dc: -1, cost: STEP }, // 左上
{ dr: -1, dc: 1, cost: STEP }, // 右上
{ dr: 1, dc: -1, cost: STEP }, // 左下
{ dr: 1, dc: 1, cost: STEP }, // 右下
];
while (true) {
const item = popAndSkipStale();
if (!item) break;
const curIdx = item.idx;
const curDist = item.d;
const curRow = Math.floor(curIdx / this.mapCols);
const curCol = curIdx - curRow * this.mapCols;
// 正交邻格
for (let i = 0; i < orthDirs.length; i++) {
const dir = orthDirs[i];
const nr = curRow + dir.dr;
const nc = curCol + dir.dc;
if (nr < 0 || nr >= this.mapRows || nc < 0 || nc >= this.mapCols) continue;
const nbIdx = nr * this.mapCols + nc;
if (!canWalk[nbIdx]) continue;
const nd = curDist + dir.cost;
if (nd < dist[nbIdx]) {
dist[nbIdx] = nd;
bestTarget[nbIdx] = bestTarget[curIdx];
bestOrder[nbIdx] = bestOrder[curIdx];
nextStep[nbIdx] = curIdx;
heap.push({ idx: nbIdx, d: nd }, nd);
} else if (nd === dist[nbIdx]) {
const candOrder = bestOrder[curIdx];
const goalIdx = bestTarget[curIdx];
if (
candOrder < bestOrder[nbIdx] ||
(candOrder === bestOrder[nbIdx] &&
(nextStep[nbIdx] < 0 ||
this.dijkstraTowardGoalTieBetter(nbIdx, curIdx, nextStep[nbIdx], goalIdx)))
) {
bestOrder[nbIdx] = candOrder;
bestTarget[nbIdx] = goalIdx;
nextStep[nbIdx] = curIdx;
heap.push({ idx: nbIdx, d: nd }, nd);
}
}
}
// 对角邻格:禁穿角
for (let i = 0; i < diagDirs.length; i++) {
const dir = diagDirs[i];
const nr = curRow + dir.dr;
const nc = curCol + dir.dc;
if (nr < 0 || nr >= this.mapRows || nc < 0 || nc >= this.mapCols) continue;
// 禁穿角:需要两个正交格都可走
const orth1R = curRow + dir.dr;
const orth1C = curCol;
const orth2R = curRow;
const orth2C = curCol + dir.dc;
const orth1Idx = orth1R * this.mapCols + orth1C;
const orth2Idx = orth2R * this.mapCols + orth2C;
if (!canWalk[orth1Idx] || !canWalk[orth2Idx]) continue;
const nbIdx = nr * this.mapCols + nc;
if (!canWalk[nbIdx]) continue;
const nd = curDist + dir.cost;
if (nd < dist[nbIdx]) {
dist[nbIdx] = nd;
bestTarget[nbIdx] = bestTarget[curIdx];
bestOrder[nbIdx] = bestOrder[curIdx];
nextStep[nbIdx] = curIdx;
heap.push({ idx: nbIdx, d: nd }, nd);
} else if (nd === dist[nbIdx]) {
const candOrder = bestOrder[curIdx];
const goalIdx = bestTarget[curIdx];
if (
candOrder < bestOrder[nbIdx] ||
(candOrder === bestOrder[nbIdx] &&
(nextStep[nbIdx] < 0 ||
this.dijkstraTowardGoalTieBetter(nbIdx, curIdx, nextStep[nbIdx], goalIdx)))
) {
bestOrder[nbIdx] = candOrder;
bestTarget[nbIdx] = goalIdx;
nextStep[nbIdx] = curIdx;
heap.push({ idx: nbIdx, d: nd }, nd);
}
}
}
}
this._distance8BuiltVersion = this._walkStateVersion;
}
/**
* 8 nextStep
* - endGrid A*
*/
public findPath8DirFast(startGrid: MapGrid, endGrid: MapGrid): MapGrid[] {
if (!startGrid || !endGrid || !startGrid.isCanWalk || !endGrid.isCanWalk) return [];
if (startGrid.Index === endGrid.Index) return [startGrid];
this.ensureDistance8DirBuilt();
const startIdx = startGrid.Index;
const endIdx = endGrid.Index;
if (!this._bestTarget8Index || this._bestTarget8Index[startIdx] !== endIdx) {
// endGrid 不是最近攻击点(可能是多目标距离并列/或外部强制目标)
return this.findPath8Dir(startGrid, endGrid);
}
const path: MapGrid[] = [];
let curIdx = startIdx;
const maxStep = this.mapRows * this.mapCols;
for (let i = 0; i < maxStep; i++) {
const mg = this.getMapGridByIndex(curIdx);
if (!mg) return [];
path.push(mg);
if (curIdx === endIdx) break;
const ni = this._nextStep8Index ? this._nextStep8Index[curIdx] : -1;
if (ni < 0) return [];
curIdx = ni;
}
if (path.length === 0) return [];
return path[path.length - 1].Index === endIdx ? path : [];
}
/**根据世界坐标获取路径最短的目标 */
getPathShortestTarget(startPos: Vec3): Node {
const startGrid = this.getMapGridByWorldPos(startPos);
if (!startGrid || !startGrid.isCanWalk) return null;
this.ensureDistance8DirBuilt();
if (!this._bestTarget8Index) return null;
const targetGridIdx = this._bestTarget8Index[startGrid.Index];
if (targetGridIdx < 0) return null;
const targetGrid = this.getMapGridByIndex(targetGridIdx);
if (!targetGrid) return null;
return this.allAttackPointGrids.get(targetGrid) ?? null;
}
/**格子是否可走且可到达任意攻击点(基于8方向距离场缓存,O(1)) */
public canGridReachAnyAttackPointFast(grid: MapGrid): boolean {
if (!grid || !grid.isCanWalk) return false;
this.ensureDistance8DirBuilt();
if (!this._bestTarget8Index) return false;
return this._bestTarget8Index[grid.Index] >= 0;
}
/**
* 使
* - 4BFS最近层扩散
* -
*/
public findNearestWalkableReachableGridForSpawn(startPos: Vec3): MapGrid | null {
const startGrid = this.getMapGridByWorldPos(startPos);
if (startGrid) {
const queue: MapGrid[] = [startGrid];
const visited = new Set<number>([startGrid.Index]);
const dirs: [number, number][] = [
[-1, 0],
[0, -1],
[0, 1],
[1, 0],
];
let head = 0;
while (head < queue.length) {
const cur = queue[head++];
if (this.canGridReachAnyAttackPointFast(cur)) {
return cur;
}
for (let i = 0; i < dirs.length; i++) {
const [dr, dc] = dirs[i];
const nb = this.getMapGrid(cur.Row + dr, cur.Col + dc);
if (!nb || visited.has(nb.Index)) continue;
visited.add(nb.Index);
queue.push(nb);
}
}
return null;
}
let best: MapGrid | null = null;
let bestDist2 = Number.MAX_VALUE;
for (let i = 0; i < this.mapGrids.length; i++) {
const g = this.mapGrids[i];
if (!this.canGridReachAnyAttackPointFast(g)) continue;
const wp = g.node.worldPosition;
const dx = wp.x - startPos.x;
const dy = wp.y - startPos.y;
const dist2 = dx * dx + dy * dy;
if (dist2 < bestDist2) {
bestDist2 = dist2;
best = g;
}
}
return best;
}
// 多起点多终点可达性检查缓存(用于性能优化)
private reachabilityCache: Map<string, boolean> = new Map();
private lastCacheClearTime: number = 0;
/**
*
* 使BFS从所有终点同时开始搜索
* @param startPositions
* @param endPositions /
* @param use8Dir 使8false使4
* @param self
* @returns true
*/
checkAllStartsCanReachAnyEnd(startPositions: Vec3[], endPositions: Vec3[], use8Dir: boolean = false, self: MapGrid): boolean {
// 基础校验
if (!startPositions || startPositions.length === 0 || !endPositions || endPositions.length === 0) {
console.log("起点或终点数组为空");
return false;
}
// 生成缓存键(必须包含 self:同一地图不同候选格会改变可走区域)
const cacheKey = this.generateReachabilityCacheKey(startPositions, endPositions, use8Dir, self);
if (this.reachabilityCache.has(cacheKey)) {
return this.reachabilityCache.get(cacheKey);
}
// 转换所有起点和终点为 MapGrid(去重)
const startGrids: MapGrid[] = [];
const startSeen = new Set<number>();
const endGrids: MapGrid[] = [];
const endSeen = new Set<number>();
for (const pos of startPositions) {
const grid = this.getMapGridByWorldPos(pos);
if (!grid || !grid.isCanWalk) {
continue;
}
if (grid == self) continue;
if (startSeen.has(grid.Index)) continue;
startSeen.add(grid.Index);
startGrids.push(grid);
}
if (startGrids.length === 0) {
this.reachabilityCache.set(cacheKey, false);
return false;
}
for (const pos of endPositions) {
const grid = this.getMapGridByWorldPos(pos);
if (grid && grid.isCanWalk) {
if (endSeen.has(grid.Index)) continue;
endSeen.add(grid.Index);
endGrids.push(grid);
}
}
// 如果没有任何有效终点,直接返回false
if (endGrids.length === 0) {
this.reachabilityCache.set(cacheKey, false);
console.log("没有有效终点");
return false;
}
// ① 从所有终点反向 BFS:每个起点须能到达至少一个终点
const reachableFromAnyEnd = this.parallelBFSFromMultipleTargets(endGrids, use8Dir);
for (const startGrid of startGrids) {
if (!reachableFromAnyEnd.has(startGrid.Index)) {
this.reachabilityCache.set(cacheKey, false);
return false;
}
}
// ② 从所有起点正向 BFS:每个受击点须仍能被至少一条怪物路线到达
const reachableFromAnyStart = this.parallelBFSFromMultipleSources(startGrids, use8Dir);
for (const endGrid of endGrids) {
if (!reachableFromAnyStart.has(endGrid.Index)) {
this.reachabilityCache.set(cacheKey, false);
return false;
}
}
this.reachabilityCache.set(cacheKey, true);
return true;
}
/**
* BFS
* @param targetGrids
* @param use8Dir 使8
* @returns Set包含所有可达格子的索引
*/
private parallelBFSFromMultipleTargets(targetGrids: MapGrid[], use8Dir: boolean): Set<number> {
const reachable = new Set<number>();
const queue: MapGrid[] = [];
const visited = new Set<number>();
// 将所有终点加入队列并标记为已访问
for (const grid of targetGrids) {
const index = grid.Index;
if (!visited.has(index)) {
visited.add(index);
queue.push(grid);
reachable.add(index);
}
}
// BFS方向定义
const directions4 = [
{ dr: -1, dc: 0 }, // 上
{ dr: 1, dc: 0 }, // 下
{ dr: 0, dc: -1 }, // 左
{ dr: 0, dc: 1 } // 右
];
const directions8 = [
{ dr: -1, dc: 0 }, // 上
{ dr: 1, dc: 0 }, // 下
{ dr: 0, dc: -1 }, // 左
{ dr: 0, dc: 1 }, // 右
{ dr: -1, dc: -1 }, // 左上
{ dr: -1, dc: 1 }, // 右上
{ dr: 1, dc: -1 }, // 左下
{ dr: 1, dc: 1 } // 右下
];
const directions = use8Dir ? directions8 : directions4;
// BFS主循环(避免 queue.shift() 带来的 O(n) 移位开销)
let head = 0;
while (head < queue.length) {
const current = queue[head++];
for (const dir of directions) {
const newRow = current.Row + dir.dr;
const newCol = current.Col + dir.dc;
// 检查边界
if (newRow < 0 || newRow >= this.mapRows || newCol < 0 || newCol >= this.mapCols) {
continue;
}
const neighbor = this.getMapGrid(newRow, newCol);
if (!neighbor || !neighbor.isCanWalk) {
continue;
}
const neighborIndex = neighbor.Index;
if (!visited.has(neighborIndex)) {
visited.add(neighborIndex);
queue.push(neighbor);
reachable.add(neighborIndex);
}
}
}
return reachable;
}
/**
* BFS parallelBFSFromMultipleTargets
*/
private parallelBFSFromMultipleSources(sourceGrids: MapGrid[], use8Dir: boolean): Set<number> {
const reachable = new Set<number>();
const queue: MapGrid[] = [];
const visited = new Set<number>();
for (const grid of sourceGrids) {
const index = grid.Index;
if (!visited.has(index)) {
visited.add(index);
queue.push(grid);
reachable.add(index);
}
}
const directions4 = [
{ dr: -1, dc: 0 },
{ dr: 1, dc: 0 },
{ dr: 0, dc: -1 },
{ dr: 0, dc: 1 },
];
const directions8 = [
{ dr: -1, dc: 0 },
{ dr: 1, dc: 0 },
{ dr: 0, dc: -1 },
{ dr: 0, dc: 1 },
{ dr: -1, dc: -1 },
{ dr: -1, dc: 1 },
{ dr: 1, dc: -1 },
{ dr: 1, dc: 1 },
];
const directions = use8Dir ? directions8 : directions4;
let head = 0;
while (head < queue.length) {
const current = queue[head++];
for (const dir of directions) {
const newRow = current.Row + dir.dr;
const newCol = current.Col + dir.dc;
if (newRow < 0 || newRow >= this.mapRows || newCol < 0 || newCol >= this.mapCols) {
continue;
}
const neighbor = this.getMapGrid(newRow, newCol);
if (!neighbor || !neighbor.isCanWalk) {
continue;
}
const neighborIndex = neighbor.Index;
if (!visited.has(neighborIndex)) {
visited.add(neighborIndex);
queue.push(neighbor);
reachable.add(neighborIndex);
}
}
}
return reachable;
}
/**
*
* 使
*/
private generateReachabilityCacheKey(startPositions: Vec3[], endPositions: Vec3[], use8Dir: boolean, self: MapGrid): string {
// 使用格子的索引来生成键,这样即使坐标有微小变化,只要在同一格子内,键就相同
const startIndices: number[] = [];
const endIndices: number[] = [];
for (const pos of startPositions) {
const grid = this.getMapGridByWorldPos(pos);
if (grid) {
startIndices.push(grid.Index);
}
}
for (const pos of endPositions) {
const grid = this.getMapGridByWorldPos(pos);
if (grid) {
endIndices.push(grid.Index);
}
}
// 排序以确保相同配置生成相同键
startIndices.sort((a, b) => a - b);
endIndices.sort((a, b) => a - b);
const selfIdx = self ? self.Index : -1;
return `${startIndices.join(',')}_${endIndices.join(',')}_${use8Dir ? '8' : '4'}_${selfIdx}`;
}
/**随机雷击一个障碍物 */
randomThunderObstacle() {
let grids = this.mapGrids.filter(grid => grid.isCanBeThunder);
if (grids.length == 0) return null;
let grid = grids[Math.floor(Math.random() * grids.length)];
grid.getComponent(MapObstacle).thunderAtk();
return grid;
}
/**找出count个最佳摆放点 */
getBestObstaclePlacementGrids(count: number): MapGrid[] {
//allBestObstaclePlacementGrids是地图预设的所有最佳摆放点
if (count <= 0 || this.allBestObstaclePlacementGrids.length == 0) return [];
//1.先筛选出没有被障碍物占据的最佳摆放点
let canPlaceGrids = this.allBestObstaclePlacementGrids.filter(grid => grid.isCanPutObstacle && grid.isCanWalk && !grid.ShowGuidePos);
if (canPlaceGrids.length == 0) return [];
//2.从这些可以放置的格子中选出一个假如放进去后能让怪物绕路最远的格子
canPlaceGrids.sort((a, b) => {
a.isCanWalk = false;
let disA = this.getTotalDistanceToAttackPoints();
a.isCanWalk = true;
b.isCanWalk = false;
let disB = this.getTotalDistanceToAttackPoints();
b.isCanWalk = true;
return disB - disA;
});
if (canPlaceGrids.length > count) {
canPlaceGrids = canPlaceGrids.slice(0, count);
}
return canPlaceGrids;
}
/**计算当前所有出怪口到怪物攻击点的汇总距离 */
getTotalDistanceToAttackPoints(): number {
let result = 0;
const wallTargets = this.mapTargetParent.children;
let spawnPoints: Node[] = this.spawnPointParent.children;
if (wallTargets.length === 0 || spawnPoints.length === 0) return 0;
spawnPoints = spawnPoints.filter(x => {
let arr = x.name.split("_");
let index = parseInt(arr[1]);
if (index >= 7) {
return true;
}
});
spawnPoints.unshift(this.mapGrids[6].node);
for (let i = 0; i < spawnPoints.length; i++) {
let startNode = spawnPoints[i];
for (let j = 0; j < wallTargets.length; j++) {
let endNode = wallTargets[j];
let startGrid = this.getMapGridByWorldPos(startNode.worldPosition);
let endGrid = this.getMapGridByWorldPos(endNode.worldPosition);
let morePaths = this.findPath8Dir(startGrid, endGrid);
for (let k = 0; k < morePaths.length - 1; k++) {
result += Vec3.distance(morePaths[k].node.worldPosition, morePaths[k + 1].node.worldPosition);
}
}
}
return result;
}
/**更新推荐点 */
updateRecommendPoints(count: number = 1) {
let grids = this.getBestObstaclePlacementGrids(count);
if (grids.length == 0) return;
for (let i = 0; i < grids.length; i++) {
let grid = grids[i];
grid.ShowGuidePos = true;
}
GEvent.Ins.emit(GEvent.UpdateBestPlace);
}
/**显示推荐点 */
showRecommendPoints() {
this.guidePosParent.active = true;
}
/**隐藏推荐点 */
hideRecommendPoints() {
this.guidePosParent.active = false;
}
/**获取所有可移除的障碍物 */
getAllCanRemoveObstacleGrids(): MapGrid[] {
return this.mapGrids.filter(grid => grid.isCanRemove);
}
}