Skip to content

组件系统

在 ECS 架构中,组件(Component)是数据和行为的载体。组件定义了实体具有的属性和功能,是 ECS 架构的核心构建块。

基本概念

组件是继承自 Component 抽象基类的具体类,用于:

  • 存储实体的数据(如位置、速度、健康值等)
  • 定义与数据相关的行为方法
  • 提供生命周期回调钩子
  • 支持序列化和调试

创建组件

基础组件定义

typescript
import { Component, ECSComponent } from '@esengine/ecs-framework';

@ECSComponent('Position')
class Position extends Component {
  x: number = 0;
  y: number = 0;

  constructor(x: number = 0, y: number = 0) {
    super();
    this.x = x;
    this.y = y;
  }
}

@ECSComponent('Health')
class Health extends Component {
  current: number;
  max: number;

  constructor(max: number = 100) {
    super();
    this.max = max;
    this.current = max;
  }

  // 组件可以包含行为方法
  takeDamage(damage: number): void {
    this.current = Math.max(0, this.current - damage);
  }

  heal(amount: number): void {
    this.current = Math.min(this.max, this.current + amount);
  }

  isDead(): boolean {
    return this.current <= 0;
  }
}

组件装饰器

必须使用 @ECSComponent 装饰器,这确保了:

  • 组件在代码混淆后仍能正确识别
  • 提供稳定的类型名称用于序列化和调试
  • 框架能正确管理组件注册
typescript
// 正确的用法
@ECSComponent('Velocity')
class Velocity extends Component {
  dx: number = 0;
  dy: number = 0;
}

// 错误的用法 - 没有装饰器
class BadComponent extends Component {
  // 这样定义的组件可能在生产环境出现问题
}

组件生命周期

组件提供了生命周期钩子,可以重写来执行特定的逻辑:

typescript
@ECSComponent('ExampleComponent')
class ExampleComponent extends Component {
  private resource: SomeResource | null = null;

  /**
   * 组件被添加到实体时调用
   * 用于初始化资源、建立引用等
   */
  onAddedToEntity(): void {
    console.log(`组件 ${this.constructor.name} 已添加,实体ID: ${this.entityId}`);
    this.resource = new SomeResource();
  }

  /**
   * 组件从实体移除时调用
   * 用于清理资源、断开引用等
   */
  onRemovedFromEntity(): void {
    console.log(`组件 ${this.constructor.name} 已移除`);
    if (this.resource) {
      this.resource.cleanup();
      this.resource = null;
    }
  }
}

组件与实体的关系

组件存储了所属实体的ID (entityId),而不是直接引用实体对象。这是ECS数据导向设计的体现,避免了循环引用。

在实际使用中,应该在 System 中处理实体和组件的交互,而不是在组件内部:

typescript
@ECSComponent('Health')
class Health extends Component {
  current: number;
  max: number;

  constructor(max: number = 100) {
    super();
    this.max = max;
    this.current = max;
  }

  isDead(): boolean {
    return this.current <= 0;
  }
}

@ECSComponent('Damage')
class Damage extends Component {
  value: number;

  constructor(value: number) {
    super();
    this.value = value;
  }
}

// 推荐:在 System 中处理逻辑
class DamageSystem extends EntitySystem {
  constructor() {
    super(new Matcher().all(Health, Damage));
  }

  process(entities: readonly Entity[]): void {
    for (const entity of entities) {
      const health = entity.getComponent(Health)!;
      const damage = entity.getComponent(Damage)!;

      health.current -= damage.value;

      if (health.isDead()) {
        entity.destroy();
      }

      // 应用伤害后移除 Damage 组件
      entity.removeComponent(damage);
    }
  }
}

组件属性

每个组件都有一些内置属性:

typescript
@ECSComponent('ExampleComponent')
class ExampleComponent extends Component {
  someData: string = "example";

  onAddedToEntity(): void {
    console.log(`组件ID: ${this.id}`);           // 唯一的组件ID
    console.log(`所属实体ID: ${this.entityId}`); // 所属实体的ID
  }
}

如果需要访问实体对象,应该在 System 中进行:

typescript
class ExampleSystem extends EntitySystem {
  constructor() {
    super(new Matcher().all(ExampleComponent));
  }

