@esengine/pathfinding
Version:
寻路系统 | Pathfinding System - A*, Grid, NavMesh
1,477 lines (1,474 loc) • 48 kB
JavaScript
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