# 组件系统 在 ECS 架构中,组件(Component)是数据和行为的载体。组件定义了实体具有的属性和功能,是 ECS 架构的核心构建块。 ## 基本概念 组件是继承自 `Component` 抽象基类的具体类,用于: - 存储实体的数据(如位置、速度、健康值等) - 定义与数据相关的行为方法 - 提供生命周期回调钩子 - 支持序列化和调试 ## 创建组件 ### 基础组件定义 ```typescript import { Component, ECSComponent } from '@esengine/esengine'; @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 装饰器 `@ECSComponent` 是组件类必须使用的装饰器,它为组件提供了类型标识和元数据管理。 #### 为什么必须使用 | 功能 | 说明 | |------|------| | **类型识别** | 提供稳定的类型名称,代码混淆后仍能正确识别 | | **序列化支持** | 序列化/反序列化时使用该名称作为类型标识 | | **组件注册** | 自动注册到 ComponentRegistry,分配唯一的位掩码 | | **调试支持** | 在调试工具和日志中显示可读的组件名称 | #### 基本语法 ```typescript @ECSComponent(typeName: string) ``` - `typeName`: 组件的类型名称,建议使用与类名相同或相近的名称 #### 使用示例 ```typescript // ✅ 正确的用法 @ECSComponent('Velocity') class Velocity extends Component { dx: number = 0; dy: number = 0; } // ✅ 推荐:类型名与类名保持一致 @ECSComponent('PlayerController') class PlayerController extends Component { speed: number = 5; } // ❌ 错误的用法 - 没有装饰器 class BadComponent extends Component { // 这样定义的组件可能在生产环境出现问题: // 1. 代码压缩后类名变化,无法正确序列化 // 2. 组件未注册到框架,查询和匹配可能失效 } ``` #### 与 @Serializable 配合使用 当组件需要支持序列化时,`@ECSComponent` 和 `@Serializable` 需要一起使用: ```typescript import { Component, ECSComponent, Serializable, Serialize } from '@esengine/esengine'; @ECSComponent('Player') @Serializable({ version: 1 }) class PlayerComponent extends Component { @Serialize() name: string = ''; @Serialize() level: number = 1; // 不使用 @Serialize() 的字段不会被序列化 private _cachedData: any = null; } ``` > **注意**:`@ECSComponent` 的 `typeName` 和 `@Serializable` 的 `typeId` 可以不同。如果 `@Serializable` 没有指定 `typeId`,则默认使用 `@ECSComponent` 的 `typeName`。 #### 组件类型名的唯一性 每个组件的类型名应该是唯一的: ```typescript // ❌ 错误:两个组件使用相同的类型名 @ECSComponent('Health') class HealthComponent extends Component { } @ECSComponent('Health') // 冲突! class EnemyHealthComponent extends Component { } // ✅ 正确:使用不同的类型名 @ECSComponent('PlayerHealth') class PlayerHealthComponent extends Component { } @ECSComponent('EnemyHealth') class EnemyHealthComponent 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/esengine'; @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, 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 架构的数据载体,正确设计组件能让你的游戏代码更模块化、可维护和高性能。