响应式查询
This commit is contained in:
@@ -6,6 +6,7 @@ import { createLogger } from '../../Utils/Logger';
|
|||||||
import { getComponentTypeName } from '../Decorators';
|
import { getComponentTypeName } from '../Decorators';
|
||||||
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
|
import { Archetype, ArchetypeSystem } from './ArchetypeSystem';
|
||||||
import { ComponentTypeManager } from "../Utils";
|
import { ComponentTypeManager } from "../Utils";
|
||||||
|
import { ReactiveQuery, ReactiveQueryConfig } from './ReactiveQuery';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询条件类型
|
* 查询条件类型
|
||||||
@@ -124,15 +125,16 @@ export class QuerySystem {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 设置实体列表并重建索引
|
* 设置实体列表并重建索引
|
||||||
*
|
*
|
||||||
* 当实体集合发生大规模变化时调用此方法。
|
* 当实体集合发生大规模变化时调用此方法。
|
||||||
* 系统将重新构建所有索引以确保查询性能。
|
* 系统将重新构建所有索引以确保查询性能。
|
||||||
*
|
*
|
||||||
* @param entities 新的实体列表
|
* @param entities 新的实体列表
|
||||||
*/
|
*/
|
||||||
public setEntities(entities: Entity[]): void {
|
public setEntities(entities: Entity[]): void {
|
||||||
this.entities = entities;
|
this.entities = entities;
|
||||||
this.clearQueryCache();
|
this.clearQueryCache();
|
||||||
|
this.clearReactiveQueries();
|
||||||
this.rebuildIndexes();
|
this.rebuildIndexes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,12 +154,14 @@ export class QuerySystem {
|
|||||||
|
|
||||||
this.archetypeSystem.addEntity(entity);
|
this.archetypeSystem.addEntity(entity);
|
||||||
|
|
||||||
|
// 通知响应式查询
|
||||||
|
this.notifyReactiveQueriesEntityAdded(entity);
|
||||||
|
|
||||||
// 只有在非延迟模式下才立即清理缓存
|
// 只有在非延迟模式下才立即清理缓存
|
||||||
if (!deferCacheClear) {
|
if (!deferCacheClear) {
|
||||||
this.clearQueryCache();
|
this.clearQueryCache();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新版本号
|
// 更新版本号
|
||||||
this._version++;
|
this._version++;
|
||||||
}
|
}
|
||||||
@@ -240,6 +244,9 @@ export class QuerySystem {
|
|||||||
|
|
||||||
this.archetypeSystem.removeEntity(entity);
|
this.archetypeSystem.removeEntity(entity);
|
||||||
|
|
||||||
|
// 通知响应式查询
|
||||||
|
this.notifyReactiveQueriesEntityRemoved(entity);
|
||||||
|
|
||||||
this.clearQueryCache();
|
this.clearQueryCache();
|
||||||
|
|
||||||
// 更新版本号
|
// 更新版本号
|
||||||
@@ -270,6 +277,9 @@ export class QuerySystem {
|
|||||||
// 重新添加实体到索引(基于新的组件状态)
|
// 重新添加实体到索引(基于新的组件状态)
|
||||||
this.addEntityToIndexes(entity);
|
this.addEntityToIndexes(entity);
|
||||||
|
|
||||||
|
// 通知响应式查询
|
||||||
|
this.notifyReactiveQueriesEntityChanged(entity);
|
||||||
|
|
||||||
// 清理查询缓存,因为实体组件状态已改变
|
// 清理查询缓存,因为实体组件状态已改变
|
||||||
this.clearQueryCache();
|
this.clearQueryCache();
|
||||||
|
|
||||||
@@ -357,13 +367,13 @@ export class QuerySystem {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询包含所有指定组件的实体
|
* 查询包含所有指定组件的实体
|
||||||
*
|
*
|
||||||
* 返回同时包含所有指定组件类型的实体列表。
|
* 返回同时包含所有指定组件类型的实体列表。
|
||||||
* 系统会自动选择最高效的查询策略,包括索引查找和缓存机制。
|
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。
|
||||||
*
|
*
|
||||||
* @param componentTypes 要查询的组件类型列表
|
* @param componentTypes 要查询的组件类型列表
|
||||||
* @returns 查询结果,包含匹配的实体和性能信息
|
* @returns 查询结果,包含匹配的实体和性能信息
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // 查询同时具有位置和速度组件的实体
|
* // 查询同时具有位置和速度组件的实体
|
||||||
@@ -375,38 +385,20 @@ export class QuerySystem {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
this.queryStats.totalQueries++;
|
this.queryStats.totalQueries++;
|
||||||
|
|
||||||
// 生成缓存键
|
// 使用内部响应式查询作为智能缓存
|
||||||
const cacheKey = this.generateCacheKey('all', componentTypes);
|
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ALL, componentTypes);
|
||||||
|
|
||||||
// 检查缓存
|
// 从响应式查询获取结果(永远是最新的)
|
||||||
const cached = this.getFromCache(cacheKey);
|
const entities = reactiveQuery.getEntities();
|
||||||
if (cached) {
|
|
||||||
this.queryStats.cacheHits++;
|
|
||||||
return {
|
|
||||||
entities: cached,
|
|
||||||
count: cached.length,
|
|
||||||
executionTime: performance.now() - startTime,
|
|
||||||
fromCache: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.queryStats.archetypeHits++;
|
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
|
||||||
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'AND');
|
this.queryStats.cacheHits++;
|
||||||
|
|
||||||
const entities: Entity[] = [];
|
|
||||||
for (const archetype of archetypeResult.archetypes) {
|
|
||||||
for (const entity of archetype.entities) {
|
|
||||||
entities.push(entity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.addToCache(cacheKey, entities);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entities,
|
entities,
|
||||||
count: entities.length,
|
count: entities.length,
|
||||||
executionTime: performance.now() - startTime,
|
executionTime: performance.now() - startTime,
|
||||||
fromCache: false
|
fromCache: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -436,13 +428,13 @@ export class QuerySystem {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询包含任意指定组件的实体
|
* 查询包含任意指定组件的实体
|
||||||
*
|
*
|
||||||
* 返回包含任意一个指定组件类型的实体列表。
|
* 返回包含任意一个指定组件类型的实体列表。
|
||||||
* 使用集合合并算法确保高效的查询性能。
|
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。
|
||||||
*
|
*
|
||||||
* @param componentTypes 要查询的组件类型列表
|
* @param componentTypes 要查询的组件类型列表
|
||||||
* @returns 查询结果,包含匹配的实体和性能信息
|
* @returns 查询结果,包含匹配的实体和性能信息
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // 查询具有武器或护甲组件的实体
|
* // 查询具有武器或护甲组件的实体
|
||||||
@@ -454,52 +446,32 @@ export class QuerySystem {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
this.queryStats.totalQueries++;
|
this.queryStats.totalQueries++;
|
||||||
|
|
||||||
const cacheKey = this.generateCacheKey('any', componentTypes);
|
// 使用内部响应式查询作为智能缓存
|
||||||
|
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.ANY, componentTypes);
|
||||||
|
|
||||||
// 检查缓存
|
// 从响应式查询获取结果(永远是最新的)
|
||||||
const cached = this.getFromCache(cacheKey);
|
const entities = reactiveQuery.getEntities();
|
||||||
if (cached) {
|
|
||||||
this.queryStats.cacheHits++;
|
|
||||||
return {
|
|
||||||
entities: cached,
|
|
||||||
count: cached.length,
|
|
||||||
executionTime: performance.now() - startTime,
|
|
||||||
fromCache: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
this.queryStats.archetypeHits++;
|
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
|
||||||
const archetypeResult = this.archetypeSystem.queryArchetypes(componentTypes, 'OR');
|
this.queryStats.cacheHits++;
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entities: frozenEntities,
|
entities,
|
||||||
count: frozenEntities.length,
|
count: entities.length,
|
||||||
executionTime: performance.now() - startTime,
|
executionTime: performance.now() - startTime,
|
||||||
fromCache: false
|
fromCache: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询不包含任何指定组件的实体
|
* 查询不包含任何指定组件的实体
|
||||||
*
|
*
|
||||||
* 返回不包含任何指定组件类型的实体列表。
|
* 返回不包含任何指定组件类型的实体列表。
|
||||||
* 适用于排除特定类型实体的查询场景。
|
* 内部使用响应式查询作为智能缓存,自动跟踪实体变化,性能更优。
|
||||||
*
|
*
|
||||||
* @param componentTypes 要排除的组件类型列表
|
* @param componentTypes 要排除的组件类型列表
|
||||||
* @returns 查询结果,包含匹配的实体和性能信息
|
* @returns 查询结果,包含匹配的实体和性能信息
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* // 查询不具有AI和玩家控制组件的实体(如静态物体)
|
* // 查询不具有AI和玩家控制组件的实体(如静态物体)
|
||||||
@@ -511,32 +483,20 @@ export class QuerySystem {
|
|||||||
const startTime = performance.now();
|
const startTime = performance.now();
|
||||||
this.queryStats.totalQueries++;
|
this.queryStats.totalQueries++;
|
||||||
|
|
||||||
const cacheKey = this.generateCacheKey('none', componentTypes);
|
// 使用内部响应式查询作为智能缓存
|
||||||
|
const reactiveQuery = this.getOrCreateReactiveQuery(QueryConditionType.NONE, componentTypes);
|
||||||
|
|
||||||
// 检查缓存
|
// 从响应式查询获取结果(永远是最新的)
|
||||||
const cached = this.getFromCache(cacheKey);
|
const entities = reactiveQuery.getEntities();
|
||||||
if (cached) {
|
|
||||||
this.queryStats.cacheHits++;
|
|
||||||
return {
|
|
||||||
entities: cached,
|
|
||||||
count: cached.length,
|
|
||||||
executionTime: performance.now() - startTime,
|
|
||||||
fromCache: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const mask = this.createComponentMask(componentTypes);
|
// 统计为缓存命中(响应式查询本质上是永不过期的智能缓存)
|
||||||
const entities = this.entities.filter(entity =>
|
this.queryStats.cacheHits++;
|
||||||
BitMask64Utils.hasNone(entity.componentMask, mask)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.addToCache(cacheKey, entities);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
entities,
|
entities,
|
||||||
count: entities.length,
|
count: entities.length,
|
||||||
executionTime: performance.now() - startTime,
|
executionTime: performance.now() - startTime,
|
||||||
fromCache: false
|
fromCache: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,6 +719,20 @@ export class QuerySystem {
|
|||||||
this.componentMaskCache.clear();
|
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 要查询的实体
|
* @param entity 要查询的实体
|
||||||
*/
|
*/
|
||||||
public getEntityArchetype(entity: Entity): Archetype | undefined {
|
public getEntityArchetype(entity: Entity): Archetype | undefined {
|
||||||
return this.archetypeSystem.getEntityArchetype(entity);
|
return this.archetypeSystem.getEntityArchetype(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 响应式查询支持(内部智能缓存)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应式查询集合(内部使用,作为智能缓存)
|
||||||
|
* 传统查询API(queryAll/queryAny/queryNone)内部自动使用响应式查询优化性能
|
||||||
|
*/
|
||||||
|
private _reactiveQueries: Map<string, ReactiveQuery> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按组件类型索引的响应式查询
|
||||||
|
* 用于快速定位哪些查询关心某个组件类型
|
||||||
|
*/
|
||||||
|
private _reactiveQueriesByComponent: Map<ComponentType, Set<ReactiveQuery>> = 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<ReactiveQuery>();
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
458
packages/core/src/ECS/Core/ReactiveQuery.ts
Normal file
458
packages/core/src/ECS/Core/ReactiveQuery.ts
Normal file
@@ -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<number> = new Set();
|
||||||
|
|
||||||
|
/** 查询条件 */
|
||||||
|
private readonly _condition: QueryCondition;
|
||||||
|
|
||||||
|
/** 监听器列表 */
|
||||||
|
private _listeners: ReactiveQueryListener[] = [];
|
||||||
|
|
||||||
|
/** 配置 */
|
||||||
|
private readonly _config: ReactiveQueryConfig;
|
||||||
|
|
||||||
|
/** 批量变化缓存 */
|
||||||
|
private _batchChanges: {
|
||||||
|
added: Entity[];
|
||||||
|
removed: Entity[];
|
||||||
|
timer: ReturnType<typeof setTimeout> | 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,4 +15,6 @@ export * from './Core/Storage';
|
|||||||
export * from './Core/StorageDecorators';
|
export * from './Core/StorageDecorators';
|
||||||
export * from './Serialization';
|
export * from './Serialization';
|
||||||
export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker';
|
export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker';
|
||||||
export type { EntityRefRecord } from './Core/ReferenceTracker';
|
export type { EntityRefRecord } from './Core/ReferenceTracker';
|
||||||
|
export { ReactiveQuery, ReactiveQueryChangeType } from './Core/ReactiveQuery';
|
||||||
|
export type { ReactiveQueryChange, ReactiveQueryListener, ReactiveQueryConfig } from './Core/ReactiveQuery';
|
||||||
471
packages/core/tests/ECS/Core/ReactiveQuery.test.ts
Normal file
471
packages/core/tests/ECS/Core/ReactiveQuery.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
173
packages/core/tests/ECS/Core/ReactiveQueryIntegration.test.ts
Normal file
173
packages/core/tests/ECS/Core/ReactiveQueryIntegration.test.ts
Normal file
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user