本文档旨在深入解析当前项目中的实体-组件-系统(ECS)架构,并提供一套清晰的指南,帮助您将现有的游戏逻辑重构为 ECS 模式。

1. ECS 架构核心逻辑解析

ECS 是一种强大的设计模式,它将游戏对象(实体)的行为和数据(组件)与处理逻辑(系统)完全分离。

核心概念

a. 实体 (Entity) - “这是一个什么?”

  • 定义:实体是一个轻量级的“容器”或 ID。它本身不包含任何数据或逻辑。
  • 作用:它的唯一作用是将多个不同的组件组合在一起,从而定义一个游戏对象的概念。
  • 示例 (SimpleEntity.ts)

    // SimpleEntity 是一个空壳,只用于组合组件
    export class SimpleEntity extends EcsEntity {}

    一个移动的方块实体 = PositionComponent + VelocityComponent + RenderComponent + NodeComponent

b. 组件 (Component) - “它拥有什么数据?”

  • 定义:组件是纯粹的数据容器,只存储数据,不包含任何游戏逻辑
  • 作用:描述实体的一个“方面”或“属性”。例如,PositionComponent 只存储 x 和 y 坐标。
  • 特点

    • 高内聚:每个组件只负责一小块数据(如位置、速度、生命值)。
    • 可复用:任何实体都可以附加任何组件。
  • 示例 (PositionComponent.ts)

    export class PositionComponent extends EcsComponent {
        x: number = 0;
        y: number = 0;
        
        init(x: number, y: number) { /*...*/ }
    }

c. 系统 (System) - “它会做什么事?”

  • 定义:系统是纯粹的逻辑实现者,它不存储任何状态数据
  • 作用:系统会筛选出所有拥有特定组件组合的实体,并对这些实体的数据进行批量处理。
  • 核心机制

    1. 过滤器 (Filter):每个系统定义一个过滤器,用于声明它关心哪些组件。

      // MovementSystem 只关心同时拥有 Position 和 Velocity 的实体
      private moveFilter = filter.all(PositionComponent, VelocityComponent);
    2. 查询 (Query):在每一帧,系统通过过滤器向 EcsWorld 查询所有符合条件的实体。

      const entities = this.world.query(this.moveFilter);
    3. 执行 (Execute):遍历查询到的实体,获取其组件并执行逻辑。

      entities.forEach(entity => {
          const pos = entity.get(PositionComponent);
          const vel = entity.get(VelocityComponent);
          pos.x += vel.vx * dt;
          pos.y += vel.vy * dt;
      });
  • 示例 (MovementSystem.ts):它的唯一职责就是根据速度更新位置,它不关心这个实体是玩家、敌人还是子弹。

d. 世界 (World) - “中央调度器”

  • 定义EcsWorld 是一个全局的单例,是整个 ECS 架构的“大脑”。
  • 作用

    • 管理系统:注册、存储和按顺序执行所有系统 (EcsWorld.inst.addSystem(...))。
    • 管理实体:创建和销毁实体 (EcsWorld.inst.createEntity(...))。
    • 主循环:在游戏的 update 中调用 EcsWorld.inst.execute(dt),驱动所有系统运行。

2. 如何用 ECS 架构重构你的代码

假设我们有一个传统的 Player 类,现在要用 ECS 来重构它。

旧的 OOP 写法:

class Player extends Node {
    public hp: number = 100;
    public position: Vec3 = new Vec3(0, 0, 0);
    public velocity: Vec3 = new Vec3(10, 0, 0);
    private isDead: boolean = false;

    update(dt: number) {
        // 移动逻辑
        this.position.x += this.velocity.x * dt;
        this.node.setPosition(this.position);

        // 受伤逻辑
        if (this.hp <= 0) {
            this.isDead = true;
            this.node.destroy();
        }
    }

    takeDamage(amount: number) {
        this.hp -= amount;
    }
}

这个 Player 类职责过多:它既管数据(hp, position),又管逻辑(移动, 受伤)。

重构步骤

第 1 步:识别数据,创建组件 (Component)

Player 类中的所有状态数据拆分成独立的组件。

  • positionvelocity -> PositionComponent, VelocityComponent (已存在)
  • hp -> HealthComponent
  • Cocos 节点关联 -> NodeComponent (已存在)

