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 方案