Entity Component System(ECS)是一种数据驱动的架构模式,在游戏开发和需要处理大量动态对象的系统中广泛应用。它通过将传统面向对象的继承体系拆解为实体(Entity) 、组件(Component) 和 系统(System) 三个核心概念,实现了高度的灵活性和可维护性。本文使用 TypeScript 实现一个功能完整的 ECS 系统。
ECS 的核心思想
在实现之前,让我们先理解 ECS 的三个核心特性:
1. 组合优于继承(Composition over Inheritance)
传统 OOP 通过继承构建对象层级:
java
GameObject
├── Player extends GameObject
├── Enemy extends GameObject
│ ├── FlyingEnemy extends Enemy
│ └── BossEnemy extends Enemy
└── NPC extends GameObject
这种设计会导致:
- 层级爆炸:需求变化时继承链快速膨胀
- 功能重复:Flying 能力无法在 Player 和 Enemy 间复用
- 耦合严重:修改基类影响所有子类
ECS 通过组合解决这个问题:
typescript
// 灵活组合,无继承链
player = Entity + Position + Velocity + Health + PlayerInput;
enemy = Entity + Position + Velocity + Health + AI;
flyingEnemy = Entity + Position + Velocity + Health + AI + Flying;
flyingNPC = Entity + Position + Velocity + Flying + Dialogue;
2. 数据与逻辑分离(Data and Logic Separation)
- Component:纯数据结构,不包含任何逻辑
- System:纯逻辑处理,对拥有特定组件的实体执行操作
- Entity:仅仅是 ID,作为组件的容器
3. 查询驱动(Query-Based Processing)
System 声明所需的组件类型,World 自动筛选出匹配的实体:
typescript
class MovementSystem {
requiredComponents = ["Position", "Velocity"];
update(entities) {
// 只处理同时拥有 Position 和 Velocity 的实体
}
}
使用 TypeScript 实现 ECS
理解了 ECS 的设计思想后,让我们动手实现一个功能完整的 ECS 系统。我们将采用渐进式的方式,从基础类型定义开始,逐步构建出包含实体管理、组件存储、系统查询等特性的完整系统。
1:定义基础类型
首先,我们需要定义 ECS 系统中的基础数据结构:实体、组件和系统。
typescript
type EntityId = number;
type ComponentType = string;
// Component 是纯数据
interface Component {
readonly type: ComponentType;
}
// System 处理具有特定组件的实体
interface System {
readonly requiredComponents: Set<ComponentType>;
update(entities: EntityId[], world: World, deltaTime: number): void;
}
关键点:
- EntityId 只是一个数字,实体本身没有数据和行为
- Component 要求有
type字段标识组件类型,其他字段由具体组件定义 - System 声明所需组件集合,World 负责筛选匹配的实体
deltaTime用于时间相关的计算(如物理模拟)
2:实体与组件存储设计
有了基础类型定义后,我们需要考虑如何存储实体和组件。这里的核心挑战是:如何高效地查询拥有特定组件组合的实体。
typescript
export class World {
private nextEntityId = 1;
private entities = new Set<EntityId>();
private components = new Map<EntityId, Map<ComponentType, Component>>();
private systems: System[] = [];
}
数据结构说明:
typescript
// 实体存储
entities: Set<EntityId>
// O(1) 存在性检查,O(n) 遍历
// 组件存储(二级映射)
components: Map<EntityId, Map<ComponentType, Component>>
// 外层:实体ID → 该实体的所有组件
// 内层:组件类型 → 组件数据
// 系统存储
systems: System[]
// 按注册顺序执行
关键点:
- 使用 Set 存储实体 ID,快速检查实体是否存在
- 使用 二级 Map 存储组件:第一层按实体索引,第二层按组件类型索引
- 这种结构使得单个实体的组件增删改查都是 O(1)
- 查询操作需要遍历所有实体,是 O(n)(可以通过 Archetype 优化)
3:实体生命周期管理
现在实现实体的创建和删除。实体的生命周期非常简单:创建时分配 ID,删除时清理所有相关数据。
typescript
// 创建新实体
createEntity(): EntityId {
const id = this.nextEntityId++;
this.entities.add(id);
this.components.set(id, new Map());
return id;
}
// 删除实体及其所有组件
removeEntity(id: EntityId): void {
this.entities.delete(id);
this.components.delete(id);
}
关键点:
- 实体 ID 使用递增计数器生成,保证唯一性
- 创建实体时同时初始化空的组件 Map
- 删除实体时自动删除所有关联的组件
- 删除操作是级联的,不会留下孤儿组件
4:组件管理
接下来实现组件的增删改查。组件是 ECS 的核心数据载体,这些操作需要简洁高效。
typescript
// 为实体添加组件
addComponent(entityId: EntityId, component: Component): void {
const entityComponents = this.components.get(entityId);
if (entityComponents) {
entityComponents.set(component.type, component);
}
}
// 移除实体的组件
removeComponent(entityId: EntityId, componentType: ComponentType): void {
this.components.get(entityId)?.delete(componentType);
}
// 获取实体的组件
getComponent<T extends Component>(entityId: EntityId, componentType: ComponentType): T | undefined {
return this.components.get(entityId)?.get(componentType) as T | undefined;
}
关键点:
- 使用 可选链操作符 (
?.)简化空值检查 getComponent使用泛型<T>提供类型提示- 组件通过
type字段区分,支持同类型组件的替换 - 如果实体不存在,操作会静默失败(返回
undefined)
5:组件查询机制
查询是 ECS 的核心功能之一。我们需要能够找出所有拥有特定组件组合的实体。
typescript
// 检查实体是否拥有所有指定组件
hasComponents(entityId: EntityId, componentTypes: Set<ComponentType>): boolean {
const entityComponents = this.components.get(entityId);
if (!entityComponents) return false;
for (const type of componentTypes) {
if (!entityComponents.has(type)) return false;
}
return true;
}
// 查询拥有特定组件的实体
query(componentTypes: ComponentType[]): EntityId[] {
const typeSet = new Set(componentTypes);
return Array.from(this.entities).filter(id => this.hasComponents(id, typeSet));
}
查询流程:
- 遍历所有实体 ID
- 对每个实体检查是否拥有所需的所有组件
- 返回匹配的实体列表
关键点:
- 将数组转换为 Set 提高查询效率(
has是 O(1)) - 使用
filter进行声明式查询 - 时间复杂度:O(n × m),n 是实体数量,m 是查询的组件数量
- 优化空间:可以引入 Archetype 将查询优化到 O(1)
6:系统注册与更新
最后实现系统的注册和执行。系统是 ECS 中的逻辑处理单元,负责对匹配的实体执行操作。
typescript
// 注册系统
addSystem(system: System): void {
this.systems.push(system);
}
// 更新所有系统
update(deltaTime: number): void {
for (const system of this.systems) {
const matchingEntities = Array.from(this.entities)
.filter(id => this.hasComponents(id, system.requiredComponents));
system.update(matchingEntities, this, deltaTime);
}
}
执行流程:
- 遍历所有注册的系统
- 对每个系统,查询匹配
requiredComponents的实体 - 调用系统的
update方法,传入匹配的实体列表
关键点:
- 系统按注册顺序执行,顺序很重要(如:移动系统 → 碰撞检测系统)
- 每个系统在每帧都会重新查询,支持动态添加/删除组件
- 系统可以通过
world参数访问和修改实体的组件 - 优化空间:可以缓存查询结果,在组件变化时才重新查询
完整代码
typescript
type EntityId = number;
type ComponentType = string;
interface Component {
readonly type: ComponentType;
}
interface System {
readonly requiredComponents: Set<ComponentType>;
update(entities: EntityId[], world: World, deltaTime: number): void;
}
export class World {
private nextEntityId = 1;
private entities = new Set<EntityId>();
private components = new Map<EntityId, Map<ComponentType, Component>>();
private systems: System[] = [];
createEntity(): EntityId {
const id = this.nextEntityId++;
this.entities.add(id);
this.components.set(id, new Map());
return id;
}
removeEntity(id: EntityId): void {
this.entities.delete(id);
this.components.delete(id);
}
addComponent(entityId: EntityId, component: Component): void {
const entityComponents = this.components.get(entityId);
if (entityComponents) {
entityComponents.set(component.type, component);
}
}
removeComponent(entityId: EntityId, componentType: ComponentType): void {
this.components.get(entityId)?.delete(componentType);
}
getComponent<T extends Component>(
entityId: EntityId,
componentType: ComponentType
): T | undefined {
return this.components.get(entityId)?.get(componentType) as T | undefined;
}
hasComponents(
entityId: EntityId,
componentTypes: Set<ComponentType>
): boolean {
const entityComponents = this.components.get(entityId);
if (!entityComponents) return false;
for (const type of componentTypes) {
if (!entityComponents.has(type)) return false;
}
return true;
}
addSystem(system: System): void {
this.systems.push(system);
}
query(componentTypes: ComponentType[]): EntityId[] {
const typeSet = new Set(componentTypes);
return Array.from(this.entities).filter((id) =>
this.hasComponents(id, typeSet)
);
}
update(deltaTime: number): void {
for (const system of this.systems) {
const matchingEntities = Array.from(this.entities).filter((id) =>
this.hasComponents(id, system.requiredComponents)
);
system.update(matchingEntities, this, deltaTime);
}
}
}
export type { EntityId, Component, ComponentType, System };
至此,我们已经实现了一个功能完整的 ECS 系统,核心代码只有约 90 行。
使用示例
让我们通过几个实际例子来看看它是如何工作的:
基础使用
typescript
import { World } from "./ecs.ts";
// 定义组件类型
interface Position extends Component {
type: "Position";
x: number;
y: number;
}
interface Velocity extends Component {
type: "Velocity";
dx: number;
dy: number;
}
// 创建世界
const world = new World();
// 创建实体
const player = world.createEntity();
// 添加组件
world.addComponent(player, { type: "Position", x: 0, y: 0 });
world.addComponent(player, { type: "Velocity", dx: 10, dy: 5 });
// 查询拥有特定组件的实体
const movableEntities = world.query(["Position", "Velocity"]);
console.log(movableEntities); // [1]
实现 System
typescript
// 移动系统:处理所有有位置和速度的实体
class MovementSystem implements System {
requiredComponents = new Set(["Position", "Velocity"]);
update(entities: EntityId[], world: World, deltaTime: number): void {
for (const entityId of entities) {
const pos = world.getComponent<Position>(entityId, "Position");
const vel = world.getComponent<Velocity>(entityId, "Velocity");
if (pos && vel) {
pos.x += vel.dx * deltaTime;
pos.y += vel.dy * deltaTime;
}
}
}
}
// 注册并运行系统
world.addSystem(new MovementSystem());
// 每帧更新
setInterval(() => {
world.update(1 / 60); // 60 FPS
}, 16);
多系统协同
typescript
interface Health extends Component {
type: "Health";
value: number;
}
// 伤害系统:减少生命值
class DamageSystem implements System {
requiredComponents = new Set(["Health"]);
damagePerSecond = 10;
update(entities: EntityId[], world: World, deltaTime: number): void {
for (const entityId of entities) {
const health = world.getComponent<Health>(entityId, "Health");
if (health) {
health.value -= this.damagePerSecond * deltaTime;
// 如果生命值小于0,移除实体
if (health.value <= 0) {
world.removeEntity(entityId);
}
}
}
}
}
// 注册多个系统
world.addSystem(new MovementSystem());
world.addSystem(new DamageSystem());
// 创建带生命值的移动实体
const enemy = world.createEntity();
world.addComponent(enemy, { type: "Position", x: 100, y: 100 });
world.addComponent(enemy, { type: "Velocity", dx: -5, dy: 0 });
world.addComponent(enemy, { type: "Health", value: 20 });
// 更新 2 秒后,enemy 的生命值归零,会被自动删除
world.update(1);
world.update(1);
const stillExists = world.query(["Health"]);
console.log(stillExists.length); // 0
动态组件组合
typescript
interface Flying extends Component {
type: "Flying";
altitude: number;
}
// 创建不同类型的实体
const groundEnemy = world.createEntity();
world.addComponent(groundEnemy, { type: "Position", x: 0, y: 0 });
world.addComponent(groundEnemy, { type: "Velocity", dx: 5, dy: 0 });
const flyingEnemy = world.createEntity();
world.addComponent(flyingEnemy, { type: "Position", x: 0, y: 0 });
world.addComponent(flyingEnemy, { type: "Velocity", dx: 5, dy: 0 });
world.addComponent(flyingEnemy, { type: "Flying", altitude: 100 });
// 飞行系统只处理会飞的实体
class FlyingSystem implements System {
requiredComponents = new Set(["Flying", "Position"]);
update(entities: EntityId[], world: World): void {
for (const entityId of entities) {
const flying = world.getComponent<Flying>(entityId, "Flying");
console.log(
`Entity ${entityId} is flying at altitude ${flying?.altitude}`
);
}
}
}
world.addSystem(new FlyingSystem());
world.update(0); // 只有 flyingEnemy 会被处理
ECS 的优势
1. 灵活性
添加新功能只需:
- 定义新组件(数据)
- 创建新系统(逻辑)
- 组合到实体上
无需修改现有代码,符合开闭原则。
2. 可复用性
组件和系统都是独立的,可以自由组合:
typescript
// 复用 Flying 组件
player = Entity + Position + Velocity + Flying + PlayerInput;
platform = Entity + Position + Flying + Obstacle;
powerup = Entity + Position + Flying + Collectable;
3. 性能潜力
- 数据局部性:组件可以按类型连续存储(本实现未优化)
- 并行处理:不相交的系统可以并行执行
- 缓存友好:遍历单一类型的组件数据,CPU 缓存命中率高
4. 可测试性
- 组件是纯数据,易于序列化和 mock
- 系统是纯逻辑,可以独立测试
- 不依赖复杂的继承关系
5. 序列化能力
组件的纯数据特性使得 ECS 天然支持序列化:
typescript
// 导出世界状态
const snapshot = world.serialize();
// 返回 JSON 格式: { entities: [...], components: {...} }
// 恢复世界状态
const newWorld = World.deserialize(snapshot);
应用场景:
- 存档系统:保存和加载游戏进度
- 网络同步:多人游戏状态同步
- 回放功能:记录和重放游戏过程
- 调试工具:导出状态用于分析
- 热重载:开发时保持状态重载代码