实体组件系统 (Entity Component System, ECS) 是一种遵循 组合优于继承 (Composition over Inheritance) 原则的架构模式。它将游戏对象的数据与行为彻底分离。
int 或 uint)。它不包含任何数据,也不包含任何函数。它只是组件的容器索引。Position { x, y }, Velocity { x, y }, Health { current, max }.MovementSystem 筛选所有拥有 Position 和 Velocity 的实体,执行 Pos += Vel * dt。在传统的面向对象编程 (OOP) 中,我们习惯于:
class Player : Character {
int hp;
Vector3 pos;
void Update() { ... }
}
FlyingMonster 还是 SwimmingMonster?多重继承极其复杂。Player 时,预取不到下一个对象的数据,导致 CPU 经常停下来等待内存(性能杀手)。Player 类往往包含了移动、攻击、动画、音效等所有逻辑,变成一个几千行的 “God Class”。数据导向设计 (Data-Oriented Design) 关注数据的内存布局。
Velocity)在内存中紧密排列。这是一个从“我是谁”到“我有什么”的转变。
Player 对象引用。EntityID (整数)。不要把所有数据都塞进一个类。根据功能拆分数据。
| OOP 属性 | ECS 组件 |
|---|---|
class Monster { int hp; } |
struct HealthComponent { int value; } |
class Monster { float speed; } |
struct MoveSpeedComponent { float value; } |
class Monster { bool isStunned; } |
struct StunTag : IComponentData {} (空组件,仅作标记) |
不要在类里写 Update()。思考“这个行为需要什么数据”。
OOP 写法:
class Monster {
void Update() {
if (!isStunned) {
pos += speed * dt;
}
}
}
ECS 写法:
class MovementSystem : System {
void Update() {
// 筛选: 有 Position, 有 Speed, 但没有 StunTag 的实体
var group = GetEntities(Position, Speed).Exclude(StunTag);
foreach (var entity in group) {
ref var pos = ref entity.Get<Position>();
var speed = entity.Get<Speed>();
pos += speed * dt;
}
}
}
假设我们要实现:当玩家捡起磁铁道具时,全屏所有的经验宝石飞向玩家。
Player 碰撞到 Magnet。Player 调用 GameManager.Instance.GetAllGems()。Gem 对象,调用 gem.SetTarget(player)。Gem.Update() 中判断如果有 Target,则向 Target 移动。
MagnetBuffComponent: 标记玩家捡到了磁铁。MoveToTargetComponent { Entity target; }: 给宝石用的组件。MagnetSystem:
MagnetBuff。GemTag 的实体。MoveToTargetComponent { target = player } 给这些实体。HomingSystem:
Position 和 MoveToTargetComponent 的实体。HomingSystem 只处理需要追踪的物体,不需要遍历所有物体来判断 if (target != null)。MoveToTargetComponent,逻辑完全复用。x 和 y 拆成两个组件。通常相关联的数据(如位置和旋转)可以放在一起,或者根据访问频率拆分。ECS 不仅仅是性能优化工具,更是一种架构解耦的利器。它强迫开发者关注数据流 (Data Flow) 而非对象状态,这在处理复杂交互(如技能系统、Buff系统)时会带来意想不到的清晰度。