组件引用完整性,升级到es2021使用weakref
This commit is contained in:
@@ -45,6 +45,13 @@ export abstract class Component implements IComponent {
|
||||
*/
|
||||
public readonly id: number;
|
||||
|
||||
/**
|
||||
* 所属实体ID
|
||||
*
|
||||
* 存储实体ID而非引用,避免循环引用,符合ECS数据导向设计。
|
||||
*/
|
||||
public entityId: number | null = null;
|
||||
|
||||
/**
|
||||
* 创建组件实例
|
||||
*
|
||||
|
||||
296
packages/core/src/ECS/Core/ReferenceTracker.ts
Normal file
296
packages/core/src/ECS/Core/ReferenceTracker.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
147
packages/core/src/ECS/Decorators/EntityRefDecorator.ts
Normal file
147
packages/core/src/ECS/Decorators/EntityRefDecorator.ts
Normal 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;
|
||||
}
|
||||
@@ -10,4 +10,13 @@ export {
|
||||
SYSTEM_TYPE_NAME
|
||||
} from './TypeDecorators';
|
||||
|
||||
export type { SystemMetadata } from './TypeDecorators';
|
||||
export type { SystemMetadata } from './TypeDecorators';
|
||||
|
||||
export {
|
||||
EntityRef,
|
||||
getEntityRefMetadata,
|
||||
hasEntityRef,
|
||||
ENTITY_REF_METADATA
|
||||
} from './EntityRefDecorator';
|
||||
|
||||
export type { EntityRefMetadata } from './EntityRefDecorator';
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 获取系统列表
|
||||
*/
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -13,4 +13,6 @@ export * from './Core/Events';
|
||||
export * from './Core/Query';
|
||||
export * from './Core/Storage';
|
||||
export * from './Core/StorageDecorators';
|
||||
export * from './Serialization';
|
||||
export * from './Serialization';
|
||||
export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker';
|
||||
export type { EntityRefRecord } from './Core/ReferenceTracker';
|
||||
@@ -16,7 +16,7 @@ export interface IComponent {
|
||||
/** 组件唯一标识符 */
|
||||
readonly id: number;
|
||||
/** 组件所属的实体ID */
|
||||
entityId?: string | number;
|
||||
entityId: number | null;
|
||||
|
||||
/** 组件添加到实体时的回调 */
|
||||
onAddedToEntity(): void;
|
||||
|
||||
254
packages/core/tests/ECS/Core/ReferenceTracker.test.ts
Normal file
254
packages/core/tests/ECS/Core/ReferenceTracker.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
274
packages/core/tests/ECS/EntityRefIntegration.test.ts
Normal file
274
packages/core/tests/ECS/EntityRefIntegration.test.ts
Normal 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}`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user