创建 HealthComponent.ts:

// in component/HealthComponent.ts
import { ecsclass, EcsComponent } from 'db://pkg/@gamex/cc-ecs';

@ecsclass('HealthComponent')
export class HealthComponent extends EcsComponent {
    static allowRecycling = true;
    
    hp: number = 100;
    maxHp: number = 100;
    
    init(hp: number, maxHp: number) {
        this.hp = hp;
        this.maxHp = maxHp;
        return this;
    }
    
    protected onRemove() {
        this.hp = 100;
        this.maxHp = 100;
    }
}

第 2 步:识别逻辑,创建系统 (System)

Player 类中的所有行为逻辑拆分成独立的系统。

  • 移动逻辑 -> MovementSystem (已存在)
  • 渲染同步逻辑 -> RenderSystem (已存在)
  • 受伤/死亡逻辑 -> DamageSystem, DeathSystem

创建 DeathSystem.ts:

// in system/DeathSystem.ts
import { EcsSystem, filter, NodeComponent } from 'db://pkg/@gamex/cc-ecs';
import { HealthComponent } from '../component/HealthComponent';

export class DeathSystem extends EcsSystem {
    
    // 筛选所有拥有 HealthComponent 的实体
    private deathFilter = filter.all(HealthComponent);
    
    protected execute(): void {
        const entities = this.world.query(this.deathFilter);
        
        entities.forEach(entity => {
            const health = entity.get(HealthComponent);
            
            if (health.hp <= 0) {
                console.log(`实体 ${entity.uuid} 已经死亡,准备销毁`);
                
                // 销毁实体(及其关联的Node)
                this.world.removeEntity(entity); 
            }
        });
    }
}

第 3 步:组装实体 (Entity)

在你的游戏初始化代码中(例如 TestECSMVP.ts),用“搭积木”的方式创建玩家实体。

// in TestECSMVP.ts

// ...

private createPlayer() {
    // 1. 创建 Cocos 节点用于显示
    const playerNode = new Node('Player');
    playerNode.parent = this.node;
    // ... (添加 Graphics, UITransform 等)

    // 2. 创建 ECS 实体并关联节点
    const playerEntity = EcsWorld.inst.createEntity(SimpleEntity, playerNode);

    // 3. 添加组件,定义实体的所有属性
    playerEntity.add(PositionComponent).init(100, 100);
    playerEntity.add(VelocityComponent).init(50, 0); // 向右移动
    playerEntity.add(HealthComponent).init(150, 150);
    playerEntity.add(NodeComponent).setContentSize(40, 40);
    playerEntity.add(RenderComponent).init(Color.CYAN, 40, 'rect');
    
    console.log('玩家实体创建完毕!');
}

第 4 步:注册新系统

别忘了在主逻辑中注册你新创建的系统。顺序很重要! 通常,逻辑更新(如移动、伤害)应在渲染更新之前。

// in TestECSMVP.ts

private initECSWorld() {
    if (!this.systemsAdded) {
        // 逻辑系统
        EcsWorld.inst.addSystem(MovementSystem);
        EcsWorld.inst.addSystem(BoundarySystem);
        EcsWorld.inst.addSystem(DeathSystem); // <-- 注册新系统
        
        // 渲染系统(通常在最后)
        EcsWorld.inst.addSystem(RenderSystem);
        
        this.systemsAdded = true;
    }
    // ...
}

重构优势

  • 高度解耦MovementSystem 不知道什么是 Player,它只知道移动。你可以轻松地让怪物、子弹也拥有 PositionVelocity 组件,它们就会自动被 MovementSystem 处理。
  • 易于扩展:想添加“中毒”效果?只需创建一个 PoisonComponent (存储伤害、时长) 和一个 PoisonSystem (每秒扣血),然后把组件添加到任何实体上即可。
  • 性能更优:系统批量处理内存连续的数据,对 CPU 缓存非常友好,性能通常优于零散的对象调用。
  • 代码清晰:数据和逻辑分离,职责单一,代码更容易阅读和维护。

通过遵循以上步骤,您可以逐步、安全地将现有代码迁移到 ECS 架构,从而获得一个更灵活、更高效、更易于维护的代码库。

最后修改:2025 年 11 月 13 日
反正没人给,你也爱给不给吧。