From 7850fc610c512a8a71cb4176ab9f0bb93a8940b7 Mon Sep 17 00:00:00 2001 From: YHH <359807859@qq.com> Date: Fri, 10 Oct 2025 23:38:48 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=84=E4=BB=B6=E5=BC=95=E7=94=A8=E5=AE=8C?= =?UTF-8?q?=E6=95=B4=E6=80=A7=EF=BC=8C=E5=8D=87=E7=BA=A7=E5=88=B0es2021?= =?UTF-8?q?=E4=BD=BF=E7=94=A8weakref?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/core/src/ECS/Component.ts | 7 + .../core/src/ECS/Core/ReferenceTracker.ts | 296 ++++++++++++++++++ .../src/ECS/Decorators/EntityRefDecorator.ts | 147 +++++++++ packages/core/src/ECS/Decorators/index.ts | 11 +- packages/core/src/ECS/Entity.ts | 15 + packages/core/src/ECS/IScene.ts | 6 + packages/core/src/ECS/Scene.ts | 9 + packages/core/src/ECS/index.ts | 4 +- packages/core/src/Types/index.ts | 2 +- .../tests/ECS/Core/ReferenceTracker.test.ts | 254 +++++++++++++++ .../tests/ECS/EntityRefIntegration.test.ts | 274 ++++++++++++++++ packages/core/tsconfig.json | 4 +- 12 files changed, 1024 insertions(+), 5 deletions(-) create mode 100644 packages/core/src/ECS/Core/ReferenceTracker.ts create mode 100644 packages/core/src/ECS/Decorators/EntityRefDecorator.ts create mode 100644 packages/core/tests/ECS/Core/ReferenceTracker.test.ts create mode 100644 packages/core/tests/ECS/EntityRefIntegration.test.ts diff --git a/packages/core/src/ECS/Component.ts b/packages/core/src/ECS/Component.ts index bc03773d..1b112dda 100644 --- a/packages/core/src/ECS/Component.ts +++ b/packages/core/src/ECS/Component.ts @@ -45,6 +45,13 @@ export abstract class Component implements IComponent { */ public readonly id: number; + /** + * 所属实体ID + * + * 存储实体ID而非引用,避免循环引用,符合ECS数据导向设计。 + */ + public entityId: number | null = null; + /** * 创建组件实例 * diff --git a/packages/core/src/ECS/Core/ReferenceTracker.ts b/packages/core/src/ECS/Core/ReferenceTracker.ts new file mode 100644 index 00000000..0d40d3ea --- /dev/null +++ b/packages/core/src/ECS/Core/ReferenceTracker.ts @@ -0,0 +1,296 @@ +import { Component } from '../Component'; +import type { Entity } from '../Entity'; +import type { IScene } from '../IScene'; +import { createLogger } from '../../Utils/Logger'; + +const logger = createLogger('ReferenceTracker'); + +/** + * Entity引用记录 + */ +export interface EntityRefRecord { + component: WeakRef; + propertyKey: string; +} + +/** + * 全局EntityID到Scene的映射 + * + * 使用全局Map记录每个Entity ID对应的Scene,用于装饰器通过Component.entityId查找Scene。 + */ +const globalEntitySceneMap = new Map>(); + +/** + * 通过Entity ID获取Scene + * + * @param entityId Entity ID + * @returns Scene实例,如果不存在则返回null + */ +export function getSceneByEntityId(entityId: number): IScene | null { + const sceneRef = globalEntitySceneMap.get(entityId); + return sceneRef?.deref() || null; +} + +/** + * Entity引用追踪器 + * + * 追踪Component中对Entity的引用,当Entity被销毁时自动清理所有引用。 + * + * @example + * ```typescript + * const tracker = new ReferenceTracker(); + * tracker.registerReference(targetEntity, component, 'parent'); + * targetEntity.destroy(); // 自动将 component.parent 设为 null + * ``` + */ +export class ReferenceTracker { + /** + * Entity ID -> 引用该Entity的所有组件记录 + */ + private _references: Map> = new Map(); + + /** + * 当前Scene的引用 + */ + private _scene: WeakRef | null = null; + + /** + * 注册Entity引用 + * + * @param entity 被引用的Entity + * @param component 持有引用的Component + * @param propertyKey Component中存储引用的属性名 + */ + public registerReference(entity: Entity, component: Component, propertyKey: string): void { + const entityId = entity.id; + + let records = this._references.get(entityId); + if (!records) { + records = new Set(); + this._references.set(entityId, records); + } + + const existingRecord = this._findRecord(records, component, propertyKey); + if (existingRecord) { + return; + } + + records.add({ + component: new WeakRef(component), + propertyKey + }); + } + + /** + * 注销Entity引用 + * + * @param entity 被引用的Entity + * @param component 持有引用的Component + * @param propertyKey Component中存储引用的属性名 + */ + public unregisterReference(entity: Entity, component: Component, propertyKey: string): void { + const entityId = entity.id; + const records = this._references.get(entityId); + + if (!records) { + return; + } + + const record = this._findRecord(records, component, propertyKey); + if (record) { + records.delete(record); + + if (records.size === 0) { + this._references.delete(entityId); + } + } + } + + /** + * 清理所有指向指定Entity的引用 + * + * 将所有引用该Entity的Component属性设为null。 + * + * @param entityId 被销毁的Entity ID + */ + public clearReferencesTo(entityId: number): void { + const records = this._references.get(entityId); + + if (!records) { + return; + } + + const validRecords: EntityRefRecord[] = []; + + for (const record of records) { + const component = record.component.deref(); + + if (component) { + validRecords.push(record); + } + } + + for (const record of validRecords) { + const component = record.component.deref(); + if (component) { + (component as any)[record.propertyKey] = null; + } + } + + this._references.delete(entityId); + } + + /** + * 清理Component的所有引用注册 + * + * 当Component被移除时调用,清理该Component注册的所有引用。 + * + * @param component 被移除的Component + */ + public clearComponentReferences(component: Component): void { + for (const [entityId, records] of this._references.entries()) { + const toDelete: EntityRefRecord[] = []; + + for (const record of records) { + const comp = record.component.deref(); + if (!comp || comp === component) { + toDelete.push(record); + } + } + + for (const record of toDelete) { + records.delete(record); + } + + if (records.size === 0) { + this._references.delete(entityId); + } + } + } + + /** + * 获取指向指定Entity的所有引用记录 + * + * @param entityId Entity ID + * @returns 引用记录数组(仅包含有效引用) + */ + public getReferencesTo(entityId: number): EntityRefRecord[] { + const records = this._references.get(entityId); + if (!records) { + return []; + } + + const validRecords: EntityRefRecord[] = []; + + for (const record of records) { + const component = record.component.deref(); + if (component) { + validRecords.push(record); + } + } + + return validRecords; + } + + /** + * 清理所有失效的WeakRef引用 + * + * 遍历所有记录,移除已被GC回收的Component引用。 + */ + public cleanup(): void { + const entitiesToDelete: number[] = []; + + for (const [entityId, records] of this._references.entries()) { + const toDelete: EntityRefRecord[] = []; + + for (const record of records) { + if (!record.component.deref()) { + toDelete.push(record); + } + } + + for (const record of toDelete) { + records.delete(record); + } + + if (records.size === 0) { + entitiesToDelete.push(entityId); + } + } + + for (const entityId of entitiesToDelete) { + this._references.delete(entityId); + } + } + + /** + * 设置Scene引用 + * + * @param scene Scene实例 + */ + public setScene(scene: IScene): void { + this._scene = new WeakRef(scene); + } + + /** + * 注册Entity到Scene的映射 + * + * @param entityId Entity ID + * @param scene Scene实例 + */ + public registerEntityScene(entityId: number, scene: IScene): void { + globalEntitySceneMap.set(entityId, new WeakRef(scene)); + } + + /** + * 注销Entity到Scene的映射 + * + * @param entityId Entity ID + */ + public unregisterEntityScene(entityId: number): void { + globalEntitySceneMap.delete(entityId); + } + + /** + * 获取调试信息 + */ + public getDebugInfo(): object { + const info: Record = {}; + + for (const [entityId, records] of this._references.entries()) { + const validRecords = []; + for (const record of records) { + const component = record.component.deref(); + if (component) { + validRecords.push({ + componentId: component.id, + propertyKey: record.propertyKey + }); + } + } + + if (validRecords.length > 0) { + info[`entity_${entityId}`] = validRecords; + } + } + + return info; + } + + /** + * 查找指定的引用记录 + */ + private _findRecord( + records: Set, + component: Component, + propertyKey: string + ): EntityRefRecord | undefined { + for (const record of records) { + const comp = record.component.deref(); + if (comp === component && record.propertyKey === propertyKey) { + return record; + } + } + return undefined; + } +} diff --git a/packages/core/src/ECS/Decorators/EntityRefDecorator.ts b/packages/core/src/ECS/Decorators/EntityRefDecorator.ts new file mode 100644 index 00000000..33bde380 --- /dev/null +++ b/packages/core/src/ECS/Decorators/EntityRefDecorator.ts @@ -0,0 +1,147 @@ +import type { Entity } from '../Entity'; +import type { Component } from '../Component'; +import { getSceneByEntityId } from '../Core/ReferenceTracker'; +import { createLogger } from '../../Utils/Logger'; + +const logger = createLogger('EntityRefDecorator'); + +/** + * EntityRef元数据的Symbol键 + */ +export const ENTITY_REF_METADATA = Symbol('EntityRefMetadata'); + +/** + * EntityRef值存储的Symbol键 + */ +const ENTITY_REF_VALUES = Symbol('EntityRefValues'); + +/** + * EntityRef元数据 + */ +export interface EntityRefMetadata { + properties: Set; +} + +/** + * 获取或创建组件的EntityRef值存储Map + */ +function getValueMap(component: Component): Map { + let map = (component as any)[ENTITY_REF_VALUES]; + if (!map) { + map = new Map(); + (component as any)[ENTITY_REF_VALUES] = map; + } + return map; +} + +/** + * Entity引用装饰器 + * + * 标记Component属性为Entity引用,自动追踪引用关系。 + * 当被引用的Entity销毁时,该属性会自动设为null。 + * + * @example + * ```typescript + * class ParentComponent extends Component { + * @EntityRef() parent: Entity | null = null; + * } + * + * const parent = scene.createEntity('Parent'); + * const child = scene.createEntity('Child'); + * const comp = child.addComponent(new ParentComponent()); + * + * comp.parent = parent; + * parent.destroy(); // comp.parent 自动变为 null + * ``` + */ +export function EntityRef(): PropertyDecorator { + return function (target: any, propertyKey: string | symbol) { + const constructor = target.constructor; + + let metadata: EntityRefMetadata = constructor[ENTITY_REF_METADATA]; + if (!metadata) { + metadata = { + properties: new Set() + }; + constructor[ENTITY_REF_METADATA] = metadata; + } + + const propKeyString = typeof propertyKey === 'symbol' ? propertyKey.toString() : propertyKey; + metadata.properties.add(propKeyString); + + Object.defineProperty(target, propertyKey, { + get: function (this: Component) { + const valueMap = getValueMap(this); + return valueMap.get(propKeyString) || null; + }, + set: function (this: Component, newValue: Entity | null) { + const valueMap = getValueMap(this); + const oldValue = valueMap.get(propKeyString) || null; + + if (oldValue === newValue) { + return; + } + + const scene = this.entityId !== null ? getSceneByEntityId(this.entityId) : null; + + if (!scene || !scene.referenceTracker) { + valueMap.set(propKeyString, newValue); + return; + } + + const tracker = scene.referenceTracker; + + if (oldValue) { + tracker.unregisterReference(oldValue, this, propKeyString); + } + + if (newValue) { + if (newValue.scene !== scene) { + logger.error(`Cannot reference Entity from different Scene. Entity: ${newValue.name}, Scene: ${newValue.scene?.name || 'null'}`); + return; + } + + if (newValue.isDestroyed) { + logger.warn(`Cannot reference destroyed Entity: ${newValue.name}`); + valueMap.set(propKeyString, null); + return; + } + + tracker.registerReference(newValue, this, propKeyString); + } + + valueMap.set(propKeyString, newValue); + }, + enumerable: true, + configurable: true + }); + }; +} + +/** + * 获取Component的EntityRef元数据 + * + * @param component Component实例或Component类 + * @returns EntityRef元数据,如果不存在则返回null + */ +export function getEntityRefMetadata(component: any): EntityRefMetadata | null { + if (!component) { + return null; + } + + const constructor = typeof component === 'function' + ? component + : component.constructor; + + return constructor[ENTITY_REF_METADATA] || null; +} + +/** + * 检查Component是否有EntityRef属性 + * + * @param component Component实例或Component类 + * @returns 如果有EntityRef属性返回true + */ +export function hasEntityRef(component: any): boolean { + return getEntityRefMetadata(component) !== null; +} diff --git a/packages/core/src/ECS/Decorators/index.ts b/packages/core/src/ECS/Decorators/index.ts index 4b6630f9..6ff55d4d 100644 --- a/packages/core/src/ECS/Decorators/index.ts +++ b/packages/core/src/ECS/Decorators/index.ts @@ -10,4 +10,13 @@ export { SYSTEM_TYPE_NAME } from './TypeDecorators'; -export type { SystemMetadata } from './TypeDecorators'; \ No newline at end of file +export type { SystemMetadata } from './TypeDecorators'; + +export { + EntityRef, + getEntityRefMetadata, + hasEntityRef, + ENTITY_REF_METADATA +} from './EntityRefDecorator'; + +export type { EntityRefMetadata } from './EntityRefDecorator'; \ No newline at end of file diff --git a/packages/core/src/ECS/Entity.ts b/packages/core/src/ECS/Entity.ts index 41c5a69c..6d8736d6 100644 --- a/packages/core/src/ECS/Entity.ts +++ b/packages/core/src/ECS/Entity.ts @@ -409,6 +409,10 @@ export class Entity { this.scene.componentStorageManager.addComponent(this.id, component); + component.entityId = this.id; + if (this.scene.referenceTracker) { + this.scene.referenceTracker.registerEntityScene(this.id, this.scene); + } component.onAddedToEntity(); if (Entity.eventBus) { @@ -538,10 +542,16 @@ export class Entity { this.scene.componentStorageManager.removeComponent(this.id, componentType); } + if (this.scene?.referenceTracker) { + this.scene.referenceTracker.clearComponentReferences(component); + } + if (component.onRemovedFromEntity) { component.onRemovedFromEntity(); } + component.entityId = null; + if (Entity.eventBus) { Entity.eventBus.emitComponentRemoved({ timestamp: Date.now(), @@ -867,6 +877,11 @@ export class Entity { this._isDestroyed = true; + if (this.scene && this.scene.referenceTracker) { + this.scene.referenceTracker.clearReferencesTo(this.id); + this.scene.referenceTracker.unregisterEntityScene(this.id); + } + const childrenToDestroy = [...this._children]; for (const child of childrenToDestroy) { child.destroy(); diff --git a/packages/core/src/ECS/IScene.ts b/packages/core/src/ECS/IScene.ts index 459d4e76..f1f90a15 100644 --- a/packages/core/src/ECS/IScene.ts +++ b/packages/core/src/ECS/IScene.ts @@ -5,6 +5,7 @@ import { EntitySystem } from './Systems/EntitySystem'; import { ComponentStorageManager } from './Core/ComponentStorage'; import { QuerySystem } from './Core/QuerySystem'; import { TypeSafeEventSystem } from './Core/EventSystem'; +import type { ReferenceTracker } from './Core/ReferenceTracker'; /** * 场景接口定义 @@ -61,6 +62,11 @@ export interface IScene { */ readonly eventSystem: TypeSafeEventSystem; + /** + * 引用追踪器 + */ + readonly referenceTracker: ReferenceTracker; + /** * 获取系统列表 */ diff --git a/packages/core/src/ECS/Scene.ts b/packages/core/src/ECS/Scene.ts index 54ae0e2b..5f22110e 100644 --- a/packages/core/src/ECS/Scene.ts +++ b/packages/core/src/ECS/Scene.ts @@ -6,6 +6,7 @@ import { ComponentStorageManager, ComponentRegistry } from './Core/ComponentStor import { QuerySystem } from './Core/QuerySystem'; import { TypeSafeEventSystem } from './Core/EventSystem'; import { EventBus } from './Core/EventBus'; +import { ReferenceTracker } from './Core/ReferenceTracker'; import { IScene, ISceneConfig } from './IScene'; import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from "./Decorators"; import { TypedQueryBuilder } from './Core/Query/TypedQuery'; @@ -76,6 +77,13 @@ export class Scene implements IScene { */ public readonly eventSystem: TypeSafeEventSystem; + /** + * 引用追踪器 + * + * 追踪Component中对Entity的引用,当Entity销毁时自动清理引用。 + */ + public readonly referenceTracker: ReferenceTracker; + /** * 服务容器 * @@ -171,6 +179,7 @@ export class Scene implements IScene { this.componentStorageManager = new ComponentStorageManager(); this.querySystem = new QuerySystem(); this.eventSystem = new TypeSafeEventSystem(); + this.referenceTracker = new ReferenceTracker(); this._services = new ServiceContainer(); this.logger = createLogger('Scene'); diff --git a/packages/core/src/ECS/index.ts b/packages/core/src/ECS/index.ts index 97541d0e..deb63b75 100644 --- a/packages/core/src/ECS/index.ts +++ b/packages/core/src/ECS/index.ts @@ -13,4 +13,6 @@ export * from './Core/Events'; export * from './Core/Query'; export * from './Core/Storage'; export * from './Core/StorageDecorators'; -export * from './Serialization'; \ No newline at end of file +export * from './Serialization'; +export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker'; +export type { EntityRefRecord } from './Core/ReferenceTracker'; \ No newline at end of file diff --git a/packages/core/src/Types/index.ts b/packages/core/src/Types/index.ts index 7ee30e7c..ff417174 100644 --- a/packages/core/src/Types/index.ts +++ b/packages/core/src/Types/index.ts @@ -16,7 +16,7 @@ export interface IComponent { /** 组件唯一标识符 */ readonly id: number; /** 组件所属的实体ID */ - entityId?: string | number; + entityId: number | null; /** 组件添加到实体时的回调 */ onAddedToEntity(): void; diff --git a/packages/core/tests/ECS/Core/ReferenceTracker.test.ts b/packages/core/tests/ECS/Core/ReferenceTracker.test.ts new file mode 100644 index 00000000..75b4413a --- /dev/null +++ b/packages/core/tests/ECS/Core/ReferenceTracker.test.ts @@ -0,0 +1,254 @@ +import { describe, test, expect, beforeEach } from '@jest/globals'; +import { ReferenceTracker } from '../../../src/ECS/Core/ReferenceTracker'; +import { Component } from '../../../src/ECS/Component'; +import { Entity } from '../../../src/ECS/Entity'; +import { Scene } from '../../../src/ECS/Scene'; + +class TestComponent extends Component { + public target: Entity | null = null; +} + +describe('ReferenceTracker', () => { + let tracker: ReferenceTracker; + let scene: Scene; + let entity1: Entity; + let entity2: Entity; + let component: TestComponent; + + beforeEach(() => { + tracker = new ReferenceTracker(); + scene = new Scene(); + entity1 = scene.createEntity('Entity1'); + entity2 = scene.createEntity('Entity2'); + component = new TestComponent(); + entity1.addComponent(component); + }); + + describe('registerReference', () => { + test('应该成功注册Entity引用', () => { + tracker.registerReference(entity2, component, 'target'); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(1); + expect(refs[0].component.deref()).toBe(component); + expect(refs[0].propertyKey).toBe('target'); + }); + + test('应该避免重复注册相同引用', () => { + tracker.registerReference(entity2, component, 'target'); + tracker.registerReference(entity2, component, 'target'); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(1); + }); + + test('应该支持多个Component引用同一Entity', () => { + const component2 = new TestComponent(); + const entity3 = scene.createEntity('Entity3'); + entity3.addComponent(component2); + + tracker.registerReference(entity2, component, 'target'); + tracker.registerReference(entity2, component2, 'target'); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(2); + }); + + test('应该支持同一Component引用多个属性', () => { + tracker.registerReference(entity2, component, 'target'); + tracker.registerReference(entity2, component, 'parent'); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(2); + }); + }); + + describe('unregisterReference', () => { + test('应该成功注销Entity引用', () => { + tracker.registerReference(entity2, component, 'target'); + tracker.unregisterReference(entity2, component, 'target'); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(0); + }); + + test('注销不存在的引用不应报错', () => { + expect(() => { + tracker.unregisterReference(entity2, component, 'target'); + }).not.toThrow(); + }); + + test('应该只注销指定的引用', () => { + const component2 = new TestComponent(); + const entity3 = scene.createEntity('Entity3'); + entity3.addComponent(component2); + + tracker.registerReference(entity2, component, 'target'); + tracker.registerReference(entity2, component2, 'target'); + + tracker.unregisterReference(entity2, component, 'target'); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(1); + expect(refs[0].component.deref()).toBe(component2); + }); + }); + + describe('clearReferencesTo', () => { + test('应该将所有引用设为null', () => { + component.target = entity2; + tracker.registerReference(entity2, component, 'target'); + + tracker.clearReferencesTo(entity2.id); + + expect(component.target).toBeNull(); + }); + + test('应该清理多个Component的引用', () => { + const component2 = new TestComponent(); + const entity3 = scene.createEntity('Entity3'); + entity3.addComponent(component2); + + component.target = entity2; + component2.target = entity2; + + tracker.registerReference(entity2, component, 'target'); + tracker.registerReference(entity2, component2, 'target'); + + tracker.clearReferencesTo(entity2.id); + + expect(component.target).toBeNull(); + expect(component2.target).toBeNull(); + }); + + test('清理不存在的Entity引用不应报错', () => { + expect(() => { + tracker.clearReferencesTo(999); + }).not.toThrow(); + }); + + test('应该移除引用记录', () => { + tracker.registerReference(entity2, component, 'target'); + tracker.clearReferencesTo(entity2.id); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(0); + }); + }); + + describe('clearComponentReferences', () => { + test('应该清理Component的所有引用注册', () => { + tracker.registerReference(entity2, component, 'target'); + + tracker.clearComponentReferences(component); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(0); + }); + + test('应该只清理指定Component的引用', () => { + const component2 = new TestComponent(); + const entity3 = scene.createEntity('Entity3'); + entity3.addComponent(component2); + + tracker.registerReference(entity2, component, 'target'); + tracker.registerReference(entity2, component2, 'target'); + + tracker.clearComponentReferences(component); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(1); + expect(refs[0].component.deref()).toBe(component2); + }); + }); + + describe('getReferencesTo', () => { + test('应该返回空数组当Entity没有引用时', () => { + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toEqual([]); + }); + + test('应该只返回有效的引用记录', () => { + tracker.registerReference(entity2, component, 'target'); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(1); + }); + }); + + describe('cleanup', () => { + test('应该清理失效的WeakRef引用', () => { + let tempComponent: TestComponent | null = new TestComponent(); + const entity3 = scene.createEntity('Entity3'); + entity3.addComponent(tempComponent); + + tracker.registerReference(entity2, tempComponent, 'target'); + + expect(tracker.getReferencesTo(entity2.id)).toHaveLength(1); + + tempComponent = null; + + if (global.gc) { + global.gc(); + } + + tracker.cleanup(); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs.length).toBeLessThanOrEqual(1); + }); + }); + + describe('getDebugInfo', () => { + test('应该返回调试信息', () => { + tracker.registerReference(entity2, component, 'target'); + + const debugInfo = tracker.getDebugInfo(); + + expect(debugInfo).toHaveProperty(`entity_${entity2.id}`); + const entityRefs = (debugInfo as any)[`entity_${entity2.id}`]; + expect(entityRefs).toHaveLength(1); + expect(entityRefs[0]).toMatchObject({ + componentId: component.id, + propertyKey: 'target' + }); + }); + + test('应该只包含有效的引用', () => { + tracker.registerReference(entity2, component, 'target'); + + const debugInfo = tracker.getDebugInfo(); + expect(Object.keys(debugInfo)).toHaveLength(1); + }); + }); + + describe('边界情况', () => { + test('应该处理Component被GC回收的情况', () => { + tracker.registerReference(entity2, component, 'target'); + + tracker.cleanup(); + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs.length).toBeGreaterThanOrEqual(0); + }); + + test('应该支持大量引用', () => { + const components: TestComponent[] = []; + for (let i = 0; i < 1000; i++) { + const comp = new TestComponent(); + const ent = scene.createEntity(`Entity${i}`); + ent.addComponent(comp); + components.push(comp); + tracker.registerReference(entity2, comp, 'target'); + } + + const refs = tracker.getReferencesTo(entity2.id); + expect(refs).toHaveLength(1000); + + tracker.clearReferencesTo(entity2.id); + + const refsAfter = tracker.getReferencesTo(entity2.id); + expect(refsAfter).toHaveLength(0); + }); + }); +}); diff --git a/packages/core/tests/ECS/EntityRefIntegration.test.ts b/packages/core/tests/ECS/EntityRefIntegration.test.ts new file mode 100644 index 00000000..3d14eac1 --- /dev/null +++ b/packages/core/tests/ECS/EntityRefIntegration.test.ts @@ -0,0 +1,274 @@ +import { describe, test, expect, beforeEach } from '@jest/globals'; +import { Scene } from '../../src/ECS/Scene'; +import { Component } from '../../src/ECS/Component'; +import { Entity } from '../../src/ECS/Entity'; +import { EntityRef, ECSComponent } from '../../src/ECS/Decorators'; + +@ECSComponent('ParentRef') +class ParentComponent extends Component { + @EntityRef() parent: Entity | null = null; +} + +@ECSComponent('TargetRef') +class TargetComponent extends Component { + @EntityRef() target: Entity | null = null; + @EntityRef() ally: Entity | null = null; +} + +describe('EntityRef Integration Tests', () => { + let scene: Scene; + + beforeEach(() => { + scene = new Scene({ name: 'TestScene' }); + }); + + describe('基础功能', () => { + test('应该支持EntityRef装饰器', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + const comp = entity1.addComponent(new ParentComponent()); + + comp.parent = entity2; + + expect(comp.parent).toBe(entity2); + }); + + test('Entity销毁时应该自动清理所有引用', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + + const comp1 = child1.addComponent(new ParentComponent()); + const comp2 = child2.addComponent(new ParentComponent()); + + comp1.parent = parent; + comp2.parent = parent; + + expect(comp1.parent).toBe(parent); + expect(comp2.parent).toBe(parent); + + parent.destroy(); + + expect(comp1.parent).toBeNull(); + expect(comp2.parent).toBeNull(); + }); + + test('修改引用应该更新ReferenceTracker', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + const entity3 = scene.createEntity('Entity3'); + const comp = entity1.addComponent(new ParentComponent()); + + comp.parent = entity2; + expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); + + comp.parent = entity3; + expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(0); + expect(scene.referenceTracker.getReferencesTo(entity3.id)).toHaveLength(1); + }); + + test('设置为null应该注销引用', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + const comp = entity1.addComponent(new ParentComponent()); + + comp.parent = entity2; + expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); + + comp.parent = null; + expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(0); + }); + }); + + describe('Component生命周期', () => { + test('移除Component应该清理其所有引用注册', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + const comp = entity1.addComponent(new ParentComponent()); + + comp.parent = entity2; + expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); + + entity1.removeComponent(comp); + expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(0); + }); + + test('移除Component应该清除entityId引用', () => { + const entity1 = scene.createEntity('Entity1'); + const comp = entity1.addComponent(new ParentComponent()); + + expect(comp.entityId).toBe(entity1.id); + + entity1.removeComponent(comp); + expect(comp.entityId).toBeNull(); + }); + }); + + describe('多属性引用', () => { + test('应该支持同一Component的多个EntityRef属性', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + const entity3 = scene.createEntity('Entity3'); + const comp = entity1.addComponent(new TargetComponent()); + + comp.target = entity2; + comp.ally = entity3; + + expect(comp.target).toBe(entity2); + expect(comp.ally).toBe(entity3); + + entity2.destroy(); + + expect(comp.target).toBeNull(); + expect(comp.ally).toBe(entity3); + + entity3.destroy(); + + expect(comp.ally).toBeNull(); + }); + }); + + describe('边界情况', () => { + test('跨Scene引用应该失败', () => { + const scene2 = new Scene({ name: 'TestScene2' }); + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene2.createEntity('Entity2'); + const comp = entity1.addComponent(new ParentComponent()); + + comp.parent = entity2; + + expect(comp.parent).toBeNull(); + }); + + test('引用已销毁的Entity应该失败', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + const comp = entity1.addComponent(new ParentComponent()); + + entity2.destroy(); + comp.parent = entity2; + + expect(comp.parent).toBeNull(); + }); + + test('重复设置相同值不应重复注册', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + const comp = entity1.addComponent(new ParentComponent()); + + comp.parent = entity2; + comp.parent = entity2; + comp.parent = entity2; + + expect(scene.referenceTracker.getReferencesTo(entity2.id)).toHaveLength(1); + }); + + test('循环引用应该正常工作', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + const comp1 = entity1.addComponent(new ParentComponent()); + const comp2 = entity2.addComponent(new ParentComponent()); + + comp1.parent = entity2; + comp2.parent = entity1; + + expect(comp1.parent).toBe(entity2); + expect(comp2.parent).toBe(entity1); + + entity1.destroy(); + + expect(comp2.parent).toBeNull(); + expect(entity2.isDestroyed).toBe(false); + }); + }); + + describe('复杂场景', () => { + test('父子实体销毁应该正确清理引用', () => { + const parent = scene.createEntity('Parent'); + const child1 = scene.createEntity('Child1'); + const child2 = scene.createEntity('Child2'); + const observer = scene.createEntity('Observer'); + + parent.addChild(child1); + parent.addChild(child2); + + const observerComp = observer.addComponent(new TargetComponent()); + observerComp.target = parent; + observerComp.ally = child1; + + expect(observerComp.target).toBe(parent); + expect(observerComp.ally).toBe(child1); + + parent.destroy(); + + expect(observerComp.target).toBeNull(); + expect(observerComp.ally).toBeNull(); + expect(child1.isDestroyed).toBe(true); + expect(child2.isDestroyed).toBe(true); + }); + + test('大量引用场景', () => { + const target = scene.createEntity('Target'); + const entities: Entity[] = []; + const components: ParentComponent[] = []; + + for (let i = 0; i < 100; i++) { + const entity = scene.createEntity(`Entity${i}`); + const comp = entity.addComponent(new ParentComponent()); + comp.parent = target; + entities.push(entity); + components.push(comp); + } + + expect(scene.referenceTracker.getReferencesTo(target.id)).toHaveLength(100); + + target.destroy(); + + for (const comp of components) { + expect(comp.parent).toBeNull(); + } + + expect(scene.referenceTracker.getReferencesTo(target.id)).toHaveLength(0); + }); + + test('批量销毁后引用应全部清理', () => { + const entities: Entity[] = []; + const components: TargetComponent[] = []; + + for (let i = 0; i < 50; i++) { + entities.push(scene.createEntity(`Entity${i}`)); + } + + for (let i = 0; i < 50; i++) { + const comp = entities[i].addComponent(new TargetComponent()); + if (i > 0) { + comp.target = entities[i - 1]; + } + if (i < 49) { + comp.ally = entities[i + 1]; + } + components.push(comp); + } + + scene.destroyAllEntities(); + + for (const comp of components) { + expect(comp.target).toBeNull(); + expect(comp.ally).toBeNull(); + } + }); + }); + + describe('调试功能', () => { + test('getDebugInfo应该返回引用信息', () => { + const entity1 = scene.createEntity('Entity1'); + const entity2 = scene.createEntity('Entity2'); + const comp = entity1.addComponent(new ParentComponent()); + + comp.parent = entity2; + + const debugInfo = scene.referenceTracker.getDebugInfo(); + expect(debugInfo).toHaveProperty(`entity_${entity2.id}`); + }); + }); +}); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index d9789258..5874ebdd 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { - "target": "ES2020", + "target": "ES2021", "module": "ES2020", "moduleResolution": "node", "allowImportingTsExtensions": false, - "lib": ["ES2020", "DOM"], + "lib": ["ES2021", "DOM"], "outDir": "./bin", "rootDir": "./src", "strict": true,