EntityRef 装饰器
框架提供了 @EntityRef 装饰器用于特殊场景下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。
什么时候需要 EntityRef?
Section titled “什么时候需要 EntityRef?”在以下场景中,@EntityRef 可以简化代码:
- 父子关系: 需要在组件中直接访问父实体或子实体
- 复杂关联: 实体之间有多个引用关系
- 频繁访问: 需要在多处访问引用的实体,使用ID查找会有性能开销
@EntityRef 装饰器通过 ReferenceTracker 自动追踪引用关系:
- 当被引用的实体销毁时,所有指向它的
@EntityRef属性自动设为null - 防止跨场景引用(会输出警告并拒绝设置)
- 防止引用已销毁的实体(会输出警告并设为
null) - 使用 WeakRef 避免内存泄漏(自动GC支持)
- 组件移除时自动清理引用注册
import { Component, ECSComponent, EntityRef, Entity } from '@esengine/ecs-framework';
@ECSComponent('Parent')class ParentComponent extends Component { @EntityRef() parent: Entity | null = null;}
// 使用示例const scene = new Scene();const parent = scene.createEntity('Parent');const child = scene.createEntity('Child');
const comp = child.addComponent(new ParentComponent());comp.parent = parent;
console.log(comp.parent); // Entity { name: 'Parent' }
// 当 parent 被销毁时,comp.parent 自动变为 nullparent.destroy();console.log(comp.parent); // null多个引用属性
Section titled “多个引用属性”一个组件可以有多个 @EntityRef 属性:
@ECSComponent('Combat')class CombatComponent extends Component { @EntityRef() target: Entity | null = null;
@EntityRef() ally: Entity | null = null;
@EntityRef() lastAttacker: Entity | null = null;}
// 使用示例const player = scene.createEntity('Player');const enemy = scene.createEntity('Enemy');const npc = scene.createEntity('NPC');
const combat = player.addComponent(new CombatComponent());combat.target = enemy;combat.ally = npc;
// enemy 销毁后,只有 target 变为 null,ally 仍然有效enemy.destroy();console.log(combat.target); // nullconsole.log(combat.ally); // Entity { name: 'NPC' }@EntityRef 提供了多重安全检查:
const scene1 = new Scene();const scene2 = new Scene();
const entity1 = scene1.createEntity('Entity1');const entity2 = scene2.createEntity('Entity2');
const comp = entity1.addComponent(new ParentComponent());
// 跨场景引用会失败comp.parent = entity2; // 输出错误日志,comp.parent 为 nullconsole.log(comp.parent); // null
// 引用已销毁的实体会失败const entity3 = scene1.createEntity('Entity3');entity3.destroy();comp.parent = entity3; // 输出警告日志,comp.parent 为 nullconsole.log(comp.parent); // null@EntityRef 使用以下机制实现自动引用追踪:
- ReferenceTracker: Scene 持有一个引用追踪器,记录所有实体引用关系
- WeakRef: 使用弱引用存储组件,避免循环引用导致内存泄漏
- 属性拦截: 通过
Object.defineProperty拦截 getter/setter - 自动清理: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
// 简化的实现原理class ReferenceTracker { // entityId -> 引用该实体的所有组件记录 private _references: Map<number, Set<{ component: WeakRef<Component>, propertyKey: string }>>;
// 实体销毁时调用 clearReferencesTo(entityId: number): void { const records = this._references.get(entityId); if (records) { for (const record of records) { const component = record.component.deref(); if (component) { // 将组件的引用属性设为 null (component as any)[record.propertyKey] = null; } } this._references.delete(entityId); } }}@EntityRef 会带来一些性能开销:
- 写入开销: 每次设置引用时需要更新 ReferenceTracker
- 内存开销: ReferenceTracker 需要维护引用映射表
- 销毁开销: 实体销毁时需要遍历所有引用并清理
对于大多数场景,这些开销是可以接受的。但如果有大量实体和频繁的引用变更,存储ID可能更高效。
ReferenceTracker 提供了调试接口:
// 查看某个实体被哪些组件引用const references = scene.referenceTracker.getReferencesTo(entity.id);console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`);
// 获取完整的调试信息const debugInfo = scene.referenceTracker.getDebugInfo();console.log(debugInfo);与存储 ID 方式的对比
Section titled “与存储 ID 方式的对比”存储 ID(推荐大多数情况)
Section titled “存储 ID(推荐大多数情况)”@ECSComponent('Follower')class Follower extends Component { targetId: number | null = null;}
// 在 System 中查找class FollowerSystem extends EntitySystem { process(entities: readonly Entity[]): void { for (const entity of entities) { const follower = entity.getComponent(Follower)!; const target = entity.scene?.findEntityById(follower.targetId); if (target) { // 跟随逻辑 } } }}使用 EntityRef(适合复杂关联)
Section titled “使用 EntityRef(适合复杂关联)”@ECSComponent('Transform')class Transform extends Component { @EntityRef() parent: Entity | null = null;
position: { x: number, y: number } = { x: 0, y: 0 };
// 可以直接访问父实体的组件 getWorldPosition(): { x: number, y: number } { if (!this.parent) { return { ...this.position }; }
const parentTransform = this.parent.getComponent(Transform); if (parentTransform) { const parentPos = parentTransform.getWorldPosition(); return { x: parentPos.x + this.position.x, y: parentPos.y + this.position.y }; }
return { ...this.position }; }}| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 存储 ID | 大多数情况 | 简单、无额外开销 | 需要在 System 中查找 |
| @EntityRef | 父子关系、复杂关联 | 自动清理、代码简洁 | 有性能开销 |
- 推荐做法: 大部分情况使用存储ID + System查找的方式
- EntityRef 适用场景: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
- 核心优势: 自动清理、防止悬空引用、代码更简洁
- 注意事项: 有性能开销,不适合大量动态引用的场景