UNPKG

@esengine/pathfinding

Version:

寻路系统 | Pathfinding System - A*, Grid, NavMesh

1,855 lines (1,851 loc) 75.4 kB
import { AStarPathfinder, BinaryHeap, CollisionResolverAdapter, DEFAULT_FLOW_CONTROLLER_CONFIG, DEFAULT_HPA_CONFIG, DEFAULT_ORCA_PARAMS, DEFAULT_PATHFINDING_OPTIONS, DEFAULT_PATH_CACHE_CONFIG, EMPTY_COLLISION_RESULT, EMPTY_PATH_RESULT, EMPTY_PLAN_RESULT, FlowController, GridPathfinderAdapter, HPAPathfinder, IncrementalAStarPathfinder, IncrementalGridPathPlannerAdapter, IndexedBinaryHeap, JPSPathfinder, NavMeshPathPlannerAdapter, ORCALocalAvoidanceAdapter, PassPermission, PathCache, PathPlanState, chebyshevDistance, createAStarPathfinder, createAStarPlanner, createDefaultCollisionResolver, createFlowController, createHPAPathfinder, createHPAPlanner, createIncrementalAStarPathfinder, createIncrementalAStarPlanner, createJPSPathfinder, createJPSPlanner, createNavMeshPathPlanner, createORCAAvoidance, createPathCache, createPoint, euclideanDistance, isIncrementalPlanner, manhattanDistance, octileDistance } from "./chunk-ZYGBA7VK.js"; import { DEFAULT_REPLANNING_CONFIG, EMPTY_PROGRESS, PathfindingState } from "./chunk-YKA3PWU3.js"; import "./chunk-H5EFZBBT.js"; import { CollisionResolver, DEFAULT_AGENT_PARAMS, DEFAULT_COLLISION_CONFIG, DEFAULT_ORCA_CONFIG, EMPTY_COLLISION, KDTree, ORCASolver, createCollisionResolver, createKDTree, createORCASolver, solveORCALinearProgram } from "./chunk-3VEX32JO.js"; import { __name, __publicField } from "./chunk-T626JPC7.js"; // src/core/GridPathfinder.ts var CLOSED_FLAG = 1; var OPENED_FLAG = 2; var BACKWARD_CLOSED = 4; var BACKWARD_OPENED = 8; var _a; var GridState = (_a = class { constructor(width, height, bidirectional = false) { __publicField(this, "size"); __publicField(this, "width"); __publicField(this, "g"); __publicField(this, "f"); __publicField(this, "flags"); __publicField(this, "parent"); __publicField(this, "heapIndex"); __publicField(this, "version"); __publicField(this, "currentVersion", 1); // 双向搜索额外状态 __publicField(this, "gBack", null); __publicField(this, "fBack", null); __publicField(this, "parentBack", null); __publicField(this, "heapIndexBack", null); this.width = width; this.size = width * height; this.g = new Float32Array(this.size); this.f = new Float32Array(this.size); this.flags = new Uint8Array(this.size); this.parent = new Int32Array(this.size); this.heapIndex = new Int32Array(this.size); this.version = new Uint32Array(this.size); if (bidirectional) { this.gBack = new Float32Array(this.size); this.fBack = new Float32Array(this.size); this.parentBack = new Int32Array(this.size); this.heapIndexBack = new Int32Array(this.size); } } reset() { this.currentVersion++; if (this.currentVersion > 4294967295) { this.version.fill(0); this.currentVersion = 1; } } isInit(i) { return this.version[i] === this.currentVersion; } init(i) { if (!this.isInit(i)) { this.g[i] = Infinity; this.f[i] = Infinity; this.flags[i] = 0; this.parent[i] = -1; this.heapIndex[i] = -1; if (this.gBack) { this.gBack[i] = Infinity; this.fBack[i] = Infinity; this.parentBack[i] = -1; this.heapIndexBack[i] = -1; } this.version[i] = this.currentVersion; } } // Forward getG(i) { return this.isInit(i) ? this.g[i] : Infinity; } setG(i, v) { this.init(i); this.g[i] = v; } getF(i) { return this.isInit(i) ? this.f[i] : Infinity; } setF(i, v) { this.init(i); this.f[i] = v; } getParent(i) { return this.isInit(i) ? this.parent[i] : -1; } setParent(i, v) { this.init(i); this.parent[i] = v; } getHeapIndex(i) { return this.isInit(i) ? this.heapIndex[i] : -1; } setHeapIndex(i, v) { this.init(i); this.heapIndex[i] = v; } isClosed(i) { return this.isInit(i) && (this.flags[i] & CLOSED_FLAG) !== 0; } setClosed(i) { this.init(i); this.flags[i] |= CLOSED_FLAG; } isOpened(i) { return this.isInit(i) && (this.flags[i] & OPENED_FLAG) !== 0; } setOpened(i) { this.init(i); this.flags[i] |= OPENED_FLAG; } // Backward getGBack(i) { return this.isInit(i) ? this.gBack[i] : Infinity; } setGBack(i, v) { this.init(i); this.gBack[i] = v; } getFBack(i) { return this.isInit(i) ? this.fBack[i] : Infinity; } setFBack(i, v) { this.init(i); this.fBack[i] = v; } getParentBack(i) { return this.isInit(i) ? this.parentBack[i] : -1; } setParentBack(i, v) { this.init(i); this.parentBack[i] = v; } getHeapIndexBack(i) { return this.isInit(i) ? this.heapIndexBack[i] : -1; } setHeapIndexBack(i, v) { this.init(i); this.heapIndexBack[i] = v; } isClosedBack(i) { return this.isInit(i) && (this.flags[i] & BACKWARD_CLOSED) !== 0; } setClosedBack(i) { this.init(i); this.flags[i] |= BACKWARD_CLOSED; } isOpenedBack(i) { return this.isInit(i) && (this.flags[i] & BACKWARD_OPENED) !== 0; } setOpenedBack(i) { this.init(i); this.flags[i] |= BACKWARD_OPENED; } }, __name(_a, "GridState"), _a); var _a2; var GridHeap = (_a2 = class { constructor(state, isBack = false) { __publicField(this, "heap", []); __publicField(this, "state"); __publicField(this, "isBack"); this.state = state; this.isBack = isBack; } get size() { return this.heap.length; } get isEmpty() { return this.heap.length === 0; } getF(i) { return this.isBack ? this.state.getFBack(i) : this.state.getF(i); } getHeapIndex(i) { return this.isBack ? this.state.getHeapIndexBack(i) : this.state.getHeapIndex(i); } setHeapIndex(i, v) { if (this.isBack) this.state.setHeapIndexBack(i, v); else this.state.setHeapIndex(i, v); } push(i) { this.setHeapIndex(i, this.heap.length); this.heap.push(i); this.bubbleUp(this.heap.length - 1); } pop() { if (this.heap.length === 0) return -1; const result = this.heap[0]; this.setHeapIndex(result, -1); const last = this.heap.pop(); if (this.heap.length > 0) { this.heap[0] = last; this.setHeapIndex(last, 0); this.sinkDown(0); } return result; } update(i) { const pos = this.getHeapIndex(i); if (pos >= 0 && pos < this.heap.length) { this.bubbleUp(pos); this.sinkDown(this.getHeapIndex(i)); } } clear() { this.heap.length = 0; } bubbleUp(pos) { const idx = this.heap[pos]; const f = this.getF(idx); while (pos > 0) { const pp = pos - 1 >> 1; const pi = this.heap[pp]; if (f >= this.getF(pi)) break; this.heap[pos] = pi; this.setHeapIndex(pi, pos); pos = pp; } this.heap[pos] = idx; this.setHeapIndex(idx, pos); } sinkDown(pos) { const len = this.heap.length; const idx = this.heap[pos]; const f = this.getF(idx); const half = len >> 1; while (pos < half) { const left = (pos << 1) + 1; const right = left + 1; let smallest = pos, smallestF = f; const lf = this.getF(this.heap[left]); if (lf < smallestF) { smallest = left; smallestF = lf; } if (right < len) { const rf = this.getF(this.heap[right]); if (rf < smallestF) smallest = right; } if (smallest === pos) break; const si = this.heap[smallest]; this.heap[pos] = si; this.setHeapIndex(si, pos); pos = smallest; } this.heap[pos] = idx; this.setHeapIndex(idx, pos); } }, __name(_a2, "GridHeap"), _a2); var _GridPathfinder = class _GridPathfinder { constructor(map, config) { __publicField(this, "map"); __publicField(this, "mode"); __publicField(this, "state"); __publicField(this, "openList"); __publicField(this, "openListBack"); this.map = map; this.mode = config?.mode ?? "fast"; const isBidir = this.mode === "bidirectional"; this.state = new GridState(map.width, map.height, isBidir); this.openList = new GridHeap(this.state, false); this.openListBack = isBidir ? new GridHeap(this.state, true) : null; } findPath(startX, startY, endX, endY, options) { if (this.mode === "bidirectional") { return this.findPathBidirectional(startX, startY, endX, endY, options); } return this.findPathUnidirectional(startX, startY, endX, endY, options); } findPathUnidirectional(startX, startY, endX, endY, options) { const opts = options ? { ...DEFAULT_PATHFINDING_OPTIONS, ...options } : DEFAULT_PATHFINDING_OPTIONS; const { width, height } = this.map; this.state.reset(); this.openList.clear(); if (!this.validate(startX, startY, endX, endY)) return EMPTY_PATH_RESULT; const startIdx = startY * width + startX; const endIdx = endY * width + endX; if (startIdx === endIdx) { return { found: true, path: [ { x: startX, y: startY } ], cost: 0, nodesSearched: 1 }; } const hw = opts.heuristicWeight; const h0 = this.map.heuristic({ x: startX, y: startY }, { x: endX, y: endY }) * hw; this.state.setG(startIdx, 0); this.state.setF(startIdx, h0); this.state.setOpened(startIdx); this.openList.push(startIdx); let searched = 0; const maxNodes = opts.maxNodes; const { allowDiagonal, avoidCorners, diagonalCost } = this.map["options"]; const nodes = this.map["nodes"]; const dx = allowDiagonal ? [ 0, 1, 1, 1, 0, -1, -1, -1 ] : [ 0, 1, 0, -1 ]; const dy = allowDiagonal ? [ -1, -1, 0, 1, 1, 1, 0, -1 ] : [ -1, 0, 1, 0 ]; const dirCount = dx.length; while (!this.openList.isEmpty && searched < maxNodes) { const cur = this.openList.pop(); this.state.setClosed(cur); searched++; if (cur === endIdx) { return this.buildPath(startIdx, endIdx, searched); } const cx = cur % width, cy = cur / width | 0; const curG = this.state.getG(cur); for (let d = 0; d < dirCount; d++) { const nx = cx + dx[d], ny = cy + dy[d]; if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; const neighbor = nodes[ny][nx]; if (!neighbor.walkable) continue; if (avoidCorners && dx[d] !== 0 && dy[d] !== 0) { if (!nodes[cy][cx + dx[d]].walkable || !nodes[cy + dy[d]][cx].walkable) continue; } const ni = ny * width + nx; if (this.state.isClosed(ni)) continue; const isDiag = dx[d] !== 0 && dy[d] !== 0; const cost = isDiag ? neighbor.cost * diagonalCost : neighbor.cost; const tentG = curG + cost; if (!this.state.isOpened(ni)) { const h = this.map.heuristic({ x: nx, y: ny }, { x: endX, y: endY }) * hw; this.state.setG(ni, tentG); this.state.setF(ni, tentG + h); this.state.setParent(ni, cur); this.state.setOpened(ni); this.openList.push(ni); } else if (tentG < this.state.getG(ni)) { const h = this.state.getF(ni) - this.state.getG(ni); this.state.setG(ni, tentG); this.state.setF(ni, tentG + h); this.state.setParent(ni, cur); this.openList.update(ni); } } } return { found: false, path: [], cost: 0, nodesSearched: searched }; } findPathBidirectional(startX, startY, endX, endY, options) { const opts = options ? { ...DEFAULT_PATHFINDING_OPTIONS, ...options } : DEFAULT_PATHFINDING_OPTIONS; const { width, height } = this.map; this.state.reset(); this.openList.clear(); this.openListBack.clear(); if (!this.validate(startX, startY, endX, endY)) return EMPTY_PATH_RESULT; const startIdx = startY * width + startX; const endIdx = endY * width + endX; if (startIdx === endIdx) { return { found: true, path: [ { x: startX, y: startY } ], cost: 0, nodesSearched: 1 }; } const hw = opts.heuristicWeight; const startPos = { x: startX, y: startY }; const endPos = { x: endX, y: endY }; const hf = this.map.heuristic(startPos, endPos) * hw; this.state.setG(startIdx, 0); this.state.setF(startIdx, hf); this.state.setOpened(startIdx); this.openList.push(startIdx); const hb = this.map.heuristic(endPos, startPos) * hw; this.state.setGBack(endIdx, 0); this.state.setFBack(endIdx, hb); this.state.setOpenedBack(endIdx); this.openListBack.push(endIdx); let searched = 0; const maxNodes = opts.maxNodes; let meetIdx = -1, bestCost = Infinity; const { allowDiagonal, avoidCorners, diagonalCost } = this.map["options"]; const nodes = this.map["nodes"]; const dx = allowDiagonal ? [ 0, 1, 1, 1, 0, -1, -1, -1 ] : [ 0, 1, 0, -1 ]; const dy = allowDiagonal ? [ -1, -1, 0, 1, 1, 1, 0, -1 ] : [ -1, 0, 1, 0 ]; const dirCount = dx.length; while ((!this.openList.isEmpty || !this.openListBack.isEmpty) && searched < maxNodes) { if (!this.openList.isEmpty) { const cur = this.openList.pop(); this.state.setClosed(cur); searched++; const curG = this.state.getG(cur); if (this.state.isClosedBack(cur)) { const total = curG + this.state.getGBack(cur); if (total < bestCost) { bestCost = total; meetIdx = cur; } } if (meetIdx !== -1 && curG >= bestCost) break; const cx = cur % width, cy = cur / width | 0; for (let d = 0; d < dirCount; d++) { const nx = cx + dx[d], ny = cy + dy[d]; if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; const neighbor = nodes[ny][nx]; if (!neighbor.walkable) continue; if (avoidCorners && dx[d] !== 0 && dy[d] !== 0) { if (!nodes[cy][cx + dx[d]].walkable || !nodes[cy + dy[d]][cx].walkable) continue; } const ni = ny * width + nx; if (this.state.isClosed(ni)) continue; const isDiag = dx[d] !== 0 && dy[d] !== 0; const cost = isDiag ? neighbor.cost * diagonalCost : neighbor.cost; const tentG = curG + cost; if (!this.state.isOpened(ni)) { const h = this.map.heuristic({ x: nx, y: ny }, endPos) * hw; this.state.setG(ni, tentG); this.state.setF(ni, tentG + h); this.state.setParent(ni, cur); this.state.setOpened(ni); this.openList.push(ni); } else if (tentG < this.state.getG(ni)) { const h = this.state.getF(ni) - this.state.getG(ni); this.state.setG(ni, tentG); this.state.setF(ni, tentG + h); this.state.setParent(ni, cur); this.openList.update(ni); } } } if (!this.openListBack.isEmpty) { const cur = this.openListBack.pop(); this.state.setClosedBack(cur); searched++; const curG = this.state.getGBack(cur); if (this.state.isClosed(cur)) { const total = curG + this.state.getG(cur); if (total < bestCost) { bestCost = total; meetIdx = cur; } } if (meetIdx !== -1 && curG >= bestCost) break; const cx = cur % width, cy = cur / width | 0; for (let d = 0; d < dirCount; d++) { const nx = cx + dx[d], ny = cy + dy[d]; if (nx < 0 || nx >= width || ny < 0 || ny >= height) continue; const neighbor = nodes[ny][nx]; if (!neighbor.walkable) continue; if (avoidCorners && dx[d] !== 0 && dy[d] !== 0) { if (!nodes[cy][cx + dx[d]].walkable || !nodes[cy + dy[d]][cx].walkable) continue; } const ni = ny * width + nx; if (this.state.isClosedBack(ni)) continue; const isDiag = dx[d] !== 0 && dy[d] !== 0; const cost = isDiag ? neighbor.cost * diagonalCost : neighbor.cost; const tentG = curG + cost; if (!this.state.isOpenedBack(ni)) { const h = this.map.heuristic({ x: nx, y: ny }, startPos) * hw; this.state.setGBack(ni, tentG); this.state.setFBack(ni, tentG + h); this.state.setParentBack(ni, cur); this.state.setOpenedBack(ni); this.openListBack.push(ni); } else if (tentG < this.state.getGBack(ni)) { const h = this.state.getFBack(ni) - this.state.getGBack(ni); this.state.setGBack(ni, tentG); this.state.setFBack(ni, tentG + h); this.state.setParentBack(ni, cur); this.openListBack.update(ni); } } } } if (meetIdx === -1) { return { found: false, path: [], cost: 0, nodesSearched: searched }; } return this.buildPathBidirectional(startIdx, endIdx, meetIdx, searched); } validate(startX, startY, endX, endY) { const { width, height } = this.map; if (startX < 0 || startX >= width || startY < 0 || startY >= height) return false; if (endX < 0 || endX >= width || endY < 0 || endY >= height) return false; return this.map.isWalkable(startX, startY) && this.map.isWalkable(endX, endY); } buildPath(startIdx, endIdx, searched) { const w = this.state.width; const path = []; let cur = endIdx; while (cur !== -1) { path.push({ x: cur % w, y: cur / w | 0 }); cur = cur === startIdx ? -1 : this.state.getParent(cur); } path.reverse(); return { found: true, path, cost: this.state.getG(endIdx), nodesSearched: searched }; } buildPathBidirectional(startIdx, endIdx, meetIdx, searched) { const w = this.state.width; const path = []; let cur = meetIdx; while (cur !== -1 && cur !== startIdx) { path.push({ x: cur % w, y: cur / w | 0 }); cur = this.state.getParent(cur); } path.push({ x: startIdx % w, y: startIdx / w | 0 }); path.reverse(); cur = this.state.getParentBack(meetIdx); while (cur !== -1 && cur !== endIdx) { path.push({ x: cur % w, y: cur / w | 0 }); cur = this.state.getParentBack(cur); } if (meetIdx !== endIdx) { path.push({ x: endIdx % w, y: endIdx / w | 0 }); } const cost = this.state.getG(meetIdx) + this.state.getGBack(meetIdx); return { found: true, path, cost, nodesSearched: searched }; } clear() { this.state.reset(); this.openList.clear(); this.openListBack?.clear(); } }; __name(_GridPathfinder, "GridPathfinder"); var GridPathfinder = _GridPathfinder; function createGridPathfinder(map, config) { return new GridPathfinder(map, config); } __name(createGridPathfinder, "createGridPathfinder"); // src/core/PathValidator.ts var _PathValidator = class _PathValidator { /** * @zh 验证路径段的有效性 * @en Validate path segment validity * * @param path - @zh 要验证的路径 @en Path to validate * @param fromIndex - @zh 起始索引 @en Start index * @param toIndex - @zh 结束索引 @en End index * @param map - @zh 地图实例 @en Map instance * @returns @zh 验证结果 @en Validation result */ validatePath(path, fromIndex, toIndex, map) { const end = Math.min(toIndex, path.length); for (let i = fromIndex; i < end; i++) { const point = path[i]; const x = Math.floor(point.x); const y = Math.floor(point.y); if (!map.isWalkable(x, y)) { return { valid: false, invalidIndex: i }; } if (i > fromIndex) { const prev = path[i - 1]; if (!this.checkLineOfSight(prev.x, prev.y, point.x, point.y, map)) { return { valid: false, invalidIndex: i }; } } } return { valid: true, invalidIndex: -1 }; } /** * @zh 检查两点之间的视线(使用 Bresenham 算法) * @en Check line of sight between two points (using Bresenham algorithm) * * @param x1 - @zh 起点 X @en Start X * @param y1 - @zh 起点 Y @en Start Y * @param x2 - @zh 终点 X @en End X * @param y2 - @zh 终点 Y @en End Y * @param map - @zh 地图实例 @en Map instance * @returns @zh 是否有视线 @en Whether there is line of sight */ checkLineOfSight(x1, y1, x2, y2, map) { const ix1 = Math.floor(x1); const iy1 = Math.floor(y1); const ix2 = Math.floor(x2); const iy2 = Math.floor(y2); let dx = Math.abs(ix2 - ix1); let dy = Math.abs(iy2 - iy1); let x = ix1; let y = iy1; const sx = ix1 < ix2 ? 1 : -1; const sy = iy1 < iy2 ? 1 : -1; if (dx > dy) { let err = dx / 2; while (x !== ix2) { if (!map.isWalkable(x, y)) { return false; } err -= dy; if (err < 0) { y += sy; err += dx; } x += sx; } } else { let err = dy / 2; while (y !== iy2) { if (!map.isWalkable(x, y)) { return false; } err -= dx; if (err < 0) { x += sx; err += dy; } y += sy; } } return map.isWalkable(ix2, iy2); } }; __name(_PathValidator, "PathValidator"); var PathValidator = _PathValidator; var _ObstacleChangeManager = class _ObstacleChangeManager { constructor() { __publicField(this, "changes", /* @__PURE__ */ new Map()); __publicField(this, "epoch", 0); } /** * @zh 记录障碍物变化 * @en Record obstacle change * * @param x - @zh X 坐标 @en X coordinate * @param y - @zh Y 坐标 @en Y coordinate * @param wasWalkable - @zh 变化前是否可通行 @en Was walkable before change */ recordChange(x, y, wasWalkable) { const key = `${x},${y}`; this.changes.set(key, { x, y, wasWalkable, timestamp: Date.now() }); } /** * @zh 获取影响区域 * @en Get affected region * * @returns @zh 影响区域或 null(如果没有变化)@en Affected region or null if no changes */ getAffectedRegion() { if (this.changes.size === 0) { return null; } let minX = Infinity; let minY = Infinity; let maxX = -Infinity; let maxY = -Infinity; for (const change of this.changes.values()) { minX = Math.min(minX, change.x); minY = Math.min(minY, change.y); maxX = Math.max(maxX, change.x); maxY = Math.max(maxY, change.y); } return { minX, minY, maxX, maxY }; } /** * @zh 获取所有变化 * @en Get all changes * * @returns @zh 变化列表 @en List of changes */ getChanges() { return Array.from(this.changes.values()); } /** * @zh 检查是否有变化 * @en Check if there are changes * * @returns @zh 是否有变化 @en Whether there are changes */ hasChanges() { return this.changes.size > 0; } /** * @zh 获取当前 epoch * @en Get current epoch * * @returns @zh 当前 epoch @en Current epoch */ getEpoch() { return this.epoch; } /** * @zh 清空变化记录并推进 epoch * @en Clear changes and advance epoch */ flush() { this.changes.clear(); this.epoch++; } /** * @zh 清空所有状态 * @en Clear all state */ clear() { this.changes.clear(); this.epoch = 0; } }; __name(_ObstacleChangeManager, "ObstacleChangeManager"); var ObstacleChangeManager = _ObstacleChangeManager; function createPathValidator() { return new PathValidator(); } __name(createPathValidator, "createPathValidator"); function createObstacleChangeManager() { return new ObstacleChangeManager(); } __name(createObstacleChangeManager, "createObstacleChangeManager"); // src/grid/GridMap.ts var _GridNode = class _GridNode { constructor(x, y, width, walkable = true, cost = 1) { __publicField(this, "id"); __publicField(this, "position"); __publicField(this, "x"); __publicField(this, "y"); __publicField(this, "cost"); __publicField(this, "walkable"); this.x = x; this.y = y; this.id = y * width + x; this.position = createPoint(x, y); this.walkable = walkable; this.cost = cost; } }; __name(_GridNode, "GridNode"); var GridNode = _GridNode; var DIRECTIONS_4 = [ { dx: 0, dy: -1 }, { dx: 1, dy: 0 }, { dx: 0, dy: 1 }, { dx: -1, dy: 0 } // Left ]; var DIRECTIONS_8 = [ { dx: 0, dy: -1 }, { dx: 1, dy: -1 }, { dx: 1, dy: 0 }, { dx: 1, dy: 1 }, { dx: 0, dy: 1 }, { dx: -1, dy: 1 }, { dx: -1, dy: 0 }, { dx: -1, dy: -1 } // Up-Left ]; var DEFAULT_GRID_OPTIONS = { allowDiagonal: true, diagonalCost: Math.SQRT2, avoidCorners: true, heuristic: octileDistance }; var _GridMap = class _GridMap { constructor(width, height, options) { __publicField(this, "width"); __publicField(this, "height"); __publicField(this, "nodes"); __publicField(this, "options"); if (width <= 0 || !Number.isFinite(width) || !Number.isInteger(width)) { throw new Error(`width must be a positive integer, got: ${width}`); } if (height <= 0 || !Number.isFinite(height) || !Number.isInteger(height)) { throw new Error(`height must be a positive integer, got: ${height}`); } this.width = width; this.height = height; this.options = { ...DEFAULT_GRID_OPTIONS, ...options }; this.nodes = this.createNodes(); } /** * @zh 创建网格节点 * @en Create grid nodes */ createNodes() { const nodes = []; for (let y = 0; y < this.height; y++) { nodes[y] = []; for (let x = 0; x < this.width; x++) { nodes[y][x] = new GridNode(x, y, this.width, true, 1); } } return nodes; } /** * @zh 获取指定位置的节点 * @en Get node at position */ getNodeAt(x, y) { if (!this.isInBounds(x, y)) { return null; } return this.nodes[y][x]; } /** * @zh 检查坐标是否在边界内 * @en Check if coordinates are within bounds */ isInBounds(x, y) { return x >= 0 && x < this.width && y >= 0 && y < this.height; } /** * @zh 检查位置是否可通行 * @en Check if position is walkable */ isWalkable(x, y) { const node = this.getNodeAt(x, y); return node !== null && node.walkable; } /** * @zh 设置位置是否可通行 * @en Set position walkability */ setWalkable(x, y, walkable) { const node = this.getNodeAt(x, y); if (node) { node.walkable = walkable; } } /** * @zh 设置位置的移动代价 * @en Set movement cost at position * * @param x - @zh X 坐标 @en X coordinate * @param y - @zh Y 坐标 @en Y coordinate * @param cost - @zh 移动代价,必须为正数 @en Movement cost, must be positive * @throws @zh 如果 cost 不是正数则抛出错误 @en Throws if cost is not positive */ setCost(x, y, cost) { if (cost <= 0 || !Number.isFinite(cost)) { throw new Error(`cost must be a positive finite number, got: ${cost}`); } const node = this.getNodeAt(x, y); if (node) { node.cost = cost; } } /** * @zh 获取节点的邻居 * @en Get neighbors of a node */ getNeighbors(node) { const neighbors = []; const { x, y } = node.position; const directions = this.options.allowDiagonal ? DIRECTIONS_8 : DIRECTIONS_4; for (let i = 0; i < directions.length; i++) { const dir = directions[i]; const nx = x + dir.dx; const ny = y + dir.dy; if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) { continue; } const neighbor = this.nodes[ny][nx]; if (!neighbor.walkable) { continue; } if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) { const hNode = this.nodes[y][x + dir.dx]; const vNode = this.nodes[y + dir.dy][x]; if (!hNode.walkable || !vNode.walkable) { continue; } } neighbors.push(neighbor); } return neighbors; } /** * @zh 遍历节点的邻居(零分配) * @en Iterate over neighbors (zero allocation) */ forEachNeighbor(node, callback) { const { x, y } = node.position; const directions = this.options.allowDiagonal ? DIRECTIONS_8 : DIRECTIONS_4; for (let i = 0; i < directions.length; i++) { const dir = directions[i]; const nx = x + dir.dx; const ny = y + dir.dy; if (nx < 0 || nx >= this.width || ny < 0 || ny >= this.height) { continue; } const neighbor = this.nodes[ny][nx]; if (!neighbor.walkable) { continue; } if (this.options.avoidCorners && dir.dx !== 0 && dir.dy !== 0) { const hNode = this.nodes[y][x + dir.dx]; const vNode = this.nodes[y + dir.dy][x]; if (!hNode.walkable || !vNode.walkable) { continue; } } if (callback(neighbor) === false) { return; } } } /** * @zh 计算启发式距离 * @en Calculate heuristic distance */ heuristic(a, b) { return this.options.heuristic(a, b); } /** * @zh 计算移动代价 * @en Calculate movement cost */ getMovementCost(from, to) { const dx = Math.abs(from.position.x - to.position.x); const dy = Math.abs(from.position.y - to.position.y); if (dx !== 0 && dy !== 0) { return to.cost * this.options.diagonalCost; } return to.cost; } /** * @zh 从二维数组加载地图 * @en Load map from 2D array * * @param data - @zh 0=可通行,非0=不可通行 @en 0=walkable, non-0=blocked */ loadFromArray(data) { for (let y = 0; y < Math.min(data.length, this.height); y++) { for (let x = 0; x < Math.min(data[y].length, this.width); x++) { this.nodes[y][x].walkable = data[y][x] === 0; } } } /** * @zh 从字符串加载地图 * @en Load map from string * * @param str - @zh 地图字符串,'.'=可通行,'#'=障碍 @en Map string, '.'=walkable, '#'=blocked */ loadFromString(str) { const lines = str.trim().split("\n"); for (let y = 0; y < Math.min(lines.length, this.height); y++) { const line = lines[y]; for (let x = 0; x < Math.min(line.length, this.width); x++) { this.nodes[y][x].walkable = line[x] !== "#"; } } } /** * @zh 导出为字符串 * @en Export to string */ toString() { let result = ""; for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { result += this.nodes[y][x].walkable ? "." : "#"; } result += "\n"; } return result; } /** * @zh 重置所有节点为可通行 * @en Reset all nodes to walkable */ reset() { for (let y = 0; y < this.height; y++) { for (let x = 0; x < this.width; x++) { this.nodes[y][x].walkable = true; this.nodes[y][x].cost = 1; } } } /** * @zh 设置矩形区域的通行性 * @en Set walkability for a rectangle region */ setRectWalkable(x, y, width, height, walkable) { for (let dy = 0; dy < height; dy++) { for (let dx = 0; dx < width; dx++) { this.setWalkable(x + dx, y + dy, walkable); } } } }; __name(_GridMap, "GridMap"); var GridMap = _GridMap; function createGridMap(width, height, options) { return new GridMap(width, height, options); } __name(createGridMap, "createGridMap"); // src/navmesh/NavMesh.ts var _a3; var NavMeshNode = (_a3 = class { constructor(polygon) { __publicField(this, "id"); __publicField(this, "position"); __publicField(this, "cost"); __publicField(this, "walkable"); __publicField(this, "polygon"); this.id = polygon.id; this.position = polygon.center; this.cost = 1; this.walkable = true; this.polygon = polygon; } }, __name(_a3, "NavMeshNode"), _a3); var _NavMesh = class _NavMesh { constructor() { __publicField(this, "polygons", /* @__PURE__ */ new Map()); __publicField(this, "nodes", /* @__PURE__ */ new Map()); __publicField(this, "nextId", 0); // @zh 动态障碍物支持 // @en Dynamic obstacle support __publicField(this, "obstacles", /* @__PURE__ */ new Map()); __publicField(this, "nextObstacleId", 0); __publicField(this, "disabledPolygons", /* @__PURE__ */ new Set()); } /** * @zh 添加导航多边形 * @en Add navigation polygon * * @returns @zh 多边形ID @en Polygon ID */ addPolygon(vertices, neighbors = []) { const id = this.nextId++; const center = this.calculateCenter(vertices); const polygon = { id, vertices, center, neighbors, portals: /* @__PURE__ */ new Map() }; this.polygons.set(id, polygon); this.nodes.set(id, new NavMeshNode(polygon)); return id; } /** * @zh 设置两个多边形之间的连接 * @en Set connection between two polygons */ setConnection(polyA, polyB, portal) { const polygonA = this.polygons.get(polyA); const polygonB = this.polygons.get(polyB); if (!polygonA || !polygonB) { return; } const neighborsA = [ ...polygonA.neighbors ]; const portalsA = new Map(polygonA.portals); if (!neighborsA.includes(polyB)) { neighborsA.push(polyB); } portalsA.set(polyB, portal); this.polygons.set(polyA, { ...polygonA, neighbors: neighborsA, portals: portalsA }); const reversePortal = { left: portal.right, right: portal.left }; const neighborsB = [ ...polygonB.neighbors ]; const portalsB = new Map(polygonB.portals); if (!neighborsB.includes(polyA)) { neighborsB.push(polyA); } portalsB.set(polyA, reversePortal); this.polygons.set(polyB, { ...polygonB, neighbors: neighborsB, portals: portalsB }); } /** * @zh 自动检测并建立相邻多边形的连接 * @en Auto-detect and build connections between adjacent polygons */ build() { const polygonList = Array.from(this.polygons.values()); for (let i = 0; i < polygonList.length; i++) { for (let j = i + 1; j < polygonList.length; j++) { const polyA = polygonList[i]; const polyB = polygonList[j]; const sharedEdge = this.findSharedEdge(polyA.vertices, polyB.vertices); if (sharedEdge) { this.setConnection(polyA.id, polyB.id, sharedEdge); } } } } /** * @zh 查找两个多边形的共享边 * @en Find shared edge between two polygons */ findSharedEdge(verticesA, verticesB) { const epsilon = 1e-4; for (let i = 0; i < verticesA.length; i++) { const a1 = verticesA[i]; const a2 = verticesA[(i + 1) % verticesA.length]; for (let j = 0; j < verticesB.length; j++) { const b1 = verticesB[j]; const b2 = verticesB[(j + 1) % verticesB.length]; const match1 = Math.abs(a1.x - b2.x) < epsilon && Math.abs(a1.y - b2.y) < epsilon && Math.abs(a2.x - b1.x) < epsilon && Math.abs(a2.y - b1.y) < epsilon; const match2 = Math.abs(a1.x - b1.x) < epsilon && Math.abs(a1.y - b1.y) < epsilon && Math.abs(a2.x - b2.x) < epsilon && Math.abs(a2.y - b2.y) < epsilon; if (match1 || match2) { return { left: a1, right: a2 }; } } } return null; } /** * @zh 计算多边形中心 * @en Calculate polygon center */ calculateCenter(vertices) { let x = 0; let y = 0; for (const v of vertices) { x += v.x; y += v.y; } return createPoint(x / vertices.length, y / vertices.length); } /** * @zh 查找包含点的多边形 * @en Find polygon containing point */ findPolygonAt(x, y) { for (const polygon of this.polygons.values()) { if (this.isPointInPolygon(x, y, polygon.vertices)) { return polygon; } } return null; } /** * @zh 检查点是否在多边形内 * @en Check if point is inside polygon */ isPointInPolygon(x, y, vertices) { let inside = false; const n = vertices.length; for (let i = 0, j = n - 1; i < n; j = i++) { const xi = vertices[i].x; const yi = vertices[i].y; const xj = vertices[j].x; const yj = vertices[j].y; if (yi > y !== yj > y && x < (xj - xi) * (y - yi) / (yj - yi) + xi) { inside = !inside; } } return inside; } // ========================================================================== // IPathfindingMap 接口实现 | IPathfindingMap Interface Implementation // ========================================================================== getNodeAt(x, y) { const polygon = this.findPolygonAt(x, y); return polygon ? this.nodes.get(polygon.id) ?? null : null; } getNeighbors(node) { const navNode = node; const neighbors = []; for (const neighborId of navNode.polygon.neighbors) { const neighbor = this.nodes.get(neighborId); if (neighbor) { neighbors.push(neighbor); } } return neighbors; } heuristic(a, b) { return euclideanDistance(a, b); } getMovementCost(from, to) { return euclideanDistance(from.position, to.position); } isWalkable(x, y) { return this.findPolygonAt(x, y) !== null; } // ========================================================================== // 寻路 | Pathfinding // ========================================================================== /** * @zh 在导航网格上寻路 * @en Find path on navigation mesh */ findPath(startX, startY, endX, endY, options) { const opts = { ...DEFAULT_PATHFINDING_OPTIONS, ...options }; const startPolygon = this.findPolygonAt(startX, startY); const endPolygon = this.findPolygonAt(endX, endY); if (!startPolygon || !endPolygon) { return EMPTY_PATH_RESULT; } if (startPolygon.id === endPolygon.id) { return { found: true, path: [ createPoint(startX, startY), createPoint(endX, endY) ], cost: euclideanDistance(createPoint(startX, startY), createPoint(endX, endY)), nodesSearched: 1 }; } const polygonPath = this.findPolygonPath(startPolygon, endPolygon, opts); if (!polygonPath.found) { return EMPTY_PATH_RESULT; } const start = createPoint(startX, startY); const end = createPoint(endX, endY); const pointPath = this.funnelPath(start, end, polygonPath.polygons, opts.agentRadius); return { found: true, path: pointPath, cost: this.calculatePathLength(pointPath), nodesSearched: polygonPath.nodesSearched }; } /** * @zh 在多边形图上寻路 * @en Find path on polygon graph * * @param start - @zh 起始多边形 @en Start polygon * @param end - @zh 目标多边形 @en End polygon * @param opts - @zh 寻路选项 @en Pathfinding options * @param checkObstacles - @zh 是否检查障碍物 @en Whether to check obstacles */ findPolygonPath(start, end, opts, checkObstacles = false) { const openList = new BinaryHeap((a, b) => a.f - b.f); const closed = /* @__PURE__ */ new Set(); const states = /* @__PURE__ */ new Map(); const startState = { polygon: start, g: 0, f: euclideanDistance(start.center, end.center) * opts.heuristicWeight, parent: null }; states.set(start.id, startState); openList.push(startState); let nodesSearched = 0; while (!openList.isEmpty && nodesSearched < opts.maxNodes) { const current = openList.pop(); nodesSearched++; if (current.polygon.id === end.id) { const path = []; let state = current; while (state) { path.unshift(state.polygon); state = state.parent; } return { found: true, polygons: path, nodesSearched }; } closed.add(current.polygon.id); for (const neighborId of current.polygon.neighbors) { if (closed.has(neighborId)) { continue; } if (checkObstacles && this.isPolygonBlocked(neighborId)) { continue; } const neighborPolygon = this.polygons.get(neighborId); if (!neighborPolygon) { continue; } const g = current.g + euclideanDistance(current.polygon.center, neighborPolygon.center); let neighborState = states.get(neighborId); if (!neighborState) { neighborState = { polygon: neighborPolygon, g, f: g + euclideanDistance(neighborPolygon.center, end.center) * opts.heuristicWeight, parent: current }; states.set(neighborId, neighborState); openList.push(neighborState); } else if (g < neighborState.g) { neighborState.g = g; neighborState.f = g + euclideanDistance(neighborPolygon.center, end.center) * opts.heuristicWeight; neighborState.parent = current; openList.update(neighborState); } } } return { found: false, polygons: [], nodesSearched }; } /** * @zh 使用漏斗算法优化路径(支持代理半径) * @en Optimize path using funnel algorithm (supports agent radius) * * @param start - @zh 起点 @en Start point * @param end - @zh 终点 @en End point * @param polygons - @zh 多边形路径 @en Polygon path * @param agentRadius - @zh 代理半径 @en Agent radius */ funnelPath(start, end, polygons, agentRadius = 0) { if (polygons.length <= 1) { return [ start, end ]; } const portals = []; for (let i = 0; i < polygons.length - 1; i++) { const portal = polygons[i].portals.get(polygons[i + 1].id); if (portal) { if (agentRadius > 0) { const shrunk = this.shrinkPortal(portal.left, portal.right, agentRadius); portals.push({ left: shrunk.left, right: shrunk.right, originalLeft: portal.left, originalRight: portal.right }); } else { portals.push({ left: portal.left, right: portal.right, originalLeft: portal.left, originalRight: portal.right }); } } } if (portals.length === 0) { return [ start, end ]; } const path = [ start ]; let apex = start; let apexOriginal = start; let leftIndex = 0; let rightIndex = 0; let left = portals[0].left; let right = portals[0].right; let leftOriginal = portals[0].originalLeft; let rightOriginal = portals[0].originalRight; for (let i = 1; i <= portals.length; i++) { const nextLeft = i < portals.length ? portals[i].left : end; const nextRight = i < portals.length ? portals[i].right : end; if (this.triArea2(apex, right, nextRight) <= 0) { if (this.pointsEqual(apex, right) || this.triArea2(apex, left, nextRight) > 0) { right = nextRight; rightIndex = i; if (i < portals.length) { rightOriginal = portals[i].originalRight; } } else { const turnPoint = agentRadius > 0 ? this.offsetTurningPoint(apexOriginal, leftOriginal, left, agentRadius, "left") : left; path.push(turnPoint); apex = left; apexOriginal = leftOriginal; leftIndex = rightIndex = leftIndex; left = right = apex; leftOriginal = rightOriginal = apexOriginal; i = leftIndex; continue; } } if (this.triArea2(apex, left, nextLeft) >= 0) { if (this.pointsEqual(apex, left) || this.triArea2(apex, right, nextLeft) < 0) { left = nextLeft; leftIndex = i; if (i < portals.length) { leftOriginal = portals[i].originalLeft; } } else { const turnPoint = agentRadius > 0 ? this.offsetTurningPoint(apexOriginal, rightOriginal, right, agentRadius, "right") : right; path.push(turnPoint); apex = right; apexOriginal = rightOriginal; leftIndex = rightIndex = rightIndex; left = right = apex; leftOriginal = rightOriginal = apexOriginal; i = rightIndex; continue; } } } path.push(end); return path; } /** * @zh 收缩 portal(将两端点向内移动 agentRadius) * @en Shrink portal (move endpoints inward by agentRadius) */ shrinkPortal(left, right, radius) { const dx = right.x - left.x; const dy = right.y - left.y; const len = Math.sqrt(dx * dx + dy * dy); if (len <= radius * 2) { const cx = (left.x + right.x) / 2; const cy = (left.y + right.y) / 2; return { left: createPoint(cx, cy), right: createPoint(cx, cy) }; } const nx = dx / len; const ny = dy / len; return { left: createPoint(left.x + nx * radius, left.y + ny * radius), right: createPoint(right.x - nx * radius, right.y - ny * radius) }; } /** * @zh 偏移拐点以保持与角落的距离 * @en Offset turning point to maintain distance from corner * * @param prevApex - @zh 上一个顶点 @en Previous apex * @param cornerOriginal - @zh 原始角落位置 @en Original corner position * @param cornerShrunk - @zh 收缩后的角落位置 @en Shrunk corner position * @param radius - @zh 代理半径 @en Agent radius * @param side - @zh 转向侧 ('left' 或 'right') @en Turn side ('left' or 'right') */ offsetTurningPoint(prevApex, cornerOriginal, cornerShrunk, radius, side) { const dx = cornerOriginal.x - prevApex.x; const dy = cornerOriginal.y - prevApex.y; const len = Math.sqrt(dx * dx + dy * dy); if (len < 1e-4) { return cornerShrunk; } let perpX, perpY; if (side === "left") { perpX = dy / len; perpY = -dx / len; } else { perpX = -dy / len; perpY = dx / len; } return createPoint(cornerShrunk.x + perpX * radius, cornerShrunk.y + perpY * radius); } /** * @zh 检查两点是否相等 * @en Check if two points are equal */ pointsEqual(a, b) { return Math.abs(a.x - b.x) < 1e-4 && Math.abs(a.y - b.y) < 1e-4; } /** * @zh 计算三角形面积的两倍(用于判断点的相对位置) * @en Calculate twice the triangle area (for point relative position) */ triArea2(a, b, c) { return (c.x - a.x) * (b.y - a.y) - (b.x - a.x) * (c.y - a.y); } /** * @zh 计算路径总长度 * @en Calculate total path length */ calculatePathLength(path) { let length = 0; for (let i = 1; i < path.length; i++) { length += euclideanDistance(path[i - 1], path[i]); } return length; } // ========================================================================= // 动态障碍物管理 | Dynamic Obstacle Management // ========================================================================= /** * @zh 添加圆形障碍物 * @en Add circular obstacle * * @param x - @zh 中心 X @en Center X * @param y - @zh 中心 Y @en Center Y * @param radius - @zh 半径 @en Radius * @returns @zh 障碍物 ID @en Obstacle ID */ addCircleObstacle(x, y, radius) { const id = this.nextObstacleId++; this.obstacles.set(id, { id, type: "circle", enabled: true, position: createPoint(x, y), radius }); return id; } /** * @zh 添加矩形障碍物 * @en Add rectangular obstacle * * @param x - @zh 中心 X @en Center X * @param y - @zh 中心 Y @en Center Y * @param halfWidth - @zh 半宽 @en Half width * @param halfHeight - @zh 半高 @en Half height * @returns @zh 障碍物 ID @en Obstacle ID */ addRectObstacle(x, y, halfWidth, halfHeight) { const id = this.nextObstacleId++; this.obstacles.set(id, { id, type: "rect", enabled: true, position: createPoint(x, y), halfWidth, halfHeight }); return id; } /** * @zh 添加多边形障碍物 * @en Add polygon obstacle * * @param vertices - @zh 顶点列表 @en Vertex list * @returns @zh 障碍物 ID @en Obstacle ID */ addPolygonObstacle(vertices) { const id = this.nextObstacleId++; const center = this.calculateCenter(vertices); this.obstacles.set(id, { id, type: "polygon", enabled: true, position: center, vertices }); return id; } /** * @zh 移除障碍物 * @en Remove obstacle */ removeObstacle(obstacleId) { return this.obstacles.delete(obstacleId); } /** * @zh 启用/禁用障碍物 * @en Enable/disable obstacle */ setObstacleEnabled(obstacleId, enabled) { const obstacle = this.obstacles.get(obstacleId); if (obstacle) { obstacle.enabled = enabled; } } /** * @zh 更新障碍物位置 * @en Update obstacle position */ updateObstaclePosition(obstacleId, x, y) { const obstacle = this.obstacles.get(obstacleId); if (obstacle) { obstacle.position = createPoint(x, y); } } /** * @zh 获取所有障碍物 * @en Get all obstacles */ getObstacles() { return Array.from(this.obstacles.values()); } /** * @zh 获取启用的障碍物 * @en Get enabled obstacles */ getEnabledObstacles() { return Array.from(this.obstacles.values()).filter((o) => o.enabled); } /** * @zh 清除所有障碍物 * @en Clear all obstacles */ clearObstacles() { this.obstacles.clear(); this.nextObstacleId = 0; } // ========================================================================= // 多边形禁用管理 | Polygon Disable Management // ========================================================================= /** * @zh 禁用多边形 * @en Disable polygon */ disablePolygon(polygonId) { this.disabledPolygons.add(polygonId); } /** * @zh 启用多边形 * @en Enable polygon */ enablePolygon(polygonId) { this.disabledPolygons.delete(polygonId); } /** * @zh 检查多边形是否被禁用 * @en Check if polygon is disabled */ isPolygonDisable