diff --git a/packages/spatial/src/aoi/AOINodes.ts b/packages/spatial/src/aoi/AOINodes.ts new file mode 100644 index 00000000..a29cd292 --- /dev/null +++ b/packages/spatial/src/aoi/AOINodes.ts @@ -0,0 +1,328 @@ +/** + * @zh AOI 蓝图节点 + * @en AOI Blueprint Nodes + * + * @zh 提供 AOI 功能的蓝图节点 + * @en Provides blueprint nodes for AOI functionality + */ + +import type { BlueprintNodeTemplate, BlueprintNode, INodeExecutor, ExecutionResult } from '@esengine/blueprint'; +import type { IAOIManager } from './IAOI'; + +// ============================================================================= +// 执行上下文接口 | Execution Context Interface +// ============================================================================= + +/** + * @zh AOI 上下文 + * @en AOI context + */ +interface AOIContext { + aoiManager: IAOIManager; + entity: unknown; + evaluateInput(nodeId: string, pinName: string, defaultValue?: unknown): unknown; + setOutputs(nodeId: string, outputs: Record): void; +} + +// ============================================================================= +// GetEntitiesInView 节点 | GetEntitiesInView Node +// ============================================================================= + +/** + * @zh GetEntitiesInView 节点模板 + * @en GetEntitiesInView node template + */ +export const GetEntitiesInViewTemplate: BlueprintNodeTemplate = { + type: 'GetEntitiesInView', + title: 'Get Entities In View', + category: 'entity', + description: 'Get all entities within view range / 获取视野范围内的所有实体', + keywords: ['aoi', 'view', 'entities', 'visible'], + menuPath: ['AOI', 'Get Entities In View'], + isPure: true, + inputs: [ + { + name: 'observer', + displayName: 'Observer', + type: 'object' + } + ], + outputs: [ + { + name: 'entities', + displayName: 'Entities', + type: 'array' + }, + { + name: 'count', + displayName: 'Count', + type: 'int' + } + ], + color: '#9c27b0' +}; + +/** + * @zh GetEntitiesInView 节点执行器 + * @en GetEntitiesInView node executor + */ +export class GetEntitiesInViewExecutor implements INodeExecutor { + execute(node: BlueprintNode, context: unknown): ExecutionResult { + const ctx = context as AOIContext; + const observer = ctx.evaluateInput(node.id, 'observer', ctx.entity); + + const entities = ctx.aoiManager?.getEntitiesInView(observer) ?? []; + + return { + outputs: { + entities, + count: entities.length + } + }; + } +} + +// ============================================================================= +// GetObserversOf 节点 | GetObserversOf Node +// ============================================================================= + +/** + * @zh GetObserversOf 节点模板 + * @en GetObserversOf node template + */ +export const GetObserversOfTemplate: BlueprintNodeTemplate = { + type: 'GetObserversOf', + title: 'Get Observers Of', + category: 'entity', + description: 'Get all observers who can see the entity / 获取能看到该实体的所有观察者', + keywords: ['aoi', 'observers', 'watchers', 'visible'], + menuPath: ['AOI', 'Get Observers Of'], + isPure: true, + inputs: [ + { + name: 'target', + displayName: 'Target', + type: 'object' + } + ], + outputs: [ + { + name: 'observers', + displayName: 'Observers', + type: 'array' + }, + { + name: 'count', + displayName: 'Count', + type: 'int' + } + ], + color: '#9c27b0' +}; + +/** + * @zh GetObserversOf 节点执行器 + * @en GetObserversOf node executor + */ +export class GetObserversOfExecutor implements INodeExecutor { + execute(node: BlueprintNode, context: unknown): ExecutionResult { + const ctx = context as AOIContext; + const target = ctx.evaluateInput(node.id, 'target', ctx.entity); + + const observers = ctx.aoiManager?.getObserversOf(target) ?? []; + + return { + outputs: { + observers, + count: observers.length + } + }; + } +} + +// ============================================================================= +// CanSee 节点 | CanSee Node +// ============================================================================= + +/** + * @zh CanSee 节点模板 + * @en CanSee node template + */ +export const CanSeeTemplate: BlueprintNodeTemplate = { + type: 'CanSee', + title: 'Can See', + category: 'entity', + description: 'Check if observer can see target / 检查观察者是否能看到目标', + keywords: ['aoi', 'visibility', 'can', 'see', 'check'], + menuPath: ['AOI', 'Can See'], + isPure: true, + inputs: [ + { + name: 'observer', + displayName: 'Observer', + type: 'object' + }, + { + name: 'target', + displayName: 'Target', + type: 'object' + } + ], + outputs: [ + { + name: 'canSee', + displayName: 'Can See', + type: 'bool' + } + ], + color: '#9c27b0' +}; + +/** + * @zh CanSee 节点执行器 + * @en CanSee node executor + */ +export class CanSeeExecutor implements INodeExecutor { + execute(node: BlueprintNode, context: unknown): ExecutionResult { + const ctx = context as AOIContext; + const observer = ctx.evaluateInput(node.id, 'observer', ctx.entity); + const target = ctx.evaluateInput(node.id, 'target', null); + + const canSee = ctx.aoiManager?.canSee(observer, target) ?? false; + + return { + outputs: { + canSee + } + }; + } +} + +// ============================================================================= +// OnEntityEnterView 事件节点 | OnEntityEnterView Event Node +// ============================================================================= + +/** + * @zh OnEntityEnterView 事件节点模板 + * @en OnEntityEnterView event node template + */ +export const OnEntityEnterViewTemplate: BlueprintNodeTemplate = { + type: 'EventEntityEnterView', + title: 'On Entity Enter View', + category: 'event', + description: 'Triggered when an entity enters view / 当实体进入视野时触发', + keywords: ['aoi', 'event', 'enter', 'view', 'visible'], + menuPath: ['AOI', 'Events', 'On Entity Enter View'], + color: '#e91e63', + inputs: [], + outputs: [ + { + name: 'exec', + displayName: '', + type: 'exec' + }, + { + name: 'entity', + displayName: 'Entity', + type: 'object' + }, + { + name: 'positionX', + displayName: 'Position X', + type: 'float' + }, + { + name: 'positionY', + displayName: 'Position Y', + type: 'float' + } + ] +}; + +/** + * @zh OnEntityEnterView 事件执行器 + * @en OnEntityEnterView event executor + */ +export class OnEntityEnterViewExecutor implements INodeExecutor { + execute(_node: BlueprintNode, _context: unknown): ExecutionResult { + // Event nodes don't execute directly, they are triggered by the runtime + return { nextExec: 'exec' }; + } +} + +// ============================================================================= +// OnEntityExitView 事件节点 | OnEntityExitView Event Node +// ============================================================================= + +/** + * @zh OnEntityExitView 事件节点模板 + * @en OnEntityExitView event node template + */ +export const OnEntityExitViewTemplate: BlueprintNodeTemplate = { + type: 'EventEntityExitView', + title: 'On Entity Exit View', + category: 'event', + description: 'Triggered when an entity exits view / 当实体离开视野时触发', + keywords: ['aoi', 'event', 'exit', 'view', 'invisible'], + menuPath: ['AOI', 'Events', 'On Entity Exit View'], + color: '#e91e63', + inputs: [], + outputs: [ + { + name: 'exec', + displayName: '', + type: 'exec' + }, + { + name: 'entity', + displayName: 'Entity', + type: 'object' + }, + { + name: 'positionX', + displayName: 'Position X', + type: 'float' + }, + { + name: 'positionY', + displayName: 'Position Y', + type: 'float' + } + ] +}; + +/** + * @zh OnEntityExitView 事件执行器 + * @en OnEntityExitView event executor + */ +export class OnEntityExitViewExecutor implements INodeExecutor { + execute(_node: BlueprintNode, _context: unknown): ExecutionResult { + // Event nodes don't execute directly, they are triggered by the runtime + return { nextExec: 'exec' }; + } +} + +// ============================================================================= +// 节点定义集合 | Node Definition Collection +// ============================================================================= + +/** + * @zh AOI 节点定义集合 + * @en AOI node definition collection + */ +export const AOINodeDefinitions = { + templates: [ + GetEntitiesInViewTemplate, + GetObserversOfTemplate, + CanSeeTemplate, + OnEntityEnterViewTemplate, + OnEntityExitViewTemplate + ], + executors: new Map([ + ['GetEntitiesInView', new GetEntitiesInViewExecutor()], + ['GetObserversOf', new GetObserversOfExecutor()], + ['CanSee', new CanSeeExecutor()], + ['EventEntityEnterView', new OnEntityEnterViewExecutor()], + ['EventEntityExitView', new OnEntityExitViewExecutor()] + ]) +}; diff --git a/packages/spatial/src/aoi/GridAOI.ts b/packages/spatial/src/aoi/GridAOI.ts new file mode 100644 index 00000000..66402efc --- /dev/null +++ b/packages/spatial/src/aoi/GridAOI.ts @@ -0,0 +1,490 @@ +/** + * @zh 网格 AOI 实现 + * @en Grid AOI Implementation + * + * @zh 基于均匀网格的兴趣区域管理实现 + * @en Uniform grid based area of interest management implementation + */ + +import type { IVector2 } from '@esengine/ecs-framework-math'; +import type { + IAOIManager, + IAOIObserverConfig, + IAOIEvent, + AOIEventListener +} from './IAOI'; +import { distanceSquared } from '../ISpatialQuery'; + +// ============================================================================= +// 内部类型 | Internal Types +// ============================================================================= + +/** + * @zh AOI 观察者数据 + * @en AOI observer data + */ +interface AOIObserverData { + entity: T; + position: IVector2; + viewRange: number; + viewRangeSq: number; + observable: boolean; + cellKey: string; + /** @zh 当前可见的实体集合 @en Currently visible entities */ + visibleEntities: Set; + /** @zh 实体特定的监听器 @en Entity-specific listeners */ + listeners: Set>; +} + +// ============================================================================= +// 网格 AOI 配置 | Grid AOI Configuration +// ============================================================================= + +/** + * @zh 网格 AOI 配置 + * @en Grid AOI configuration + */ +export interface GridAOIConfig { + /** + * @zh 网格单元格大小(建议设置为平均视野范围的 1-2 倍) + * @en Grid cell size (recommended 1-2x average view range) + */ + cellSize: number; +} + +// ============================================================================= +// 网格 AOI 实现 | Grid AOI Implementation +// ============================================================================= + +/** + * @zh 网格 AOI 实现 + * @en Grid AOI implementation + * + * @zh 使用均匀网格进行空间划分,高效管理大量实体的兴趣区域 + * @en Uses uniform grid for spatial partitioning, efficiently managing AOI for many entities + */ +export class GridAOI implements IAOIManager { + private readonly _cellSize: number; + private readonly _cells: Map>> = new Map(); + private readonly _observers: Map> = new Map(); + private readonly _globalListeners: Set> = new Set(); + + constructor(config: GridAOIConfig) { + this._cellSize = config.cellSize; + } + + // ========================================================================= + // IAOIManager 实现 | IAOIManager Implementation + // ========================================================================= + + get count(): number { + return this._observers.size; + } + + /** + * @zh 添加观察者 + * @en Add observer + */ + addObserver(entity: T, position: IVector2, config: IAOIObserverConfig): void { + if (this._observers.has(entity)) { + this.updatePosition(entity, position); + this.updateViewRange(entity, config.viewRange); + return; + } + + const cellKey = this._getCellKey(position); + const data: AOIObserverData = { + entity, + position: { x: position.x, y: position.y }, + viewRange: config.viewRange, + viewRangeSq: config.viewRange * config.viewRange, + observable: config.observable !== false, + cellKey, + visibleEntities: new Set(), + listeners: new Set() + }; + + this._observers.set(entity, data); + this._addToCell(cellKey, data); + + // Initial visibility check + this._updateVisibility(data); + } + + /** + * @zh 移除观察者 + * @en Remove observer + */ + removeObserver(entity: T): boolean { + const data = this._observers.get(entity); + if (!data) { + return false; + } + + // Notify all observers who were watching this entity + if (data.observable) { + for (const [, otherData] of this._observers) { + if (otherData !== data && otherData.visibleEntities.has(entity)) { + otherData.visibleEntities.delete(entity); + this._emitEvent({ + type: 'exit', + observer: otherData.entity, + target: entity, + position: data.position + }, otherData); + } + } + } + + // Notify this entity about all entities it was watching + for (const visible of data.visibleEntities) { + const visibleData = this._observers.get(visible); + if (visibleData) { + this._emitEvent({ + type: 'exit', + observer: entity, + target: visible, + position: visibleData.position + }, data); + } + } + + this._removeFromCell(data.cellKey, data); + this._observers.delete(entity); + return true; + } + + /** + * @zh 更新观察者位置 + * @en Update observer position + */ + updatePosition(entity: T, newPosition: IVector2): boolean { + const data = this._observers.get(entity); + if (!data) { + return false; + } + + const newCellKey = this._getCellKey(newPosition); + + // Update cell if changed + if (newCellKey !== data.cellKey) { + this._removeFromCell(data.cellKey, data); + data.cellKey = newCellKey; + this._addToCell(newCellKey, data); + } + + data.position = { x: newPosition.x, y: newPosition.y }; + + // Update visibility for this observer + this._updateVisibility(data); + + // Update visibility for others who might now see/unsee this entity + if (data.observable) { + this._updateObserversOfEntity(data); + } + + return true; + } + + /** + * @zh 更新观察者视野范围 + * @en Update observer view range + */ + updateViewRange(entity: T, newRange: number): boolean { + const data = this._observers.get(entity); + if (!data) { + return false; + } + + data.viewRange = newRange; + data.viewRangeSq = newRange * newRange; + + // Recalculate visibility + this._updateVisibility(data); + + return true; + } + + /** + * @zh 获取实体视野内的所有对象 + * @en Get all objects within entity's view + */ + getEntitiesInView(entity: T): T[] { + const data = this._observers.get(entity); + if (!data) { + return []; + } + return Array.from(data.visibleEntities); + } + + /** + * @zh 获取能看到指定实体的所有观察者 + * @en Get all observers who can see the specified entity + */ + getObserversOf(entity: T): T[] { + const data = this._observers.get(entity); + if (!data || !data.observable) { + return []; + } + + const observers: T[] = []; + for (const [, otherData] of this._observers) { + if (otherData !== data && otherData.visibleEntities.has(entity)) { + observers.push(otherData.entity); + } + } + return observers; + } + + /** + * @zh 检查观察者是否能看到目标 + * @en Check if observer can see target + */ + canSee(observer: T, target: T): boolean { + const data = this._observers.get(observer); + if (!data) { + return false; + } + return data.visibleEntities.has(target); + } + + /** + * @zh 添加全局事件监听器 + * @en Add global event listener + */ + addListener(listener: AOIEventListener): void { + this._globalListeners.add(listener); + } + + /** + * @zh 移除全局事件监听器 + * @en Remove global event listener + */ + removeListener(listener: AOIEventListener): void { + this._globalListeners.delete(listener); + } + + /** + * @zh 为特定观察者添加事件监听器 + * @en Add event listener for specific observer + */ + addEntityListener(entity: T, listener: AOIEventListener): void { + const data = this._observers.get(entity); + if (data) { + data.listeners.add(listener); + } + } + + /** + * @zh 移除特定观察者的事件监听器 + * @en Remove event listener for specific observer + */ + removeEntityListener(entity: T, listener: AOIEventListener): void { + const data = this._observers.get(entity); + if (data) { + data.listeners.delete(listener); + } + } + + /** + * @zh 清空所有观察者 + * @en Clear all observers + */ + clear(): void { + this._cells.clear(); + this._observers.clear(); + } + + // ========================================================================= + // 私有方法 | Private Methods + // ========================================================================= + + private _getCellKey(position: IVector2): string { + const cellX = Math.floor(position.x / this._cellSize); + const cellY = Math.floor(position.y / this._cellSize); + return `${cellX},${cellY}`; + } + + private _getCellCoords(position: IVector2): { x: number; y: number } { + return { + x: Math.floor(position.x / this._cellSize), + y: Math.floor(position.y / this._cellSize) + }; + } + + private _addToCell(cellKey: string, data: AOIObserverData): void { + let cell = this._cells.get(cellKey); + if (!cell) { + cell = new Set(); + this._cells.set(cellKey, cell); + } + cell.add(data); + } + + private _removeFromCell(cellKey: string, data: AOIObserverData): void { + const cell = this._cells.get(cellKey); + if (cell) { + cell.delete(data); + if (cell.size === 0) { + this._cells.delete(cellKey); + } + } + } + + /** + * @zh 更新观察者的可见实体列表 + * @en Update observer's visible entities list + */ + private _updateVisibility(data: AOIObserverData): void { + const newVisible = new Set(); + + // Calculate search radius in cells + const cellRadius = Math.ceil(data.viewRange / this._cellSize); + const centerCell = this._getCellCoords(data.position); + + // Check all cells within range + for (let dx = -cellRadius; dx <= cellRadius; dx++) { + for (let dy = -cellRadius; dy <= cellRadius; dy++) { + const cellKey = `${centerCell.x + dx},${centerCell.y + dy}`; + const cell = this._cells.get(cellKey); + if (!cell) continue; + + for (const otherData of cell) { + if (otherData === data) continue; + if (!otherData.observable) continue; + + const distSq = distanceSquared(data.position, otherData.position); + if (distSq <= data.viewRangeSq) { + newVisible.add(otherData.entity); + } + } + } + } + + // Find entities that entered view + for (const entity of newVisible) { + if (!data.visibleEntities.has(entity)) { + const targetData = this._observers.get(entity); + if (targetData) { + this._emitEvent({ + type: 'enter', + observer: data.entity, + target: entity, + position: targetData.position + }, data); + } + } + } + + // Find entities that exited view + for (const entity of data.visibleEntities) { + if (!newVisible.has(entity)) { + const targetData = this._observers.get(entity); + const position = targetData?.position ?? { x: 0, y: 0 }; + this._emitEvent({ + type: 'exit', + observer: data.entity, + target: entity, + position + }, data); + } + } + + data.visibleEntities = newVisible; + } + + /** + * @zh 更新其他观察者对于某个实体的可见性 + * @en Update other observers' visibility of an entity + */ + private _updateObserversOfEntity(movedData: AOIObserverData): void { + const cellRadius = Math.ceil(this._getMaxViewRange() / this._cellSize) + 1; + const centerCell = this._getCellCoords(movedData.position); + + for (let dx = -cellRadius; dx <= cellRadius; dx++) { + for (let dy = -cellRadius; dy <= cellRadius; dy++) { + const cellKey = `${centerCell.x + dx},${centerCell.y + dy}`; + const cell = this._cells.get(cellKey); + if (!cell) continue; + + for (const otherData of cell) { + if (otherData === movedData) continue; + + const distSq = distanceSquared(otherData.position, movedData.position); + const wasVisible = otherData.visibleEntities.has(movedData.entity); + const isVisible = distSq <= otherData.viewRangeSq; + + if (isVisible && !wasVisible) { + otherData.visibleEntities.add(movedData.entity); + this._emitEvent({ + type: 'enter', + observer: otherData.entity, + target: movedData.entity, + position: movedData.position + }, otherData); + } else if (!isVisible && wasVisible) { + otherData.visibleEntities.delete(movedData.entity); + this._emitEvent({ + type: 'exit', + observer: otherData.entity, + target: movedData.entity, + position: movedData.position + }, otherData); + } + } + } + } + } + + /** + * @zh 获取最大视野范围(用于优化搜索) + * @en Get maximum view range (for search optimization) + */ + private _getMaxViewRange(): number { + let max = 0; + for (const [, data] of this._observers) { + if (data.viewRange > max) { + max = data.viewRange; + } + } + return max; + } + + /** + * @zh 发送事件 + * @en Emit event + */ + private _emitEvent(event: IAOIEvent, observerData: AOIObserverData): void { + // Entity-specific listeners + for (const listener of observerData.listeners) { + try { + listener(event); + } catch (e) { + console.error('AOI entity listener error:', e); + } + } + + // Global listeners + for (const listener of this._globalListeners) { + try { + listener(event); + } catch (e) { + console.error('AOI global listener error:', e); + } + } + } +} + +// ============================================================================= +// 工厂函数 | Factory Functions +// ============================================================================= + +/** + * @zh 创建网格 AOI 管理器 + * @en Create grid AOI manager + * + * @param cellSize - @zh 网格单元格大小 @en Grid cell size + */ +export function createGridAOI(cellSize: number = 100): GridAOI { + return new GridAOI({ cellSize }); +} diff --git a/packages/spatial/src/aoi/IAOI.ts b/packages/spatial/src/aoi/IAOI.ts new file mode 100644 index 00000000..b23d1b0d --- /dev/null +++ b/packages/spatial/src/aoi/IAOI.ts @@ -0,0 +1,205 @@ +/** + * @zh AOI (Area of Interest) 兴趣区域接口 + * @en AOI (Area of Interest) Interface + * + * @zh 提供 MMO 游戏中的兴趣区域管理能力 + * @en Provides area of interest management for MMO games + */ + +import type { IVector2 } from '@esengine/ecs-framework-math'; + +// ============================================================================= +// AOI 事件类型 | AOI Event Types +// ============================================================================= + +/** + * @zh AOI 事件类型 + * @en AOI event type + */ +export type AOIEventType = 'enter' | 'exit' | 'update'; + +/** + * @zh AOI 事件 + * @en AOI event + */ +export interface IAOIEvent { + /** + * @zh 事件类型 + * @en Event type + */ + readonly type: AOIEventType; + + /** + * @zh 观察者(谁看到了变化) + * @en Observer (who saw the change) + */ + readonly observer: T; + + /** + * @zh 目标(发生变化的对象) + * @en Target (the object that changed) + */ + readonly target: T; + + /** + * @zh 目标位置 + * @en Target position + */ + readonly position: IVector2; +} + +/** + * @zh AOI 事件监听器 + * @en AOI event listener + */ +export type AOIEventListener = (event: IAOIEvent) => void; + +// ============================================================================= +// AOI 观察者接口 | AOI Observer Interface +// ============================================================================= + +/** + * @zh AOI 观察者配置 + * @en AOI observer configuration + */ +export interface IAOIObserverConfig { + /** + * @zh 视野范围 + * @en View range + */ + viewRange: number; + + /** + * @zh 是否是可被观察的对象(默认 true) + * @en Whether this observer can be observed by others (default true) + */ + observable?: boolean; +} + +// ============================================================================= +// AOI 管理器接口 | AOI Manager Interface +// ============================================================================= + +/** + * @zh AOI 管理器接口 + * @en AOI manager interface + * + * @zh 管理兴趣区域,追踪对象的进入/离开事件 + * @en Manages areas of interest, tracking enter/exit events for objects + * + * @typeParam T - @zh 被管理对象的类型 @en Type of managed objects + */ +export interface IAOIManager { + /** + * @zh 添加观察者 + * @en Add observer + * + * @param entity - @zh 实体对象 @en Entity object + * @param position - @zh 初始位置 @en Initial position + * @param config - @zh 观察者配置 @en Observer configuration + */ + addObserver(entity: T, position: IVector2, config: IAOIObserverConfig): void; + + /** + * @zh 移除观察者 + * @en Remove observer + * + * @param entity - @zh 实体对象 @en Entity object + * @returns @zh 是否成功移除 @en Whether removal was successful + */ + removeObserver(entity: T): boolean; + + /** + * @zh 更新观察者位置 + * @en Update observer position + * + * @param entity - @zh 实体对象 @en Entity object + * @param newPosition - @zh 新位置 @en New position + * @returns @zh 是否成功更新 @en Whether update was successful + */ + updatePosition(entity: T, newPosition: IVector2): boolean; + + /** + * @zh 更新观察者视野范围 + * @en Update observer view range + * + * @param entity - @zh 实体对象 @en Entity object + * @param newRange - @zh 新视野范围 @en New view range + * @returns @zh 是否成功更新 @en Whether update was successful + */ + updateViewRange(entity: T, newRange: number): boolean; + + /** + * @zh 获取实体视野内的所有对象 + * @en Get all objects within entity's view + * + * @param entity - @zh 观察者实体 @en Observer entity + * @returns @zh 视野内的对象数组 @en Array of objects within view + */ + getEntitiesInView(entity: T): T[]; + + /** + * @zh 获取能看到指定实体的所有观察者 + * @en Get all observers who can see the specified entity + * + * @param entity - @zh 目标实体 @en Target entity + * @returns @zh 能看到目标的观察者数组 @en Array of observers who can see target + */ + getObserversOf(entity: T): T[]; + + /** + * @zh 检查观察者是否能看到目标 + * @en Check if observer can see target + * + * @param observer - @zh 观察者 @en Observer + * @param target - @zh 目标 @en Target + * @returns @zh 是否在视野内 @en Whether target is in view + */ + canSee(observer: T, target: T): boolean; + + /** + * @zh 添加事件监听器 + * @en Add event listener + * + * @param listener - @zh 监听器函数 @en Listener function + */ + addListener(listener: AOIEventListener): void; + + /** + * @zh 移除事件监听器 + * @en Remove event listener + * + * @param listener - @zh 监听器函数 @en Listener function + */ + removeListener(listener: AOIEventListener): void; + + /** + * @zh 为特定观察者添加事件监听器 + * @en Add event listener for specific observer + * + * @param entity - @zh 观察者实体 @en Observer entity + * @param listener - @zh 监听器函数 @en Listener function + */ + addEntityListener(entity: T, listener: AOIEventListener): void; + + /** + * @zh 移除特定观察者的事件监听器 + * @en Remove event listener for specific observer + * + * @param entity - @zh 观察者实体 @en Observer entity + * @param listener - @zh 监听器函数 @en Listener function + */ + removeEntityListener(entity: T, listener: AOIEventListener): void; + + /** + * @zh 清空所有观察者 + * @en Clear all observers + */ + clear(): void; + + /** + * @zh 获取观察者数量 + * @en Get observer count + */ + readonly count: number; +} diff --git a/packages/spatial/src/aoi/index.ts b/packages/spatial/src/aoi/index.ts new file mode 100644 index 00000000..7690d365 --- /dev/null +++ b/packages/spatial/src/aoi/index.ts @@ -0,0 +1,15 @@ +/** + * @zh AOI (Area of Interest) 兴趣区域模块 + * @en AOI (Area of Interest) Module + */ + +export type { + AOIEventType, + IAOIEvent, + AOIEventListener, + IAOIObserverConfig, + IAOIManager +} from './IAOI'; + +export type { GridAOIConfig } from './GridAOI'; +export { GridAOI, createGridAOI } from './GridAOI'; diff --git a/packages/spatial/src/index.ts b/packages/spatial/src/index.ts index f64a7fd6..f3837f01 100644 --- a/packages/spatial/src/index.ts +++ b/packages/spatial/src/index.ts @@ -2,8 +2,8 @@ * @zh @esengine/spatial - 空间查询和索引系统 * @en @esengine/spatial - Spatial Query and Indexing System * - * @zh 提供空间查询能力,支持范围查询、最近邻查询和射线检测 - * @en Provides spatial query capabilities including range queries, nearest neighbor queries, and raycasting + * @zh 提供空间查询能力,支持范围查询、最近邻查询、射线检测和 AOI 兴趣区域管理 + * @en Provides spatial query capabilities including range queries, nearest neighbor queries, raycasting, and AOI management */ // ============================================================================= @@ -38,25 +38,40 @@ export { export type { GridSpatialIndexConfig } from './GridSpatialIndex'; export { GridSpatialIndex, createGridSpatialIndex } from './GridSpatialIndex'; +// ============================================================================= +// AOI 兴趣区域 | AOI (Area of Interest) +// ============================================================================= + +export type { + AOIEventType, + IAOIEvent, + AOIEventListener, + IAOIObserverConfig, + IAOIManager +} from './aoi'; + +export type { GridAOIConfig } from './aoi'; +export { GridAOI, createGridAOI } from './aoi'; + // ============================================================================= // 服务令牌 | Service Tokens // ============================================================================= -export { SpatialIndexToken, SpatialQueryToken } from './tokens'; +export { SpatialIndexToken, SpatialQueryToken, AOIManagerToken } from './tokens'; // ============================================================================= // 蓝图节点 | Blueprint Nodes // ============================================================================= export { - // Templates + // Spatial Query Templates FindInRadiusTemplate, FindInRectTemplate, FindNearestTemplate, FindKNearestTemplate, RaycastTemplate, RaycastFirstTemplate, - // Executors + // Spatial Query Executors FindInRadiusExecutor, FindInRectExecutor, FindNearestExecutor, @@ -66,3 +81,20 @@ export { // Collection SpatialQueryNodeDefinitions } from './nodes'; + +export { + // AOI Templates + GetEntitiesInViewTemplate, + GetObserversOfTemplate, + CanSeeTemplate, + OnEntityEnterViewTemplate, + OnEntityExitViewTemplate, + // AOI Executors + GetEntitiesInViewExecutor, + GetObserversOfExecutor, + CanSeeExecutor, + OnEntityEnterViewExecutor, + OnEntityExitViewExecutor, + // Collection + AOINodeDefinitions +} from './aoi/AOINodes'; diff --git a/packages/spatial/src/tokens.ts b/packages/spatial/src/tokens.ts index b6aae601..ac8ceda2 100644 --- a/packages/spatial/src/tokens.ts +++ b/packages/spatial/src/tokens.ts @@ -5,6 +5,7 @@ import { createServiceToken } from '@esengine/ecs-framework'; import type { ISpatialIndex, ISpatialQuery } from './ISpatialQuery'; +import type { IAOIManager } from './aoi/IAOI'; /** * @zh 空间索引服务令牌 @@ -23,3 +24,12 @@ export const SpatialIndexToken = createServiceToken>('spa * @en Used for injecting spatial query service (read-only) */ export const SpatialQueryToken = createServiceToken>('spatialQuery'); + +/** + * @zh AOI 管理器服务令牌 + * @en AOI manager service token + * + * @zh 用于注入 AOI 兴趣区域管理服务 + * @en Used for injecting AOI (Area of Interest) manager service + */ +export const AOIManagerToken = createServiceToken>('aoiManager');