本文档旨在深入解析当前项目中的实体-组件-系统(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) - “它会做什么事?”
- 定义:系统是纯粹的逻辑实现者,它不存储任何状态数据。
- 作用:系统会筛选出所有拥有特定组件组合的实体,并对这些实体的数据进行批量处理。
核心机制:
过滤器 (Filter):每个系统定义一个过滤器,用于声明它关心哪些组件。
// MovementSystem 只关心同时拥有 Position 和 Velocity 的实体 private moveFilter = filter.all(PositionComponent, VelocityComponent);查询 (Query):在每一帧,系统通过过滤器向
EcsWorld查询所有符合条件的实体。const entities = this.world.query(this.moveFilter);执行 (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 类中的所有状态数据拆分成独立的组件。
position和velocity->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,它只知道移动。你可以轻松地让怪物、子弹也拥有Position和Velocity组件,它们就会自动被MovementSystem处理。 - 易于扩展:想添加“中毒”效果?只需创建一个
PoisonComponent(存储伤害、时长) 和一个PoisonSystem(每秒扣血),然后把组件添加到任何实体上即可。 - 性能更优:系统批量处理内存连续的数据,对 CPU 缓存非常友好,性能通常优于零散的对象调用。
- 代码清晰:数据和逻辑分离,职责单一,代码更容易阅读和维护。
通过遵循以上步骤,您可以逐步、安全地将现有代码迁移到 ECS 架构,从而获得一个更灵活、更高效、更易于维护的代码库。