diff --git a/packages/core/src/ECS/Core/QuerySystem.ts b/packages/core/src/ECS/Core/QuerySystem.ts index aca7fa13..ff051cb2 100644 --- a/packages/core/src/ECS/Core/QuerySystem.ts +++ b/packages/core/src/ECS/Core/QuerySystem.ts @@ -6,6 +6,7 @@ import { createLogger } from '../../Utils/Logger'; import { getComponentTypeName } from '../Decorators'; import { Archetype, ArchetypeSystem } from './ArchetypeSystem'; import { ComponentTypeManager } from "../Utils"; +import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery'; /** * 查询条件类型 @@ -124,15 +125,16 @@ export class QuerySystem { /** * 设置实体列表并重建索引 - * + * * 当实体集合发生大规模变化时调用此方法。 * 系统将重新构建所有索引以确保查询性能。 - * + * * @param entities 新的实体列表 */ public setEntities(entities: Entity[]): void { this.entities = entities; this.clearQueryCache(); + this.clearReactiveQueries(); this.rebuildIndexes(); } @@ -152,12 +154,14 @@ export class QuerySystem { this.archetypeSystem.addEntity(entity); + // 通知响应式查询 + this.notifyReactiveQueriesEntityAdded(entity); // 只有在非延迟模式下才立即清理缓存 if (!deferCacheClear) { this.clearQueryCache(); } - + // 更新版本号 this._version++; } @@ -240,6 +244,9 @@ export class QuerySystem { this.archetypeSystem.removeEntity(entity); + // 通知响应式查询 + this.notifyReactiveQueriesEntityRemoved(entity); + this.clearQueryCache(); // 更新版本号 @@ -270,6 +277,9 @@ export class QuerySystem { // 重新添加实体到索引(基于新的组件状态) this.addEntityToIndexes(entity); + // 通知响应式查询 + this.notifyReactiveQueriesEntityChanged(entity); + // 清理查询缓存,因为实体组件状态已改变 this.clearQueryCache(); @@ -357,13 +367,13 @@ export class QuerySystem { /** * 查询包含所有指定组件的实体 - * + * * 返回同时包含所有指定组件类型的实体列表。 - * 系统会自动选择最高效的查询策略,包括索引查找和缓存机制。 - * + * 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。 + * * @param componentTypes 要查询的组件类型列表 * @returns 查询结果,包含匹配的实体和性能信息 - * + * * @example * ```typescript * // 查询同时具有位置和速度组件的实体 @@ -375,38 +385,20 @@ export class QuerySystem { const startTime = performance.now(); this.queryStats.totalQueries++; - // 生成缓存键 - const cacheKey = this.generateCacheKey('all', componentTypes); + // 使用内部响应式查询作为智能缓存 + const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ALL, componentTypes); - // 检查缓存 - const cached = this.getFromCache(cacheKey); - if (cached) { - this.queryStats.cacheHits++; - return { - entities: cached, - count: cached.length, - executionTime: performance.now() - startTime, - fromCache: true - }; - } + // 从响应式查询获取结果(永远是最新的) + const entities = reactiveQuery.getEntities(); - this.queryStats.archetypeHits++; - const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'AND'); - - const entities: Entity[] = []; - for (const archetype of archetypeResult.archetypes) { - for (const entity of archetype.entities) { - entities.push(entity); - } - } - - this.addToCache(cacheKey, entities); + // 统计为缓存命中(响应式查询本质上是永不过期的智能缓存) + this.queryStats.cacheHits++; return { entities, count: entities.length, executionTime: performance.now() - startTime, - fromCache: false + fromCache: true }; } @@ -436,13 +428,13 @@ export class QuerySystem { /** * 查询包含任意指定组件的实体 - * + * * 返回包含任意一个指定组件类型的实体列表。 - * 使用集合合并算法确保高效的查询性能。 - * + * 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。 + * * @param componentTypes 要查询的组件类型列表 * @returns 查询结果,包含匹配的实体和性能信息 - * + * * @example * ```typescript * // 查询具有武器或护甲组件的实体 @@ -454,52 +446,32 @@ export class QuerySystem { const startTime = performance.now(); this.queryStats.totalQueries++; - const cacheKey = this.generateCacheKey('any', componentTypes); + // 使用内部响应式查询作为智能缓存 + const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ANY, componentTypes); - // 检查缓存 - const cached = this.getFromCache(cacheKey); - if (cached) { - this.queryStats.cacheHits++; - return { - entities: cached, - count: cached.length, - executionTime: performance.now() - startTime, - fromCache: true - }; - } + // 从响应式查询获取结果(永远是最新的) + const entities = reactiveQuery.getEntities(); - this.queryStats.archetypeHits++; - const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR'); - - const entities = this.acquireResultArray(); - for (const archetype of archetypeResult.archetypes) { - for (const entity of archetype.entities) { - entities.push(entity); - } - } - - const frozenEntities = [...entities]; - this.releaseResultArray(entities); - - this.addToCache(cacheKey, frozenEntities); + // 统计为缓存命中(响应式查询本质上是永不过期的智能缓存) + this.queryStats.cacheHits++; return { - entities: frozenEntities, - count: frozenEntities.length, + entities, + count: entities.length, executionTime: performance.now() - startTime, - fromCache: false + fromCache: true }; } /** * 查询不包含任何指定组件的实体 - * + * * 返回不包含任何指定组件类型的实体列表。 - * 适用于排除特定类型实体的查询场景。 - * + * 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。 + * * @param componentTypes 要排除的组件类型列表 * @returns 查询结果,包含匹配的实体和性能信息 - * + * * @example * ```typescript * // 查询不具有AI和玩家控制组件的实体(如静态物体) @@ -511,32 +483,20 @@ export class QuerySystem { const startTime = performance.now(); this.queryStats.totalQueries++; - const cacheKey = this.generateCacheKey('none', componentTypes); + // 使用内部响应式查询作为智能缓存 + const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.NONE, componentTypes); - // 检查缓存 - const cached = this.getFromCache(cacheKey); - if (cached) { - this.queryStats.cacheHits++; - return { - entities: cached, - count: cached.length, - executionTime: performance.now() - startTime, - fromCache: true - }; - } + // 从响应式查询获取结果(永远是最新的) + const entities = reactiveQuery.getEntities(); - const mask = this.createComponentMask(componentTypes); - const entities = this.entities.filter(entity => - BitMask64Utils.hasNone(entity.componentMask, mask) - ); - - this.addToCache(cacheKey, entities); + // 统计为缓存命中(响应式查询本质上是永不过期的智能缓存) + this.queryStats.cacheHits++; return { entities, count: entities.length, executionTime: performance.now() - startTime, - fromCache: false + fromCache: true }; } @@ -759,6 +719,20 @@ export class QuerySystem { this.componentMaskCache.clear(); } + /** + * 清除所有响应式查询 + * + * 销毁所有响应式查询实例并清理索引 + * 通常在setEntities时调用以确保缓存一致性 + */ + private clearReactiveQueries(): void { + for (const query of this._reactiveQueries.values()) { + query.dispose(); + } + this._reactiveQueries.clear(); + this._reactiveQueriesByComponent.clear(); + } + /** * 高效的缓存键生成 */ @@ -891,12 +865,187 @@ export class QuerySystem { /** * 获取实体所属的原型信息 - * + * * @param entity 要查询的实体 */ public getEntityArchetype(entity: Entity): Archetype | undefined { return this.archetypeSystem.getEntityArchetype(entity); } + + // ============================================================ + // 响应式查询支持(内部智能缓存) + // ============================================================ + + /** + * 响应式查询集合(内部使用,作为智能缓存) + * 传统查询API(queryAll/queryAny/queryNone)内部自动使用响应式查询优化性能 + */ + private _reactiveQueries: Map = new Map(); + + /** + * 按组件类型索引的响应式查询 + * 用于快速定位哪些查询关心某个组件类型 + */ + private _reactiveQueriesByComponent: Map> = new Map(); + + /** + * 获取或创建内部响应式查询(作为智能缓存) + * + * @param queryType 查询类型 + * @param componentTypes 组件类型列表 + * @returns 响应式查询实例 + */ + private getOrCreateReactiveQuery( + queryType: QueryConditionType, + componentTypes: ComponentType[] + ): ReactiveQuery { + // 生成缓存键(与传统缓存键格式一致) + const cacheKey = this.generateCacheKey(queryType, componentTypes); + + // 检查是否已存在响应式查询 + let reactiveQuery = this._reactiveQueries.get(cacheKey); + + if (!reactiveQuery) { + // 创建查询条件 + const mask = this.createComponentMask(componentTypes); + const condition: QueryCondition = { + type: queryType, + componentTypes, + mask + }; + + // 创建响应式查询(禁用批量模式,保持实时性) + reactiveQuery = new ReactiveQuery(condition, { + enableBatchMode: false, + debug: false + }); + + // 初始化查询结果(使用传统方式获取初始数据) + const initialEntities = this.executeTraditionalQuery(queryType, componentTypes); + reactiveQuery.initializeWith(initialEntities); + + // 注册响应式查询 + this._reactiveQueries.set(cacheKey, reactiveQuery); + + // 为每个组件类型注册索引 + for (const type of componentTypes) { + let queries = this._reactiveQueriesByComponent.get(type); + if (!queries) { + queries = new Set(); + this._reactiveQueriesByComponent.set(type, queries); + } + queries.add(reactiveQuery); + } + + this._logger.debug(`创建内部响应式查询缓存: ${cacheKey}`); + } + + return reactiveQuery; + } + + /** + * 执行传统查询(内部使用,用于响应式查询初始化) + * + * @param queryType 查询类型 + * @param componentTypes 组件类型列表 + * @returns 匹配的实体列表 + */ + private executeTraditionalQuery( + queryType: QueryConditionType, + componentTypes: ComponentType[] + ): Entity[] { + switch (queryType) { + case QueryConditionType.ALL: { + const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'AND'); + const entities: Entity[] = []; + for (const archetype of archetypeResult.archetypes) { + for (const entity of archetype.entities) { + entities.push(entity); + } + } + return entities; + } + case QueryConditionType.ANY: { + const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR'); + const entities: Entity[] = []; + for (const archetype of archetypeResult.archetypes) { + for (const entity of archetype.entities) { + entities.push(entity); + } + } + return entities; + } + case QueryConditionType.NONE: { + const mask = this.createComponentMask(componentTypes); + return this.entities.filter(entity => + BitMask64Utils.hasNone(entity.componentMask, mask) + ); + } + default: + return []; + } + } + + /** + * 通知所有响应式查询实体已添加 + * + * 使用组件类型索引,只通知关心该实体组件的查询 + * + * @param entity 添加的实体 + */ + private notifyReactiveQueriesEntityAdded(entity: Entity): void { + if (this._reactiveQueries.size === 0) return; + + const notified = new Set(); + + for (const component of entity.components) { + const componentType = component.constructor as ComponentType; + const queries = this._reactiveQueriesByComponent.get(componentType); + + if (queries) { + for (const query of queries) { + if (!notified.has(query)) { + query.notifyEntityAdded(entity); + notified.add(query); + } + } + } + } + } + + /** + * 通知所有响应式查询实体已移除 + * + * 使用组件类型索引,只通知关心该实体组件的查询 + * 注意:实体移除时可能已经清空了components,所以需要通知所有查询让它们自己判断 + * + * @param entity 移除的实体 + */ + private notifyReactiveQueriesEntityRemoved(entity: Entity): void { + if (this._reactiveQueries.size === 0) return; + + for (const query of this._reactiveQueries.values()) { + query.notifyEntityRemoved(entity); + } + } + + /** + * 通知所有响应式查询实体已变化 + * + * 实体组件变化时需要通知所有查询,因为: + * 1. 可能从匹配变为不匹配(移除组件) + * 2. 可能从不匹配变为匹配(添加组件) + * 让每个查询自己判断是否需要响应 + * + * @param entity 变化的实体 + */ + private notifyReactiveQueriesEntityChanged(entity: Entity): void { + if (this._reactiveQueries.size === 0) return; + + for (const query of this._reactiveQueries.values()) { + query.notifyEntityChanged(entity); + } + } } /** diff --git a/packages/core/src/ECS/Core/ReactiveQuery.ts b/packages/core/src/ECS/Core/ReactiveQuery.ts new file mode 100644 index 00000000..19aecd5d --- /dev/null +++ b/packages/core/src/ECS/Core/ReactiveQuery.ts @@ -0,0 +1,458 @@ +import { Entity } from '../Entity'; +import { QueryCondition, QueryConditionType } from './QuerySystem'; +import { BitMask64Utils } from '../Utils/BigIntCompatibility'; +import { createLogger } from '../../Utils/Logger'; + +const logger = createLogger('ReactiveQuery'); + +/** + * 响应式查询变化类型 + */ +export enum ReactiveQueryChangeType { + /** 实体添加到查询结果 */ + ADDED = 'added', + /** 实体从查询结果移除 */ + REMOVED = 'removed', + /** 查询结果批量更新 */ + BATCH_UPDATE = 'batch_update' +} + +/** + * 响应式查询变化事件 + */ +export interface ReactiveQueryChange { + /** 变化类型 */ + type: ReactiveQueryChangeType; + /** 变化的实体 */ + entity?: Entity; + /** 批量变化的实体 */ + entities?: readonly Entity[]; + /** 新增的实体列表(仅batch_update时有效) */ + added?: readonly Entity[]; + /** 移除的实体列表(仅batch_update时有效) */ + removed?: readonly Entity[]; +} + +/** + * 响应式查询监听器 + */ +export type ReactiveQueryListener = (change: ReactiveQueryChange) => void; + +/** + * 响应式查询配置 + */ +export interface ReactiveQueryConfig { + /** 是否启用批量模式(减少通知频率) */ + enableBatchMode?: boolean; + /** 批量模式的延迟时间(毫秒) */ + batchDelay?: number; + /** 调试模式 */ + debug?: boolean; +} + +/** + * 响应式查询类 + * + * 提供基于事件驱动的实体查询机制,只在实体/组件真正变化时触发通知。 + * + * 核心特性: + * - Event-driven: 基于事件的增量更新 + * - 精确通知: 只通知真正匹配的变化 + * - 性能优化: 避免每帧重复查询 + * + * @example + * ```typescript + * // 创建响应式查询 + * const query = new ReactiveQuery(querySystem, { + * type: QueryConditionType.ALL, + * componentTypes: [Position, Velocity], + * mask: createMask([Position, Velocity]) + * }); + * + * // 订阅变化 + * query.subscribe((change) => { + * if (change.type === ReactiveQueryChangeType.ADDED) { + * console.log('新实体:', change.entity); + * } + * }); + * + * // 获取当前结果 + * const entities = query.getEntities(); + * ``` + */ +export class ReactiveQuery { + /** 当前查询结果 */ + private _entities: Entity[] = []; + + /** 实体ID集合,用于快速查找 */ + private _entityIdSet: Set = new Set(); + + /** 查询条件 */ + private readonly _condition: QueryCondition; + + /** 监听器列表 */ + private _listeners: ReactiveQueryListener[] = []; + + /** 配置 */ + private readonly _config: ReactiveQueryConfig; + + /** 批量变化缓存 */ + private _batchChanges: { + added: Entity[]; + removed: Entity[]; + timer: ReturnType | null; + }; + + /** 查询ID(用于调试) */ + private readonly _id: string; + + /** 是否已激活 */ + private _active: boolean = true; + + constructor(condition: QueryCondition, config: ReactiveQueryConfig = {}) { + this._condition = condition; + this._config = { + enableBatchMode: config.enableBatchMode ?? true, + batchDelay: config.batchDelay ?? 16, // 默认一帧 + debug: config.debug ?? false + }; + + this._id = this.generateQueryId(); + + this._batchChanges = { + added: [], + removed: [], + timer: null + }; + + if (this._config.debug) { + logger.debug(`创建ReactiveQuery: ${this._id}`); + } + } + + /** + * 生成查询ID + */ + private generateQueryId(): string { + const typeStr = this._condition.type; + const componentsStr = this._condition.componentTypes + .map(t => t.name) + .sort() + .join(','); + return `${typeStr}:${componentsStr}`; + } + + /** + * 订阅查询变化 + * + * @param listener 监听器函数 + * @returns 取消订阅的函数 + */ + public subscribe(listener: ReactiveQueryListener): () => void { + this._listeners.push(listener); + + if (this._config.debug) { + logger.debug(`订阅ReactiveQuery: ${this._id}, 监听器数量: ${this._listeners.length}`); + } + + // 返回取消订阅函数 + return () => { + const index = this._listeners.indexOf(listener); + if (index !== -1) { + this._listeners.splice(index, 1); + } + }; + } + + /** + * 取消所有订阅 + */ + public unsubscribeAll(): void { + this._listeners.length = 0; + } + + /** + * 获取当前查询结果 + */ + public getEntities(): readonly Entity[] { + return this._entities; + } + + /** + * 获取查询结果数量 + */ + public get count(): number { + return this._entities.length; + } + + /** + * 检查实体是否匹配查询条件 + * + * @param entity 要检查的实体 + * @returns 是否匹配 + */ + public matches(entity: Entity): boolean { + const entityMask = entity.componentMask; + + switch (this._condition.type) { + case QueryConditionType.ALL: + return BitMask64Utils.hasAll(entityMask, this._condition.mask); + case QueryConditionType.ANY: + return BitMask64Utils.hasAny(entityMask, this._condition.mask); + case QueryConditionType.NONE: + return BitMask64Utils.hasNone(entityMask, this._condition.mask); + default: + return false; + } + } + + /** + * 通知实体添加 + * + * 当Scene中添加实体时调用 + * + * @param entity 添加的实体 + */ + public notifyEntityAdded(entity: Entity): void { + if (!this._active) return; + + // 检查实体是否匹配查询条件 + if (!this.matches(entity)) { + return; + } + + // 检查是否已存在 + if (this._entityIdSet.has(entity.id)) { + return; + } + + // 添加到结果集 + this._entities.push(entity); + this._entityIdSet.add(entity.id); + + // 通知监听器 + if (this._config.enableBatchMode) { + this.addToBatch('added', entity); + } else { + this.notifyListeners({ + type: ReactiveQueryChangeType.ADDED, + entity + }); + } + + if (this._config.debug) { + logger.debug(`ReactiveQuery ${this._id}: 实体添加 ${entity.name}(${entity.id})`); + } + } + + /** + * 通知实体移除 + * + * 当Scene中移除实体时调用 + * + * @param entity 移除的实体 + */ + public notifyEntityRemoved(entity: Entity): void { + if (!this._active) return; + + // 检查是否在结果集中 + if (!this._entityIdSet.has(entity.id)) { + return; + } + + // 从结果集移除 + const index = this._entities.indexOf(entity); + if (index !== -1) { + this._entities.splice(index, 1); + } + this._entityIdSet.delete(entity.id); + + // 通知监听器 + if (this._config.enableBatchMode) { + this.addToBatch('removed', entity); + } else { + this.notifyListeners({ + type: ReactiveQueryChangeType.REMOVED, + entity + }); + } + + if (this._config.debug) { + logger.debug(`ReactiveQuery ${this._id}: 实体移除 ${entity.name}(${entity.id})`); + } + } + + /** + * 通知实体组件变化 + * + * 当实体的组件发生变化时调用 + * + * @param entity 变化的实体 + */ + public notifyEntityChanged(entity: Entity): void { + if (!this._active) return; + + const wasMatching = this._entityIdSet.has(entity.id); + const isMatching = this.matches(entity); + + if (wasMatching && !isMatching) { + // 实体不再匹配,从结果集移除 + this.notifyEntityRemoved(entity); + } else if (!wasMatching && isMatching) { + // 实体现在匹配,添加到结果集 + this.notifyEntityAdded(entity); + } + } + + /** + * 批量初始化查询结果 + * + * @param entities 初始实体列表 + */ + public initializeWith(entities: readonly Entity[]): void { + // 清空现有结果 + this._entities.length = 0; + this._entityIdSet.clear(); + + // 筛选匹配的实体 + for (const entity of entities) { + if (this.matches(entity)) { + this._entities.push(entity); + this._entityIdSet.add(entity.id); + } + } + + if (this._config.debug) { + logger.debug(`ReactiveQuery ${this._id}: 初始化 ${this._entities.length} 个实体`); + } + } + + /** + * 添加到批量变化缓存 + */ + private addToBatch(type: 'added' | 'removed', entity: Entity): void { + if (type === 'added') { + this._batchChanges.added.push(entity); + } else { + this._batchChanges.removed.push(entity); + } + + // 启动批量通知定时器 + if (this._batchChanges.timer === null) { + this._batchChanges.timer = setTimeout(() => { + this.flushBatchChanges(); + }, this._config.batchDelay); + } + } + + /** + * 刷新批量变化 + */ + private flushBatchChanges(): void { + if (this._batchChanges.added.length === 0 && this._batchChanges.removed.length === 0) { + this._batchChanges.timer = null; + return; + } + + const added = [...this._batchChanges.added]; + const removed = [...this._batchChanges.removed]; + + // 清空缓存 + this._batchChanges.added.length = 0; + this._batchChanges.removed.length = 0; + this._batchChanges.timer = null; + + // 通知监听器 + this.notifyListeners({ + type: ReactiveQueryChangeType.BATCH_UPDATE, + added, + removed, + entities: this._entities + }); + + if (this._config.debug) { + logger.debug(`ReactiveQuery ${this._id}: 批量更新 +${added.length} -${removed.length}`); + } + } + + /** + * 通知所有监听器 + */ + private notifyListeners(change: ReactiveQueryChange): void { + for (const listener of this._listeners) { + try { + listener(change); + } catch (error) { + logger.error(`ReactiveQuery ${this._id}: 监听器执行出错`, error); + } + } + } + + /** + * 暂停响应式查询 + * + * 暂停后不再响应实体变化,但可以继续获取当前结果 + */ + public pause(): void { + this._active = false; + + // 清空批量变化缓存 + if (this._batchChanges.timer !== null) { + clearTimeout(this._batchChanges.timer); + this._batchChanges.timer = null; + } + this._batchChanges.added.length = 0; + this._batchChanges.removed.length = 0; + } + + /** + * 恢复响应式查询 + */ + public resume(): void { + this._active = true; + } + + /** + * 销毁响应式查询 + * + * 释放所有资源,清空监听器和结果集 + */ + public dispose(): void { + this.pause(); + this.unsubscribeAll(); + this._entities.length = 0; + this._entityIdSet.clear(); + + if (this._config.debug) { + logger.debug(`ReactiveQuery ${this._id}: 已销毁`); + } + } + + /** + * 获取查询条件 + */ + public get condition(): QueryCondition { + return this._condition; + } + + /** + * 获取查询ID + */ + public get id(): string { + return this._id; + } + + /** + * 检查是否激活 + */ + public get active(): boolean { + return this._active; + } + + /** + * 获取监听器数量 + */ + public get listenerCount(): number { + return this._listeners.length; + } +} diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index deb63b75..e0c66e28 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -15,4 +15,6 @@ export * from './Core/Storage'; export * from './Core/StorageDecorators'; export * from './Serialization'; export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker'; -export type { EntityRefRecord } from './Core/ReferenceTracker'; \ No newline at end of file +export type { EntityRefRecord } from './Core/ReferenceTracker'; +export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery'; +export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } from './Core/ReactiveQuery'; \ No newline at end of file diff --git a/packages/core/tests/ECS/Core/ReactiveQuery.test.ts b/packages/core/tests/ECS/Core/ReactiveQuery.test.ts new file mode 100644 index 00000000..2bd53b48 --- /dev/null +++ b/packages/core/tests/ECS/Core/ReactiveQuery.test.ts @@ -0,0 +1,471 @@ +import { Scene } from '../../../src/ECS/Scene'; +import { Component } from '../../../src/ECS/Component'; +import { ReactiveQueryChangeType } from '../../../src/ECS/Core/ReactiveQuery'; + +class PositionComponent extends Component { + public x: number = 0; + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } + + public reset(): void { + this.x = 0; + this.y = 0; + } +} + +class VelocityComponent extends Component { + public vx: number = 0; + public vy: number = 0; + + constructor(vx: number = 0, vy: number = 0) { + super(); + this.vx = vx; + this.vy = vy; + } + + public reset(): void { + this.vx = 0; + this.vy = 0; + } +} + +class HealthComponent extends Component { + public hp: number = 100; + + constructor(hp: number = 100) { + super(); + this.hp = hp; + } + + public reset(): void { + this.hp = 100; + } +} + +describe('ReactiveQuery', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene(); + }); + + afterEach(() => { + scene.end(); + jest.clearAllTimers(); + }); + + describe('基础功能', () => { + test('应该能够创建响应式查询', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent]); + + expect(query).toBeDefined(); + expect(query.count).toBe(0); + expect(query.getEntities()).toEqual([]); + }); + + test('应该能够初始化查询结果', () => { + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + const query = scene.querySystem.createReactiveQuery([PositionComponent]); + + expect(query.count).toBe(2); + expect(query.getEntities()).toContain(entity1); + expect(query.getEntities()).toContain(entity2); + }); + + test('应该能够销毁响应式查询', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent]); + + scene.querySystem.destroyReactiveQuery(query); + + expect(query.active).toBe(false); + }); + }); + + describe('实体添加通知', () => { + test('应该在添加匹配实体时通知订阅者', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + expect(changes).toHaveLength(1); + expect(changes[0].type).toBe(ReactiveQueryChangeType.ADDED); + expect(changes[0].entity).toBe(entity); + }); + + test('不应该在添加不匹配实体时通知订阅者', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + const entity = scene.createEntity('test'); + entity.addComponent(new HealthComponent(100)); + + expect(changes).toHaveLength(0); + }); + + test('批量模式应该合并通知', (done) => { + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: true, + batchDelay: 10 + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + setTimeout(() => { + expect(changes).toHaveLength(1); + expect(changes[0].type).toBe(ReactiveQueryChangeType.BATCH_UPDATE); + expect(changes[0].added).toHaveLength(2); + done(); + }, 50); + }); + }); + + describe('实体移除通知', () => { + test('应该在移除匹配实体时通知订阅者', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + scene.destroyEntities([entity]); + + expect(changes).toHaveLength(1); + expect(changes[0].type).toBe(ReactiveQueryChangeType.REMOVED); + expect(changes[0].entity).toBe(entity); + }); + + test('不应该在移除不匹配实体时通知订阅者', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new HealthComponent(100)); + + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + scene.destroyEntities([entity]); + + expect(changes).toHaveLength(0); + }); + }); + + describe('实体变化通知', () => { + test('应该在实体从不匹配变为匹配时通知添加', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new HealthComponent(100)); + + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + entity.addComponent(new PositionComponent(10, 20)); + + expect(changes).toHaveLength(1); + expect(changes[0].type).toBe(ReactiveQueryChangeType.ADDED); + expect(changes[0].entity).toBe(entity); + }); + + test('应该在实体从匹配变为不匹配时通知移除', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + const positionComp = entity.getComponent(PositionComponent); + if (positionComp) { + entity.removeComponent(positionComp); + } + + expect(changes).toHaveLength(1); + expect(changes[0].type).toBe(ReactiveQueryChangeType.REMOVED); + expect(changes[0].entity).toBe(entity); + }); + + test('应该在实体组件变化但仍匹配时不通知', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + entity.addComponent(new HealthComponent(100)); + + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + const healthComp = entity.getComponent(HealthComponent); + if (healthComp) { + entity.removeComponent(healthComp); + } + + expect(changes).toHaveLength(0); + }); + }); + + describe('多组件查询', () => { + test('应该正确匹配多个组件', () => { + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + entity1.addComponent(new VelocityComponent(1, 1)); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + const query = scene.querySystem.createReactiveQuery([PositionComponent, VelocityComponent]); + + expect(query.count).toBe(1); + expect(query.getEntities()).toContain(entity1); + expect(query.getEntities()).not.toContain(entity2); + }); + + test('应该在实体满足所有组件时通知添加', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + const query = scene.querySystem.createReactiveQuery([PositionComponent, VelocityComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + entity.addComponent(new VelocityComponent(1, 1)); + + expect(changes).toHaveLength(1); + expect(changes[0].type).toBe(ReactiveQueryChangeType.ADDED); + expect(changes[0].entity).toBe(entity); + }); + }); + + describe('订阅管理', () => { + test('应该能够取消订阅', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + const unsubscribe = query.subscribe((change) => { + changes.push(change); + }); + + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + expect(changes).toHaveLength(1); + + unsubscribe(); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + expect(changes).toHaveLength(1); + }); + + test('应该能够取消所有订阅', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes1: any[] = []; + query.subscribe((change) => { + changes1.push(change); + }); + + const changes2: any[] = []; + query.subscribe((change) => { + changes2.push(change); + }); + + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + expect(changes1).toHaveLength(1); + expect(changes2).toHaveLength(1); + + query.unsubscribeAll(); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + expect(changes1).toHaveLength(1); + expect(changes2).toHaveLength(1); + }); + + test('应该能够暂停和恢复查询', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + query.pause(); + + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new PositionComponent(10, 20)); + + expect(changes).toHaveLength(0); + + query.resume(); + + const entity2 = scene.createEntity('entity2'); + entity2.addComponent(new PositionComponent(30, 40)); + + expect(changes).toHaveLength(1); + }); + }); + + describe('性能测试', () => { + test('应该高效处理大量实体变化', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + let changeCount = 0; + query.subscribe(() => { + changeCount++; + }); + + const startTime = performance.now(); + + for (let i = 0; i < 1000; i++) { + const entity = scene.createEntity(`entity${i}`); + entity.addComponent(new PositionComponent(i, i)); + } + + const endTime = performance.now(); + + expect(changeCount).toBe(1000); + expect(endTime - startTime).toBeLessThan(100); + }); + + test('批量模式应该减少通知次数', (done) => { + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: true, + batchDelay: 10 + }); + + let changeCount = 0; + query.subscribe(() => { + changeCount++; + }); + + for (let i = 0; i < 100; i++) { + const entity = scene.createEntity(`entity${i}`); + entity.addComponent(new PositionComponent(i, i)); + } + + setTimeout(() => { + expect(changeCount).toBe(1); + done(); + }, 50); + }); + }); + + describe('边界情况', () => { + test('应该处理重复添加同一实体', () => { + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + scene.querySystem.addEntity(entity); + scene.querySystem.addEntity(entity); + + expect(changes).toHaveLength(0); + }); + + test('应该处理查询结果为空的情况', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent]); + + expect(query.count).toBe(0); + expect(query.getEntities()).toEqual([]); + }); + + test('应该在销毁后停止通知', () => { + const query = scene.querySystem.createReactiveQuery([PositionComponent], { + enableBatchMode: false + }); + + const changes: any[] = []; + query.subscribe((change) => { + changes.push(change); + }); + + query.dispose(); + + const entity = scene.createEntity('test'); + entity.addComponent(new PositionComponent(10, 20)); + + expect(changes).toHaveLength(0); + }); + }); +}); diff --git a/packages/core/tests/ECS/Core/ReactiveQueryIntegration.test.ts b/packages/core/tests/ECS/Core/ReactiveQueryIntegration.test.ts new file mode 100644 index 00000000..194b022e --- /dev/null +++ b/packages/core/tests/ECS/Core/ReactiveQueryIntegration.test.ts @@ -0,0 +1,173 @@ +import { Scene } from '../../../src/ECS/Scene'; +import { Component } from '../../../src/ECS/Component'; +import { EntitySystem } from '../../../src/ECS/Systems/EntitySystem'; +import { Matcher } from '../../../src/ECS/Utils/Matcher'; +import { ReactiveQuery, ReactiveQueryChangeType } from '../../../src/ECS/Core/ReactiveQuery'; + +class TransformComponent extends Component { + public x: number = 0; + public y: number = 0; + + constructor(x: number = 0, y: number = 0) { + super(); + this.x = x; + this.y = y; + } + + public reset(): void { + this.x = 0; + this.y = 0; + } +} + +class RenderableComponent extends Component { + public visible: boolean = true; + + public reset(): void { + this.visible = true; + } +} + +class ReactiveRenderSystem extends EntitySystem { + private reactiveQuery!: ReactiveQuery; + private addedCount = 0; + private removedCount = 0; + + public override initialize(): void { + super.initialize(); + + if (this.scene) { + this.reactiveQuery = this.scene.querySystem.createReactiveQuery( + [TransformComponent, RenderableComponent], + { + enableBatchMode: false + } + ); + + this.reactiveQuery.subscribe((change) => { + if (change.type === ReactiveQueryChangeType.ADDED) { + this.addedCount++; + } else if (change.type === ReactiveQueryChangeType.REMOVED) { + this.removedCount++; + } + }); + } + } + + public getAddedCount(): number { + return this.addedCount; + } + + public getRemovedCount(): number { + return this.removedCount; + } + + public getQueryEntities() { + return this.reactiveQuery.getEntities(); + } + + public override dispose(): void { + if (this.reactiveQuery && this.scene) { + this.scene.querySystem.destroyReactiveQuery(this.reactiveQuery); + } + } +} + +describe('ReactiveQuery集成测试', () => { + let scene: Scene; + let renderSystem: ReactiveRenderSystem; + + beforeEach(() => { + scene = new Scene(); + renderSystem = new ReactiveRenderSystem(Matcher.empty()); + scene.addEntityProcessor(renderSystem); + }); + + afterEach(() => { + scene.end(); + jest.clearAllTimers(); + }); + + describe('EntitySystem集成', () => { + test('应该在实体添加时收到通知', () => { + const entity1 = scene.createEntity('entity1'); + entity1.addComponent(new TransformComponent(10, 20)); + entity1.addComponent(new RenderableComponent()); + + expect(renderSystem.getAddedCount()).toBe(1); + expect(renderSystem.getQueryEntities()).toContain(entity1); + }); + + test('应该在实体移除时收到通知', () => { + const entity = scene.createEntity('entity'); + entity.addComponent(new TransformComponent(10, 20)); + entity.addComponent(new RenderableComponent()); + + expect(renderSystem.getAddedCount()).toBe(1); + + scene.destroyEntities([entity]); + + expect(renderSystem.getRemovedCount()).toBe(1); + expect(renderSystem.getQueryEntities()).not.toContain(entity); + }); + + test('应该在组件变化时收到正确通知', () => { + const entity = scene.createEntity('entity'); + entity.addComponent(new TransformComponent(10, 20)); + + expect(renderSystem.getAddedCount()).toBe(0); + + entity.addComponent(new RenderableComponent()); + + expect(renderSystem.getAddedCount()).toBe(1); + expect(renderSystem.getQueryEntities()).toContain(entity); + + const renderComp = entity.getComponent(RenderableComponent); + if (renderComp) { + entity.removeComponent(renderComp); + } + + expect(renderSystem.getRemovedCount()).toBe(1); + expect(renderSystem.getQueryEntities()).not.toContain(entity); + }); + + test('应该高效处理批量实体变化', () => { + const entities = []; + + for (let i = 0; i < 100; i++) { + const entity = scene.createEntity(`entity${i}`); + entity.addComponent(new TransformComponent(i, i)); + entity.addComponent(new RenderableComponent()); + entities.push(entity); + } + + expect(renderSystem.getAddedCount()).toBe(100); + expect(renderSystem.getQueryEntities().length).toBe(100); + + scene.destroyEntities(entities); + + expect(renderSystem.getRemovedCount()).toBe(100); + expect(renderSystem.getQueryEntities().length).toBe(0); + }); + }); + + describe('性能对比', () => { + test('响应式查询应该避免每帧重复查询', () => { + for (let i = 0; i < 50; i++) { + const entity = scene.createEntity(`entity${i}`); + entity.addComponent(new TransformComponent(i, i)); + entity.addComponent(new RenderableComponent()); + } + + expect(renderSystem.getAddedCount()).toBe(50); + + const initialCount = renderSystem.getAddedCount(); + + for (let i = 0; i < 100; i++) { + scene.update(); + } + + expect(renderSystem.getAddedCount()).toBe(initialCount); + }); + }); +});