  process(entities: readonly Entity[]): void {
    for (const entity of entities) {
      const comp = entity.getComponent(ExampleComponent)!;
      console.log(`实体名称: ${entity.name}`);
      console.log(`组件数据: ${comp.someData}`);
    }
  }
}

复杂组件示例

状态机组件

typescript
enum EntityState {
  Idle,
  Moving,
  Attacking,
  Dead
}

@ECSComponent('StateMachine')
class StateMachine extends Component {
  private _currentState: EntityState = EntityState.Idle;
  private _previousState: EntityState = EntityState.Idle;
  private _stateTimer: number = 0;

  get currentState(): EntityState {
    return this._currentState;
  }

  get previousState(): EntityState {
    return this._previousState;
  }

  get stateTimer(): number {
    return this._stateTimer;
  }

  changeState(newState: EntityState): void {
    if (this._currentState !== newState) {
      this._previousState = this._currentState;
      this._currentState = newState;
      this._stateTimer = 0;
    }
  }

  updateTimer(deltaTime: number): void {
    this._stateTimer += deltaTime;
  }

  isInState(state: EntityState): boolean {
    return this._currentState === state;
  }
}

配置数据组件

typescript
interface WeaponData {
  damage: number;
  range: number;
  fireRate: number;
  ammo: number;
}

@ECSComponent('WeaponConfig')
class WeaponConfig extends Component {
  data: WeaponData;

  constructor(weaponData: WeaponData) {
    super();
    this.data = { ...weaponData }; // 深拷贝避免共享引用
  }

  // 提供便捷的访问方法
  getDamage(): number {
    return this.data.damage;
  }

  canFire(): boolean {
    return this.data.ammo > 0;
  }

  consumeAmmo(): boolean {
    if (this.data.ammo > 0) {
      this.data.ammo--;
      return true;
    }
    return false;
  }
}

最佳实践

1. 保持组件简单

typescript
// 好的组件设计 - 单一职责
@ECSComponent('Position')
class Position extends Component {
  x: number = 0;
  y: number = 0;
}

@ECSComponent('Velocity')
class Velocity extends Component {
  dx: number = 0;
  dy: number = 0;
}

// 避免的组件设计 - 职责过多
@ECSComponent('GameObject')
class GameObject extends Component {
  x: number;
  y: number;
  dx: number;
  dy: number;
  health: number;
  damage: number;
  sprite: string;
  // 太多不相关的属性
}

2. 使用构造函数初始化

typescript
@ECSComponent('Transform')
class Transform extends Component {
  x: number;
  y: number;
  rotation: number;
  scale: number;

  constructor(x = 0, y = 0, rotation = 0, scale = 1) {
    super();
    this.x = x;
    this.y = y;
    this.rotation = rotation;
    this.scale = scale;
  }
}

3. 明确的类型定义

typescript
interface InventoryItem {
  id: string;
  name: string;
  quantity: number;
  type: 'weapon' | 'consumable' | 'misc';
}

@ECSComponent('Inventory')
class Inventory extends Component {
  items: InventoryItem[] = [];
  maxSlots: number;

  constructor(maxSlots: number = 20) {
    super();
    this.maxSlots = maxSlots;
  }

  addItem(item: InventoryItem): boolean {
    if (this.items.length < this.maxSlots) {
      this.items.push(item);
      return true;
    }
    return false;
  }

  removeItem(itemId: string): InventoryItem | null {
    const index = this.items.findIndex(item => item.id === itemId);
    if (index !== -1) {
      return this.items.splice(index, 1)[0];
    }
    return null;
  }
}

4. 引用其他实体

当组件需要关联其他实体时(如父子关系、跟随目标等),推荐方式是存储实体ID,然后在 System 中查找:

typescript
@ECSComponent('Follower')
class Follower extends Component {
  targetId: number;
  followDistance: number = 50;

  constructor(targetId: number) {
    super();
    this.targetId = targetId;
  }
}

// 在 System 中查找目标实体并处理逻辑
class FollowerSystem extends EntitySystem {
  constructor() {
    super(new Matcher().all(Follower, Position));
  }

