组件引用完整性,升级到es2021使用weakref
This commit is contained in:
@@ -45,6 +45,13 @@ export abstract class Component implements IComponent {
|
|||||||
*/
|
*/
|
||||||
public readonly id: number;
|
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
|
SYSTEM_TYPE_NAME
|
||||||
} from './TypeDecorators';
|
} 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);
|
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();
|
component.onAddedToEntity();
|
||||||
|
|
||||||
if (Entity.eventBus) {
|
if (Entity.eventBus) {
|
||||||
@@ -538,10 +542,16 @@ export class Entity {
|
|||||||
this.scene.componentStorageManager.removeComponent(this.id, componentType);
|
this.scene.componentStorageManager.removeComponent(this.id, componentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.scene?.referenceTracker) {
|
||||||
|
this.scene.referenceTracker.clearComponentReferences(component);
|
||||||
|
}
|
||||||
|
|
||||||
if (component.onRemovedFromEntity) {
|
if (component.onRemovedFromEntity) {
|
||||||
component.onRemovedFromEntity();
|
component.onRemovedFromEntity();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component.entityId = null;
|
||||||
|
|
||||||
if (Entity.eventBus) {
|
if (Entity.eventBus) {
|
||||||
Entity.eventBus.emitComponentRemoved({
|
Entity.eventBus.emitComponentRemoved({
|
||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
@@ -867,6 +877,11 @@ export class Entity {
|
|||||||
|
|
||||||
this._isDestroyed = true;
|
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];
|
const childrenToDestroy = [...this._children];
|
||||||
for (const child of childrenToDestroy) {
|
for (const child of childrenToDestroy) {
|
||||||
child.destroy();
|
child.destroy();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { EntitySystem } from './Systems/EntitySystem';
|
|||||||
import { ComponentStorageManager } from './Core/ComponentStorage';
|
import { ComponentStorageManager } from './Core/ComponentStorage';
|
||||||
import { QuerySystem } from './Core/QuerySystem';
|
import { QuerySystem } from './Core/QuerySystem';
|
||||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||||
|
import type { ReferenceTracker } from './Core/ReferenceTracker';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 场景接口定义
|
* 场景接口定义
|
||||||
@@ -61,6 +62,11 @@ export interface IScene {
|
|||||||
*/
|
*/
|
||||||
readonly eventSystem: TypeSafeEventSystem;
|
readonly eventSystem: TypeSafeEventSystem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 引用追踪器
|
||||||
|
*/
|
||||||
|
readonly referenceTracker: ReferenceTracker;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取系统列表
|
* 获取系统列表
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { ComponentStorageManager, ComponentRegistry } from './Core/ComponentStor
|
|||||||
import { QuerySystem } from './Core/QuerySystem';
|
import { QuerySystem } from './Core/QuerySystem';
|
||||||
import { TypeSafeEventSystem } from './Core/EventSystem';
|
import { TypeSafeEventSystem } from './Core/EventSystem';
|
||||||
import { EventBus } from './Core/EventBus';
|
import { EventBus } from './Core/EventBus';
|
||||||
|
import { ReferenceTracker } from './Core/ReferenceTracker';
|
||||||
import { IScene, ISceneConfig } from './IScene';
|
import { IScene, ISceneConfig } from './IScene';
|
||||||
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from "./Decorators";
|
import { getComponentInstanceTypeName, getSystemInstanceTypeName, getSystemMetadata } from "./Decorators";
|
||||||
import { TypedQueryBuilder } from './Core/Query/TypedQuery';
|
import { TypedQueryBuilder } from './Core/Query/TypedQuery';
|
||||||
@@ -76,6 +77,13 @@ export class Scene implements IScene {
|
|||||||
*/
|
*/
|
||||||
public readonly eventSystem: TypeSafeEventSystem;
|
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.componentStorageManager = new ComponentStorageManager();
|
||||||
this.querySystem = new QuerySystem();
|
this.querySystem = new QuerySystem();
|
||||||
this.eventSystem = new TypeSafeEventSystem();
|
this.eventSystem = new TypeSafeEventSystem();
|
||||||
|
this.referenceTracker = new ReferenceTracker();
|
||||||
this._services = new ServiceContainer();
|
this._services = new ServiceContainer();
|
||||||
this.logger = createLogger('Scene');
|
this.logger = createLogger('Scene');
|
||||||
|
|
||||||
|
|||||||
@@ -13,4 +13,6 @@ export * from './Core/Events';
|
|||||||
export * from './Core/Query';
|
export * from './Core/Query';
|
||||||
export * from './Core/Storage';
|
export * from './Core/Storage';
|
||||||
export * from './Core/StorageDecorators';
|
export * from './Core/StorageDecorators';
|
||||||
export * from './Serialization';
|
export * from './Serialization';
|
||||||
|
export { ReferenceTracker, getSceneByEntityId } from './Core/ReferenceTracker';
|
||||||
|
export type { EntityRefRecord } from './Core/ReferenceTracker';
|
||||||
@@ -16,7 +16,7 @@ export interface IComponent {
|
|||||||
/** 组件唯一标识符 */
|
/** 组件唯一标识符 */
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
/** 组件所属的实体ID */
|
/** 组件所属的实体ID */
|
||||||
entityId?: string | number;
|
entityId: number | null;
|
||||||
|
|
||||||
/** 组件添加到实体时的回调 */
|
/** 组件添加到实体时的回调 */
|
||||||
onAddedToEntity(): void;
|
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": {
|
"compilerOptions": {
|
||||||
"target": "ES2020",
|
"target": "ES2021",
|
||||||
"module": "ES2020",
|
"module": "ES2020",
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"allowImportingTsExtensions": false,
|
"allowImportingTsExtensions": false,
|
||||||
"lib": ["ES2020", "DOM"],
|
"lib": ["ES2021", "DOM"],
|
||||||
"outDir": "./bin",
|
"outDir": "./bin",
|
||||||
"rootDir": "./src",
|
"rootDir": "./src",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user