ECS 架构

Entity-Component-System——数据驱动设计的游戏架构, SoA 内存布局, Archetype, 稀疏集。

概念

ECS (Entity-Component-System) 是现代游戏引擎的核心数据架构,与传统的 OOP 继承树完全相反。在 ECS 中: Entity 只是一个 ID, Component 是纯数据 (无行为), System 是纯逻辑 (无状态)。这种分离带来三个根本优势: 缓存友好的数据布局 (SoA), 自然的并行化, 极高的模块复用性。

核心概念对比

概念OOP 继承ECS
对象定义class Player : Entity { … }Entity ID = 42
数据成员变量散布在继承树的各层Component: Position{x,y,z}, Velocity{vx,vy,vz}
行为成员方法 + 虚函数System: MoveSystem(Position, Velocity)
内存布局AoS (Array of Structs)SoA (Struct of Arrays)
扩展方式继承新类添加新 Component 或新 System

ECS 基础模型

Entity:     [ID=1001]              // 无数据, 仅标识符
Entity:     [ID=1002]
Entity:     [ID=1003]

Components (按类型存储, SoA):
    Position:  [(1001: 0,1,0), (1002: 5,2,1), (1003: -1,3,0)]
    Velocity:  [(1001: 1,0,0), (1003: 0,0,1)]
    Health:    [(1001: 100),   (1002: 50),  (1003: 200)]
    PlayerTag: [(1001)]
    EnemyTag:  [(1002),        (1003)]

Systems:
    MoveSystem:     FOR (Position, Velocity) ← 查询拥有这两个组件的实体
                        position += velocity * dt
                        // 只遍历: (1001), (1003) — 因为有 Position + Velocity
    DamageSystem:   FOR (Health, EnemyTag)
                        ...
    RenderSystem:   FOR (Position, Model)
                        ...

内存布局: AoS vs SoA

AoS (Array of Structs) — OOP 方式:
    struct Entity {
        Position pos;
        Velocity vel;
        Health   hp;
    };
    内存: [pos|vel|hp] [pos|vel|hp] [pos|vel|hp] ...
    问题: MoveSystem 只需要 pos 和 vel, 但 hp 也被加载到缓存 (浪费)

SoA (Struct of Arrays) — ECS 方式:
    Position[] pos_array = {(0,1,0), (5,2,1), ...};
    Velocity[] vel_array = {(1,0,0), (0,0,1), ...};
    内存: [pos|pos|pos|...] [vel|vel|vel|...]
    优势: MoveSystem 遍历时, 所有 pos 连续存放, 缓存命中率最大化

Archetype (原型)

Archetype = 一组特定 Component 类型的集合

例如:
    Archetype<Position, Velocity, Health, PlayerTag>   →  实体 1001
    Archetype<Position, Health, EnemyTag>               →  实体 1002, 1003
    Archetype<Position, Velocity>                       →  无实体

原型存储 (Unity DOTS / Bevy 方式):
    每个 Archetype 内存块连续存储其实体的所有 Component:

    Chunk (Archetype<Position, Velocity, Health, PlayerTag>, 容量=64):
        [Pos][Pos]...[Pos] [Vel][Vel]...[Vel] [HP][HP]...[HP] [EntityID...]
         <-- 64 个实体, 每列连续排列 -->

迭代效率:
    MoveSystem 需要: Position + Velocity
    匹配的 Archetype: Archetype<Position, Velocity, Health, PlayerTag>
                      Archetype<Position, Velocity>
    遍历这些 Archetype 的 Chunk, 逐列读取 → 极佳缓存友好

Sparse Set (稀疏集)

另一种 ECS 存储方案 (EnTT, flecs):

Sparse Set 实现:
    // 每个 Component 类型有一个 Sparse Set
    SparseSet<Position>:
        sparse[]:  // Entity → 索引 (大小 = max_entity_count)
        dense[]:   // 索引 → Entity 实际数据
        packed[]:  // [Position{0,1,0}, Position{5,2,1}, Position{-1,3,0}]

    查询 Position + Velocity:
        选择 dense 更小的 SparseSet, 遍历其 dense
        检查每个 Entity 是否也存在于另一个 SparseSet 中
        // O(min(|A|, |B|)) 迭代, O(1) 检查

    添加/删除 Component = SparseSet 的插入/删除 = O(1)
    比 Archetype 更灵活 (不需要移动 Archetype)
    但缓存局部性不如 Archetype 方案