  process(entities: readonly Entity[]): void {
    for (const entity of entities) {
      const follower = entity.getComponent(Follower)!;
      const position = entity.getComponent(Position)!;

      // 通过场景查找目标实体
      const target = entity.scene?.findEntityById(follower.targetId);
      if (target) {
        const targetPos = target.getComponent(Position);
        if (targetPos) {
          // 跟随逻辑
          const dx = targetPos.x - position.x;
          const dy = targetPos.y - position.y;
          const distance = Math.sqrt(dx * dx + dy * dy);

          if (distance > follower.followDistance) {
            // 移动靠近目标
          }
        }
      }
    }
  }
}

这种方式的优势:

  • 组件保持简单,只存储基本数据类型
  • 符合数据导向设计
  • 在 System 中统一处理查找和逻辑
  • 易于理解和维护

避免在组件中直接存储实体引用

typescript
// 错误示范:直接存储实体引用
@ECSComponent('BadFollower')
class BadFollower extends Component {
  target: Entity; // 实体销毁后仍持有引用,可能导致内存泄漏
}

高级特性

EntityRef 装饰器 - 自动引用追踪

框架提供了 @EntityRef 装饰器用于特殊场景下安全地存储实体引用。这是一个高级特性,一般情况下推荐使用存储ID的方式。

什么时候需要 EntityRef?

在以下场景中,@EntityRef 可以简化代码:

  1. 父子关系: 需要在组件中直接访问父实体或子实体
  2. 复杂关联: 实体之间有多个引用关系
  3. 频繁访问: 需要在多处访问引用的实体,使用ID查找会有性能开销

核心特性

@EntityRef 装饰器通过 ReferenceTracker 自动追踪引用关系:

  • 当被引用的实体销毁时,所有指向它的 @EntityRef 属性自动设为 null
  • 防止跨场景引用(会输出警告并拒绝设置)
  • 防止引用已销毁的实体(会输出警告并设为 null)
  • 使用 WeakRef 避免内存泄漏(自动GC支持)
  • 组件移除时自动清理引用注册

基本用法

typescript
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 自动变为 null
parent.destroy();
console.log(comp.parent); // null

多个引用属性

一个组件可以有多个 @EntityRef 属性:

typescript
@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); // null
console.log(combat.ally);   // Entity { name: 'NPC' }

安全检查

@EntityRef 提供了多重安全检查:

typescript
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 为 null
console.log(comp.parent); // null

// 引用已销毁的实体会失败
const entity3 = scene1.createEntity('Entity3');
entity3.destroy();
comp.parent = entity3; // 输出警告日志,comp.parent 为 null
console.log(comp.parent); // null

实现原理

@EntityRef 使用以下机制实现自动引用追踪:

  1. ReferenceTracker: Scene 持有一个引用追踪器,记录所有实体引用关系
  2. WeakRef: 使用弱引用存储组件,避免循环引用导致内存泄漏
  3. 属性拦截: 通过 Object.defineProperty 拦截 getter/setter
  4. 自动清理: 实体销毁时,ReferenceTracker 遍历所有引用并设为 null
typescript
// 简化的实现原理
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可能更高效。

最佳实践

typescript
// 推荐:适合使用 @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 };
  }
}

// 不推荐:不适合使用 @EntityRef 的场景 - 大量动态目标
@ECSComponent('AITarget')
class AITarget extends Component {
  @EntityRef()
  target: Entity | null = null; // 如果目标频繁变化,用ID更好

  updateCooldown: number = 0;
}

// 推荐:这种场景用ID更好
@ECSComponent('AITarget')
class AITargetBetter extends Component {
  targetId: number | null = null; // 存储ID
  updateCooldown: number = 0;
}

调试支持

ReferenceTracker 提供了调试接口:

typescript
// 查看某个实体被哪些组件引用
const references = scene.referenceTracker.getReferencesTo(entity.id);
console.log(`实体 ${entity.name} 被 ${references.length} 个组件引用`);

// 获取完整的调试信息
const debugInfo = scene.referenceTracker.getDebugInfo();
console.log(debugInfo);

总结

  • 推荐做法: 大部分情况使用存储ID + System查找的方式
  • EntityRef 适用场景: 父子关系、复杂关联、组件内需要直接访问引用实体的场景
  • 核心优势: 自动清理、防止悬空引用、代码更简洁
  • 注意事项: 有性能开销,不适合大量动态引用的场景

组件是 ECS 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。

Released under the MIT License.