组件引用完整性,升级到es2021使用weakref

This commit is contained in:
YHH
2025-10-10 23:38:48 +08:00
parent 536871d09b
commit 7850fc610c
12 changed files with 1024 additions and 5 deletions

View File

@@ -45,6 +45,13 @@ export abstract class Component implements IComponent {
*/
public readonly id: number;
/**
* 所属实体ID
*
* 存储实体ID而非引用避免循环引用符合ECS数据导向设计。
*/
public entityId: number | null = null;
/**
* 创建组件实例
*

View File

@@ -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<Component>;
propertyKey: string;
}
/**
* 全局EntityID到Scene的映射
*
* 使用全局Map记录每个Entity ID对应的Scene用于装饰器通过Component.entityId查找Scene。
*/
const globalEntitySceneMap = new Map<number, WeakRef<IScene>>();
/**
* 通过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<number, Set<EntityRefRecord>> = new Map();
/**
* 当前Scene的引用
*/
private _scene: WeakRef<IScene> | 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<string, any> = {};
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<EntityRefRecord>,
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;
}
}

View File

@@ -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<string>;
}
/**
* 获取或创建组件的EntityRef值存储Map
*/
function getValueMap(component: Component): Map<string, Entity | null> {
let map = (component as any)[ENTITY_REF_VALUES];
if (!map) {
map = new Map<string, Entity | null>();
(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;
}

View File

@@ -11,3 +11,12 @@ export {
} from './TypeDecorators';
export type { SystemMetadata } from './TypeDecorators';
export {
EntityRef,
getEntityRefMetadata,
hasEntityRef,
ENTITY_REF_METADATA
} from './EntityRefDecorator';
export type { EntityRefMetadata } from './EntityRefDecorator';

View File

@@ -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();

View File

@@ -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;
/**
* 获取系统列表
*/

View File

@@ -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');

View File

@@ -14,3 +14,5 @@ export * from './Core/Query';
export * from './Core/Storage';
export * from './Core/StorageDecorators';
export * from './Serialization';
export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker';
export type { EntityRefRecord } from './Core/ReferenceTracker';

View File

@@ -16,7 +16,7 @@ export interface IComponent {
/** 组件唯一标识符 */
readonly id: number;
/** 组件所属的实体ID */
entityId?: string | number;
entityId: number | null;
/** 组件添加到实体时的回调 */
onAddedToEntity(): void;

View File

@@ -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);
});
});
});

View File

@@ -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}`);
});
});
});

View File

@@ -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,