UNPKG

@esengine/pathfinding

Version:

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

1,477 lines (1,474 loc) 48 kB
import { CollisionResolverAdapter, DEFAULT_FLOW_CONTROLLER_CONFIG, DEFAULT_ORCA_PARAMS, FlowController, GridPathfinderAdapter, IncrementalGridPathPlannerAdapter, NavMeshPathPlannerAdapter, ORCALocalAvoidanceAdapter, PassPermission, PathPlanState, createAStarPlanner, createDefaultCollisionResolver, createFlowController, createHPAPlanner, createIncrementalAStarPlanner, createJPSPlanner, createNavMeshPathPlanner, createORCAAvoidance, isIncrementalPlanner } from "./chunk-ZYGBA7VK.js"; import "./chunk-YKA3PWU3.js"; import "./chunk-3VEX32JO.js"; import { __name, __publicField } from "./chunk-T626JPC7.js"; // src/ecs/NavigationAgentComponent.ts import { Component, ECSComponent, Serializable, Serialize, Property } from "@esengine/ecs-framework"; function _ts_decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } __name(_ts_decorate, "_ts_decorate"); function _ts_metadata(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } __name(_ts_metadata, "_ts_metadata"); var NavigationState = /* @__PURE__ */ (function(NavigationState2) { NavigationState2["Idle"] = "idle"; NavigationState2["Navigating"] = "navigating"; NavigationState2["Arrived"] = "arrived"; NavigationState2["Blocked"] = "blocked"; NavigationState2["Unreachable"] = "unreachable"; return NavigationState2; })({}); var _NavigationAgentComponent = class _NavigationAgentComponent extends Component { constructor() { super(...arguments); // ========================================================================= // 核心物理属性 | Core Physical Properties // ========================================================================= /** * @zh 代理半径 * @en Agent radius */ __publicField(this, "radius", 0.5); /** * @zh 最大速度 * @en Maximum speed */ __publicField(this, "maxSpeed", 5); /** * @zh 加速度(用于平滑移动) * @en Acceleration (for smooth movement) */ __publicField(this, "acceleration", 10); // ========================================================================= // 寻路配置 | Pathfinding Configuration // ========================================================================= /** * @zh 路径点到达阈值 * @en Waypoint arrival threshold */ __publicField(this, "waypointThreshold", 0.5); /** * @zh 目标到达阈值 * @en Destination arrival threshold */ __publicField(this, "arrivalThreshold", 0.3); /** * @zh 路径重新计算间隔(秒) * @en Path recalculation interval (seconds) */ __publicField(this, "repathInterval", 0.5); // ========================================================================= // 配置选项 | Configuration Options // ========================================================================= /** * @zh 是否启用导航 * @en Whether navigation is enabled */ __publicField(this, "enabled", true); /** * @zh 是否自动重新计算被阻挡的路径 * @en Whether to auto repath when blocked */ __publicField(this, "autoRepath", true); /** * @zh 是否启用平滑转向 * @en Whether to enable smooth steering */ __publicField(this, "smoothSteering", true); // ========================================================================= // 运行时状态 | Runtime State (Non-serialized) // ========================================================================= /** * @zh 当前位置 * @en Current position */ __publicField(this, "position", { x: 0, y: 0 }); /** * @zh 当前速度 * @en Current velocity */ __publicField(this, "velocity", { x: 0, y: 0 }); /** * @zh 目标位置 * @en Destination position */ __publicField(this, "destination", null); /** * @zh 当前导航状态 * @en Current navigation state */ __publicField(this, "state", "idle"); /** * @zh 当前路径 * @en Current path */ __publicField(this, "path", []); /** * @zh 当前路径点索引 * @en Current waypoint index */ __publicField(this, "currentWaypointIndex", 0); /** * @zh 上次重新计算路径的时间 * @en Last repath time */ __publicField(this, "lastRepathTime", 0); // ========================================================================= // 增量寻路状态(时间切片)| Incremental Pathfinding State (Time Slicing) // ========================================================================= /** * @zh 当前增量寻路请求 ID * @en Current incremental pathfinding request ID */ __publicField(this, "currentRequestId", -1); /** * @zh 寻路进度 (0-1) * @en Pathfinding progress (0-1) */ __publicField(this, "pathProgress", 0); /** * @zh 优先级(数字越小优先级越高) * @en Priority (lower number = higher priority) */ __publicField(this, "priority", 50); /** * @zh 是否正在等待路径计算完成 * @en Whether waiting for path computation to complete */ __publicField(this, "isComputingPath", false); } // ========================================================================= // 公共方法 | Public Methods // ========================================================================= /** * @zh 设置位置 * @en Set position * * @param x - @zh X 坐标 @en X coordinate * @param y - @zh Y 坐标 @en Y coordinate */ setPosition(x, y) { this.position = { x, y }; } /** * @zh 设置目标位置 * @en Set destination * * @param x - @zh 目标 X 坐标 @en Destination X coordinate * @param y - @zh 目标 Y 坐标 @en Destination Y coordinate */ setDestination(x, y) { this.destination = { x, y }; this.state = "navigating"; this.path = []; this.currentWaypointIndex = 0; this.lastRepathTime = 0; } /** * @zh 停止导航 * @en Stop navigation */ stop() { this.destination = null; this.state = "idle"; this.path = []; this.currentWaypointIndex = 0; this.velocity = { x: 0, y: 0 }; } /** * @zh 获取当前路径点 * @en Get current waypoint * * @returns @zh 当前路径点,如果没有则返回 null @en Current waypoint, or null if none */ getCurrentWaypoint() { if (this.currentWaypointIndex < this.path.length) { return this.path[this.currentWaypointIndex]; } return null; } /** * @zh 获取到目标的距离 * @en Get distance to destination * * @returns @zh 到目标的距离,如果没有目标则返回 Infinity @en Distance to destination, or Infinity if no destination */ getDistanceToDestination() { if (!this.destination) return Infinity; const dx = this.destination.x - this.position.x; const dy = this.destination.y - this.position.y; return Math.sqrt(dx * dx + dy * dy); } /** * @zh 获取当前速度大小 * @en Get current speed * * @returns @zh 当前速度大小 @en Current speed magnitude */ getCurrentSpeed() { return Math.sqrt(this.velocity.x * this.velocity.x + this.velocity.y * this.velocity.y); } /** * @zh 检查是否已到达目标 * @en Check if arrived at destination * * @returns @zh 是否已到达 @en Whether arrived */ hasArrived() { return this.state === "arrived"; } /** * @zh 检查路径是否被阻挡 * @en Check if path is blocked * * @returns @zh 是否被阻挡 @en Whether blocked */ isBlocked() { return this.state === "blocked"; } /** * @zh 检查目标是否无法到达 * @en Check if destination is unreachable * * @returns @zh 是否无法到达 @en Whether unreachable */ isUnreachable() { return this.state === "unreachable"; } /** * @zh 重置组件状态 * @en Reset component state */ reset() { this.position = { x: 0, y: 0 }; this.velocity = { x: 0, y: 0 }; this.destination = null; this.state = "idle"; this.path = []; this.currentWaypointIndex = 0; this.lastRepathTime = 0; } /** * @zh 组件从实体移除时调用 * @en Called when component is removed from entity */ onRemovedFromEntity() { this.reset(); } }; __name(_NavigationAgentComponent, "NavigationAgentComponent"); var NavigationAgentComponent = _NavigationAgentComponent; _ts_decorate([ Serialize(), Property({ type: "number", label: "Radius", min: 0.1, max: 10 }), _ts_metadata("design:type", Number) ], NavigationAgentComponent.prototype, "radius", void 0); _ts_decorate([ Serialize(), Property({ type: "number", label: "Max Speed", min: 0.1, max: 100 }), _ts_metadata("design:type", Number) ], NavigationAgentComponent.prototype, "maxSpeed", void 0); _ts_decorate([ Serialize(), Property({ type: "number", label: "Acceleration", min: 0.1, max: 100 }), _ts_metadata("design:type", Number) ], NavigationAgentComponent.prototype, "acceleration", void 0); _ts_decorate([ Serialize(), Property({ type: "number", label: "Waypoint Threshold", min: 0.1, max: 10 }), _ts_metadata("design:type", Number) ], NavigationAgentComponent.prototype, "waypointThreshold", void 0); _ts_decorate([ Serialize(), Property({ type: "number", label: "Arrival Threshold", min: 0.1, max: 10 }), _ts_metadata("design:type", Number) ], NavigationAgentComponent.prototype, "arrivalThreshold", void 0); _ts_decorate([ Serialize(), Property({ type: "number", label: "Repath Interval", min: 0.1, max: 10 }), _ts_metadata("design:type", Number) ], NavigationAgentComponent.prototype, "repathInterval", void 0); _ts_decorate([ Serialize(), Property({ type: "boolean", label: "Enabled" }), _ts_metadata("design:type", Boolean) ], NavigationAgentComponent.prototype, "enabled", void 0); _ts_decorate([ Serialize(), Property({ type: "boolean", label: "Auto Repath" }), _ts_metadata("design:type", Boolean) ], NavigationAgentComponent.prototype, "autoRepath", void 0); _ts_decorate([ Serialize(), Property({ type: "boolean", label: "Smooth Steering" }), _ts_metadata("design:type", Boolean) ], NavigationAgentComponent.prototype, "smoothSteering", void 0); NavigationAgentComponent = _ts_decorate([ ECSComponent("NavigationAgent"), Serializable({ version: 1, typeId: "NavigationAgent" }) ], NavigationAgentComponent); // src/ecs/NavigationSystem.ts import { EntitySystem, Matcher, ECSSystem } from "@esengine/ecs-framework"; function _ts_decorate2(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } __name(_ts_decorate2, "_ts_decorate"); function _ts_metadata2(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } __name(_ts_metadata2, "_ts_metadata"); var DEFAULT_CONFIG = { timeStep: 1 / 60, enablePathPlanning: true, enableFlowControl: true, enableLocalAvoidance: true, enableCollisionResolution: true, enableAgentCollisionResolution: true, enableTimeSlicing: false, iterationsBudget: 1e3, maxAgentsPerFrame: 10, maxIterationsPerAgent: 200 }; var _NavigationSystem = class _NavigationSystem extends EntitySystem { constructor(config = {}) { super(Matcher.all(NavigationAgentComponent)); __publicField(this, "config"); __publicField(this, "pathPlanner", null); __publicField(this, "flowController", null); __publicField(this, "localAvoidance", null); __publicField(this, "collisionResolver", null); /** * @zh 静态障碍物(墙壁、建筑等)- 由 PathPlanner 和 CollisionResolver 处理 * @en Static obstacles (walls, buildings) - handled by PathPlanner and CollisionResolver */ __publicField(this, "staticObstacles", []); /** * @zh 动态障碍物(移动物体等)- 由 ORCA 和 CollisionResolver 处理 * @en Dynamic obstacles (moving objects) - handled by ORCA and CollisionResolver */ __publicField(this, "dynamicObstacles", []); __publicField(this, "currentTime", 0); __publicField(this, "agentEnterTimes", /* @__PURE__ */ new Map()); // ========================================================================= // 时间切片状态 | Time Slicing State // ========================================================================= /** * @zh 是否为增量寻路器 * @en Whether the path planner is incremental */ __publicField(this, "isIncrementalPlanner", false); /** * @zh 等待寻路的代理队列(按优先级排序) * @en Queue of agents waiting for pathfinding (sorted by priority) */ __publicField(this, "pendingPathRequests", /* @__PURE__ */ new Set()); this.config = { ...DEFAULT_CONFIG, ...config }; } // ========================================================================= // 算法设置 | Algorithm Setters // ========================================================================= /** * @zh 设置路径规划器 * @en Set path planner * * @param planner - @zh 路径规划器,传入 null 禁用路径规划 @en Path planner, pass null to disable * * @zh 如果传入 IIncrementalPathPlanner 且启用了时间切片,会自动使用增量寻路 * @en If passing IIncrementalPathPlanner and time slicing is enabled, will automatically use incremental pathfinding * * @example * ```typescript * navSystem.setPathPlanner(createNavMeshPathPlanner(navMesh)); * navSystem.setPathPlanner(createAStarPlanner(gridMap)); * navSystem.setPathPlanner(createIncrementalAStarPlanner(gridMap)); // 支持时间切片 * navSystem.setPathPlanner(null); // 禁用 * ``` */ setPathPlanner(planner) { this.pathPlanner?.dispose(); this.pathPlanner = planner; this.isIncrementalPlanner = planner !== null && isIncrementalPlanner(planner); this.pendingPathRequests.clear(); } /** * @zh 获取当前路径规划器 * @en Get current path planner */ getPathPlanner() { return this.pathPlanner; } /** * @zh 设置流量控制器 * @en Set flow controller * * @param controller - @zh 流量控制器,传入 null 禁用流量控制 @en Flow controller, pass null to disable * * @example * ```typescript * navSystem.setFlowController(createFlowController()); * navSystem.setFlowController(null); // 禁用 * ``` */ setFlowController(controller) { this.flowController?.dispose(); this.flowController = controller; } /** * @zh 获取当前流量控制器 * @en Get current flow controller */ getFlowController() { return this.flowController; } /** * @zh 设置局部避让算法 * @en Set local avoidance algorithm * * @param avoidance - @zh 局部避让算法,传入 null 禁用避让 @en Local avoidance, pass null to disable * * @example * ```typescript * navSystem.setLocalAvoidance(createORCAAvoidance()); * navSystem.setLocalAvoidance(null); // 禁用 * ``` */ setLocalAvoidance(avoidance) { this.localAvoidance?.dispose(); this.localAvoidance = avoidance; } /** * @zh 获取当前局部避让算法 * @en Get current local avoidance algorithm */ getLocalAvoidance() { return this.localAvoidance; } /** * @zh 设置碰撞解决器 * @en Set collision resolver * * @param resolver - @zh 碰撞解决器,传入 null 禁用碰撞解决 @en Collision resolver, pass null to disable * * @example * ```typescript * navSystem.setCollisionResolver(createDefaultCollisionResolver()); * navSystem.setCollisionResolver(null); // 禁用 * ``` */ setCollisionResolver(resolver) { this.collisionResolver?.dispose(); this.collisionResolver = resolver; } /** * @zh 获取当前碰撞解决器 * @en Get current collision resolver */ getCollisionResolver() { return this.collisionResolver; } // ========================================================================= // 障碍物管理 | Obstacle Management // ========================================================================= /** * @zh 添加静态障碍物(墙壁、建筑等) * @en Add static obstacle (walls, buildings, etc.) * * @zh 静态障碍物由 PathPlanner 规划路径时考虑,CollisionResolver 防止穿透 * @zh ORCA 不会处理静态障碍物,因为路径规划已经绑开了它们 * @en Static obstacles are considered by PathPlanner for routing, CollisionResolver for penetration prevention * @en ORCA does NOT process static obstacles since path planning already avoids them * * @param obstacle - @zh 障碍物数据 @en Obstacle data */ addStaticObstacle(obstacle) { this.staticObstacles.push(obstacle); } /** * @zh 添加动态障碍物(移动物体、临时障碍等) * @en Add dynamic obstacle (moving objects, temporary obstacles, etc.) * * @zh 动态障碍物由 ORCA 进行局部避让,CollisionResolver 防止穿透 * @en Dynamic obstacles are handled by ORCA for local avoidance, CollisionResolver for penetration prevention * * @param obstacle - @zh 障碍物数据 @en Obstacle data */ addDynamicObstacle(obstacle) { this.dynamicObstacles.push(obstacle); } /** * @zh 移除所有静态障碍物 * @en Remove all static obstacles */ clearStaticObstacles() { this.staticObstacles = []; } /** * @zh 移除所有动态障碍物 * @en Remove all dynamic obstacles */ clearDynamicObstacles() { this.dynamicObstacles = []; } /** * @zh 移除所有障碍物(静态和动态) * @en Remove all obstacles (static and dynamic) */ clearObstacles() { this.staticObstacles = []; this.dynamicObstacles = []; } /** * @zh 获取静态障碍物列表 * @en Get static obstacles list */ getStaticObstacles() { return this.staticObstacles; } /** * @zh 获取动态障碍物列表 * @en Get dynamic obstacles list */ getDynamicObstacles() { return this.dynamicObstacles; } /** * @zh 获取所有障碍物列表(静态+动态) * @en Get all obstacles list (static + dynamic) */ getObstacles() { return [ ...this.staticObstacles, ...this.dynamicObstacles ]; } /** * @zh 获取所有障碍物用于碰撞检测 * @en Get all obstacles for collision detection */ getAllObstaclesForCollision() { return [ ...this.staticObstacles, ...this.dynamicObstacles ]; } /** * @zh 设置静态障碍物列表 * @en Set static obstacles list * * @param obstacles - @zh 障碍物列表 @en Obstacles list */ setStaticObstacles(obstacles) { this.staticObstacles = [ ...obstacles ]; } /** * @zh 设置动态障碍物列表 * @en Set dynamic obstacles list * * @param obstacles - @zh 障碍物列表 @en Obstacles list */ setDynamicObstacles(obstacles) { this.dynamicObstacles = [ ...obstacles ]; } // ========================================================================= // 系统生命周期 | System Lifecycle // ========================================================================= /** * @zh 系统销毁时调用 * @en Called when system is destroyed */ onDestroy() { this.pathPlanner?.dispose(); this.flowController?.dispose(); this.localAvoidance?.dispose(); this.collisionResolver?.dispose(); this.pathPlanner = null; this.flowController = null; this.localAvoidance = null; this.collisionResolver = null; this.staticObstacles = []; this.dynamicObstacles = []; this.agentEnterTimes.clear(); this.pendingPathRequests.clear(); this.isIncrementalPlanner = false; } // ========================================================================= // 处理管线 | Processing Pipeline // ========================================================================= /** * @zh 处理实体 * @en Process entities */ process(entities) { if (entities.length === 0) return; const deltaTime = this.config.timeStep; this.currentTime += deltaTime; const agentDataMap = /* @__PURE__ */ new Map(); const flowAgentDataList = []; const entityMap = /* @__PURE__ */ new Map(); for (const entity of entities) { entityMap.set(entity.id, entity); } if (this.config.enablePathPlanning && this.pathPlanner) { if (this.config.enableTimeSlicing && this.isIncrementalPlanner) { const agentList = []; for (const entity of entities) { const agent = entity.getComponent(NavigationAgentComponent); if (agent.enabled) { agentList.push({ entityId: entity.id, agent }); } } this.processIncrementalPathPlanning(agentList, entityMap); } else { for (const entity of entities) { const agent = entity.getComponent(NavigationAgentComponent); if (!agent.enabled) continue; this.processPathPlanning(agent, deltaTime); } } } for (const entity of entities) { const agent = entity.getComponent(NavigationAgentComponent); if (!agent.enabled) continue; if (!this.agentEnterTimes.has(entity.id)) { this.agentEnterTimes.set(entity.id, this.currentTime); } const preferredVelocity = this.calculatePreferredVelocity(agent); const agentData = this.buildAgentData(entity, agent, preferredVelocity); agentDataMap.set(entity.id, agentData); flowAgentDataList.push({ id: entity.id, position: { x: agent.position.x, y: agent.position.y }, destination: agent.destination, currentWaypoint: agent.getCurrentWaypoint(), radius: agent.radius, priority: 50, enterTime: this.agentEnterTimes.get(entity.id) }); } if (this.config.enableFlowControl && this.flowController) { this.flowController.update(flowAgentDataList, deltaTime); } if (this.config.enableLocalAvoidance && this.localAvoidance && agentDataMap.size > 0) { const proceedingAgents = []; const proceedingEntityIds = /* @__PURE__ */ new Set(); for (const entity of entities) { const agent = entity.getComponent(NavigationAgentComponent); if (!agent.enabled) continue; const flowResult = this.flowController?.getFlowControl(entity.id); const permission = flowResult?.permission ?? PassPermission.Proceed; if (permission === PassPermission.Wait) { this.handleWaitingAgent(entity, agent, flowResult.waitPosition, deltaTime); } else { const agentData = agentDataMap.get(entity.id); if (agentData) { const speedMult = flowResult?.speedMultiplier ?? 1; if (speedMult < 1) { const modifiedAgentData = { ...agentData, preferredVelocity: { x: agentData.preferredVelocity.x * speedMult, y: agentData.preferredVelocity.y * speedMult } }; proceedingAgents.push(modifiedAgentData); } else { proceedingAgents.push(agentData); } proceedingEntityIds.add(entity.id); } } } if (proceedingAgents.length > 0) { const avoidanceResults = this.localAvoidance.computeBatchAvoidance(proceedingAgents, this.dynamicObstacles, deltaTime); for (const entity of entities) { if (!proceedingEntityIds.has(entity.id)) continue; const agent = entity.getComponent(NavigationAgentComponent); if (!agent.enabled) continue; const result = avoidanceResults.get(entity.id); if (result) { this.applyAvoidanceResult(entity, agent, result.velocity, deltaTime); } else { const agentData = agentDataMap.get(entity.id); if (agentData) { this.applyAvoidanceResult(entity, agent, agentData.preferredVelocity, deltaTime); } } } } } else { for (const entity of entities) { const agent = entity.getComponent(NavigationAgentComponent); if (!agent.enabled) continue; const flowResult = this.flowController?.getFlowControl(entity.id); const permission = flowResult?.permission ?? PassPermission.Proceed; if (permission === PassPermission.Wait && this.config.enableFlowControl && this.flowController) { this.handleWaitingAgent(entity, agent, flowResult.waitPosition, deltaTime); } else { const agentData = agentDataMap.get(entity.id); if (agentData) { let velocity = agentData.preferredVelocity; const speedMult = flowResult?.speedMultiplier ?? 1; if (speedMult < 1) { velocity = { x: velocity.x * speedMult, y: velocity.y * speedMult }; } this.applyAvoidanceResult(entity, agent, velocity, deltaTime); } } } } if (this.config.enableAgentCollisionResolution && this.collisionResolver) { this.resolveAgentCollisions(entities); } this.cleanupEnterTimes(entities); } /** * @zh 处理等待中的代理 * @en Handle waiting agent */ handleWaitingAgent(entity, agent, waitPosition, deltaTime) { if (waitPosition) { const dx = waitPosition.x - agent.position.x; const dy = waitPosition.y - agent.position.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist > agent.arrivalThreshold) { const speed = Math.min(agent.maxSpeed * 0.5, dist); const velocity = { x: dx / dist * speed, y: dy / dist * speed }; this.applyAvoidanceResult(entity, agent, velocity, deltaTime); } else { agent.velocity = { x: 0, y: 0 }; } } else { agent.velocity = { x: 0, y: 0 }; } } /** * @zh 清理已移除代理的进入时间记录 * @en Cleanup enter times for removed agents */ cleanupEnterTimes(entities) { const activeIds = new Set(entities.map((e) => e.id)); for (const id of this.agentEnterTimes.keys()) { if (!activeIds.has(id)) { this.agentEnterTimes.delete(id); } } } /** * @zh 处理路径规划(同步模式) * @en Process path planning (synchronous mode) */ processPathPlanning(agent, deltaTime) { if (!agent.destination || agent.state === NavigationState.Arrived) { return; } const needsRepath = agent.path.length === 0 || agent.autoRepath && this.currentTime - agent.lastRepathTime > agent.repathInterval; if (needsRepath && this.pathPlanner) { const result = this.pathPlanner.findPath(agent.position, agent.destination, { agentRadius: agent.radius }); if (result.found) { agent.path = result.path.map((p) => ({ x: p.x, y: p.y })); agent.currentWaypointIndex = 0; agent.state = NavigationState.Navigating; } else { agent.state = NavigationState.Unreachable; agent.path = []; } agent.lastRepathTime = this.currentTime; } this.advanceWaypoint(agent); } /** * @zh 处理增量路径规划(时间切片模式) * @en Process incremental path planning (time slicing mode) * * @param agents - @zh 代理列表 @en Agent list * @param entityMap - @zh 实体 ID 到代理的映射 @en Entity ID to agent mapping */ processIncrementalPathPlanning(agents, entityMap) { if (!this.pathPlanner || !this.isIncrementalPlanner) return; const planner = this.pathPlanner; let remainingBudget = this.config.iterationsBudget; let processedCount = 0; for (const { entityId, agent } of agents) { if (!agent.destination || agent.state === NavigationState.Arrived) { if (agent.currentRequestId >= 0) { planner.cleanup(agent.currentRequestId); agent.currentRequestId = -1; agent.isComputingPath = false; } continue; } const needsRepath = !agent.isComputingPath && agent.currentRequestId < 0 && (agent.path.length === 0 || agent.autoRepath && this.currentTime - agent.lastRepathTime > agent.repathInterval); if (needsRepath) { this.pendingPathRequests.add(entityId); } } const pendingArray = Array.from(this.pendingPathRequests); pendingArray.sort((a, b) => { const agentA = agents.find((x) => x.entityId === a); const agentB = agents.find((x) => x.entityId === b); return (agentA?.agent.priority ?? 50) - (agentB?.agent.priority ?? 50); }); for (const entityId of pendingArray) { if (processedCount >= this.config.maxAgentsPerFrame) break; const entry = agents.find((x) => x.entityId === entityId); const destination = entry?.agent.destination; if (!entry || !destination) { this.pendingPathRequests.delete(entityId); continue; } const { agent } = entry; const request = planner.requestPath(agent.position, destination, { agentRadius: agent.radius }); agent.currentRequestId = request.id; agent.isComputingPath = true; agent.pathProgress = 0; this.pendingPathRequests.delete(entityId); processedCount++; } const activeAgents = agents.filter((x) => x.agent.isComputingPath && x.agent.currentRequestId >= 0); activeAgents.sort((a, b) => a.agent.priority - b.agent.priority); for (const { agent } of activeAgents) { if (remainingBudget <= 0) break; const iterations = Math.min(remainingBudget, this.config.maxIterationsPerAgent); const progress = planner.step(agent.currentRequestId, iterations); remainingBudget -= progress.nodesSearched; agent.pathProgress = progress.estimatedProgress; if (progress.state === PathPlanState.Completed) { const result = planner.getResult(agent.currentRequestId); planner.cleanup(agent.currentRequestId); if (result && result.found) { agent.path = result.path.map((p) => ({ x: p.x, y: p.y })); agent.currentWaypointIndex = 0; agent.state = NavigationState.Navigating; } else { agent.state = NavigationState.Unreachable; agent.path = []; } agent.currentRequestId = -1; agent.isComputingPath = false; agent.pathProgress = 0; agent.lastRepathTime = this.currentTime; } else if (progress.state === PathPlanState.Failed || progress.state === PathPlanState.Cancelled) { planner.cleanup(agent.currentRequestId); agent.state = NavigationState.Unreachable; agent.path = []; agent.currentRequestId = -1; agent.isComputingPath = false; agent.pathProgress = 0; agent.lastRepathTime = this.currentTime; } } for (const { agent } of agents) { this.advanceWaypoint(agent); } } /** * @zh 推进路径点 * @en Advance waypoint */ advanceWaypoint(agent) { while (agent.currentWaypointIndex < agent.path.length) { const waypoint = agent.path[agent.currentWaypointIndex]; const dx = waypoint.x - agent.position.x; const dy = waypoint.y - agent.position.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < agent.waypointThreshold) { agent.currentWaypointIndex++; } else { break; } } } /** * @zh 计算首选速度 * @en Calculate preferred velocity */ calculatePreferredVelocity(agent) { if (!agent.destination) { return { x: 0, y: 0 }; } if (agent.state === NavigationState.Unreachable || agent.state === NavigationState.Arrived) { return { x: 0, y: 0 }; } if (agent.path.length === 0 && !agent.isComputingPath) { return { x: 0, y: 0 }; } let targetX, targetY; let isLastWaypoint = false; if (agent.currentWaypointIndex < agent.path.length) { const waypoint = agent.path[agent.currentWaypointIndex]; targetX = waypoint.x; targetY = waypoint.y; isLastWaypoint = agent.currentWaypointIndex === agent.path.length - 1; } else if (agent.path.length > 0) { targetX = agent.destination.x; targetY = agent.destination.y; isLastWaypoint = true; } else { return { x: 0, y: 0 }; } const dx = targetX - agent.position.x; const dy = targetY - agent.position.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < 1e-4) { return { x: 0, y: 0 }; } const speed = isLastWaypoint ? Math.min(agent.maxSpeed, dist) : agent.maxSpeed; return { x: dx / dist * speed, y: dy / dist * speed }; } /** * @zh 构建代理数据 * @en Build agent data */ buildAgentData(entity, agent, preferredVelocity) { return { id: entity.id, position: { x: agent.position.x, y: agent.position.y }, velocity: { x: agent.velocity.x, y: agent.velocity.y }, preferredVelocity, radius: agent.radius, maxSpeed: agent.maxSpeed }; } /** * @zh 应用避让结果 * @en Apply avoidance result */ applyAvoidanceResult(entity, agent, newVelocity, deltaTime) { const allObstacles = this.getAllObstaclesForCollision(); if (this.config.enableCollisionResolution && this.collisionResolver && allObstacles.length > 0) { newVelocity = this.collisionResolver.validateVelocity(agent.position, newVelocity, agent.radius, allObstacles, deltaTime); } if (agent.smoothSteering) { newVelocity = this.applySmoothSteering(agent, newVelocity, deltaTime); } agent.velocity = { x: newVelocity.x, y: newVelocity.y }; let newPosition = { x: agent.position.x + newVelocity.x * deltaTime, y: agent.position.y + newVelocity.y * deltaTime }; if (this.config.enableCollisionResolution && this.collisionResolver && allObstacles.length > 0) { newPosition = this.collisionResolver.resolveCollision(newPosition, agent.radius, allObstacles); } if (!this.isPositionWalkable(newPosition, agent.radius)) { const slidPosition = this.findSlidePosition(agent.position, newPosition, agent.radius); if (slidPosition) { newPosition = slidPosition; agent.velocity = { x: (newPosition.x - agent.position.x) / deltaTime, y: (newPosition.y - agent.position.y) / deltaTime }; } else { agent.velocity = { x: 0, y: 0 }; return; } } agent.position = newPosition; this.checkArrival(agent); } /** * @zh 应用平滑转向 * @en Apply smooth steering */ applySmoothSteering(agent, targetVelocity, deltaTime) { const maxChange = agent.acceleration * deltaTime; const dvx = targetVelocity.x - agent.velocity.x; const dvy = targetVelocity.y - agent.velocity.y; const changeMag = Math.sqrt(dvx * dvx + dvy * dvy); if (changeMag <= maxChange) { return targetVelocity; } const factor = maxChange / changeMag; const newVel = { x: agent.velocity.x + dvx * factor, y: agent.velocity.y + dvy * factor }; const targetSpeed = Math.sqrt(targetVelocity.x * targetVelocity.x + targetVelocity.y * targetVelocity.y); const newSpeed = Math.sqrt(newVel.x * newVel.x + newVel.y * newVel.y); if (newSpeed > 1e-4 && targetSpeed > 1e-4) { const scale = targetSpeed / newSpeed; newVel.x *= scale; newVel.y *= scale; } return newVel; } /** * @zh 检查是否到达目标 * @en Check if arrived at destination */ checkArrival(agent) { if (!agent.destination) return; const dx = agent.destination.x - agent.position.x; const dy = agent.destination.y - agent.position.y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < agent.arrivalThreshold) { agent.state = NavigationState.Arrived; agent.velocity = { x: 0, y: 0 }; } } /** * @zh 解决代理间碰撞 * @en Resolve agent-agent collisions */ resolveAgentCollisions(entities) { if (!this.collisionResolver) return; const corrections = /* @__PURE__ */ new Map(); const activeEntities = entities.filter((e) => { const agent = e.getComponent(NavigationAgentComponent); return agent.enabled; }); for (let i = 0; i < activeEntities.length; i++) { for (let j = i + 1; j < activeEntities.length; j++) { const entityA = activeEntities[i]; const entityB = activeEntities[j]; const agentA = entityA.getComponent(NavigationAgentComponent); const agentB = entityB.getComponent(NavigationAgentComponent); const collision = this.collisionResolver.detectAgentCollision(agentA.position, agentA.radius, agentB.position, agentB.radius); if (collision.collided) { const halfPush = (collision.penetration + 0.01) * 0.5; const corrA = corrections.get(entityA.id) ?? { x: 0, y: 0 }; corrA.x += collision.normal.x * halfPush; corrA.y += collision.normal.y * halfPush; corrections.set(entityA.id, corrA); const corrB = corrections.get(entityB.id) ?? { x: 0, y: 0 }; corrB.x -= collision.normal.x * halfPush; corrB.y -= collision.normal.y * halfPush; corrections.set(entityB.id, corrB); } } } const allObstacles = this.getAllObstaclesForCollision(); for (const [entityId, correction] of corrections) { const entity = activeEntities.find((e) => e.id === entityId); if (!entity) continue; const agent = entity.getComponent(NavigationAgentComponent); let newPosition = { x: agent.position.x + correction.x, y: agent.position.y + correction.y }; if (!this.isPositionWalkable(newPosition, agent.radius, 1)) { const partialCorrection = this.findValidCorrection(agent.position, correction, agent.radius); if (partialCorrection) { newPosition = { x: agent.position.x + partialCorrection.x, y: agent.position.y + partialCorrection.y }; } else { continue; } } if (allObstacles.length > 0) { newPosition = this.collisionResolver.resolveCollision(newPosition, agent.radius, allObstacles); } if (!this.isPositionWalkable(newPosition, agent.radius, 1)) { continue; } agent.position = newPosition; } } /** * @zh 查找有效的修正向量(不会进入障碍物) * @en Find valid correction vector (won't enter obstacles) */ findValidCorrection(currentPos, correction, radius = 0) { if (!this.pathPlanner) return correction; const steps = [ 0.75, 0.5, 0.25, 0.1 ]; for (const scale of steps) { const testPos = { x: currentPos.x + correction.x * scale, y: currentPos.y + correction.y * scale }; if (this.isPositionWalkable(testPos, radius, 1)) { return { x: correction.x * scale, y: correction.y * scale }; } } return null; } /** * @zh 查找滑动位置(当目标位置不可行走时) * @en Find slide position (when target position is not walkable) * * @zh 尝试只沿 X 轴或 Y 轴移动,实现沿墙滑动效果 * @en Try moving only along X or Y axis, achieving wall sliding effect */ findSlidePosition(currentPos, targetPos, radius) { const dx = targetPos.x - currentPos.x; const dy = targetPos.y - currentPos.y; const xOnlyPos = { x: targetPos.x, y: currentPos.y }; if (Math.abs(dx) > 1e-3 && this.isPositionWalkable(xOnlyPos, radius)) { return xOnlyPos; } const yOnlyPos = { x: currentPos.x, y: targetPos.y }; if (Math.abs(dy) > 1e-3 && this.isPositionWalkable(yOnlyPos, radius)) { return yOnlyPos; } const halfPos = { x: currentPos.x + dx * 0.5, y: currentPos.y + dy * 0.5 }; if (this.isPositionWalkable(halfPos, radius)) { return halfPos; } return null; } /** * @zh 检查位置是否可行走(考虑代理半径) * @en Check if position is walkable (considering agent radius) * * @zh 检查代理边界框的4个角点和中心点,确保整个代理都在可行走区域 * @en Check 4 corners and center of agent bounding box, ensure entire agent is in walkable area * * @param position - @zh 位置 @en Position * @param radius - @zh 代理半径 @en Agent radius * @param tolerance - @zh 容差比例 (0-1),用于允许轻微重叠 @en Tolerance ratio (0-1), allows slight overlap */ isPositionWalkable(position, radius = 0, tolerance = 0.8) { if (!this.pathPlanner) return true; if (!this.pathPlanner.isWalkable(position)) { return false; } if (radius <= 0) { return true; } const checkRadius = radius * tolerance; const corners = [ { x: position.x - checkRadius, y: position.y - checkRadius }, { x: position.x + checkRadius, y: position.y - checkRadius }, { x: position.x - checkRadius, y: position.y + checkRadius }, { x: position.x + checkRadius, y: position.y + checkRadius } ]; for (const corner of corners) { if (!this.pathPlanner.isWalkable(corner)) { return false; } } return true; } }; __name(_NavigationSystem, "NavigationSystem"); var NavigationSystem = _NavigationSystem; NavigationSystem = _ts_decorate2([ ECSSystem("Navigation", { updateOrder: 45 }), _ts_metadata2("design:type", Function), _ts_metadata2("design:paramtypes", [ typeof INavigationSystemConfig === "undefined" ? Object : INavigationSystemConfig ]) ], NavigationSystem); // src/ecs/ORCAConfigComponent.ts import { Component as Component2, ECSComponent as ECSComponent2, Serializable as Serializable2, Serialize as Serialize2, Property as Property2 } from "@esengine/ecs-framework"; function _ts_decorate3(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } __name(_ts_decorate3, "_ts_decorate"); function _ts_metadata3(k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); } __name(_ts_metadata3, "_ts_metadata"); var _ORCAConfigComponent = class _ORCAConfigComponent extends Component2 { constructor() { super(...arguments); /** * @zh 邻居检测距离 * @en Neighbor detection distance * * @zh 代理检测邻居的最大距离,更大的值意味着更早开始避让但也更消耗性能 * @en Maximum distance for detecting neighbors, larger value means earlier avoidance but more performance cost */ __publicField(this, "neighborDist", 15); /** * @zh 最大邻居数量 * @en Maximum number of neighbors * * @zh 计算避让时考虑的最大邻居数量,更多邻居意味着更精确但也更消耗性能 * @en Maximum neighbors considered for avoidance, more neighbors means more accurate but slower */ __publicField(this, "maxNeighbors", 10); /** * @zh 代理避让时间视野 * @en Time horizon for agent avoidance * * @zh 预测其他代理未来位置的时间范围,更长意味着更平滑但可能过度避让 * @en Time range for predicting other agents' future positions, longer means smoother but may over-avoid */ __publicField(this, "timeHorizon", 2); /** * @zh 障碍物避让时间视野 * @en Time horizon for obstacle avoidance * * @zh 预测与障碍物碰撞的时间范围,通常比代理视野短 * @en Time range for predicting obstacle collisions, usually shorter than agent horizon */ __publicField(this, "timeHorizonObst", 1); } }; __name(_ORCAConfigComponent, "ORCAConfigComponent"); var ORCAConfigComponent = _ORCAConfigComponent; _ts_decorate3([ Serialize2(), Property2({ type: "number", label: "Neighbor Dist", min: 1, max: 100 }), _ts_metadata3("design:type", Number) ], ORCAConfigComponent.prototype, "neighborDist", void 0); _ts_decorate3([ Serialize2(), Property2({ type: "number", label: "Max Neighbors", min: 1, max: 50 }), _ts_metadata3("design:type", Number) ], ORCAConfigComponent.prototype, "maxNeighbors", void 0); _ts_decorate3([ Serialize2(), Property2({ type: "number", label: "Time Horizon", min: 0.1, max: 10 }), _ts_metadata3("design:type", Number) ], ORCAConfigComponent.prototype, "timeHorizon", void 0); _ts_decorate3([ Serialize2(), Property2({ type: "number", label: "Time Horizon Obst", min: 0.1, max: 10 }), _ts_metadata3("design:type", Number) ], ORCAConfigComponent.prototype, "timeHorizonObst", void 0); ORCAConfigComponent = _ts_decorate3([ ECSComponent2("ORCAConfig"), Serializable2({ version: 1, typeId: "ORCAConfig" }) ], ORCAConfigComponent); export { CollisionResolverAdapter, DEFAULT_FLOW_CONTROLLER_CONFIG, DEFAULT_ORCA_PARAMS, FlowController, GridPathfinderAdapter, IncrementalGridPathPlannerAdapter, NavMeshPathPlannerAdapter, NavigationAgentComponent, NavigationState, NavigationSystem, ORCAConfigComponent, ORCALocalAvoidanceAdapter, PassPermission, createAStarPlanner, createDefaultCollisionResolver, createFlowController, createHPAPlanner, createIncrementalAStarPlanner, createJPSPlanner, createNavMeshPathPlanner, createORCAAvoidance }; //# sourceMappingURL=ecs.js.map