ECS框架
- Entity(实体)
- Component(组件)
- System(系统)
ECS框架是一个为了迎合游戏开发,在进几年开始慢慢被推荐熟知的框架,最有代表性的作品就是《守望先锋》 ,其对传统的面向对象设计,组件化思维做了进一步的拆解,分成了Entity,Component,System3个部分,三者关系如图:
下面具体说一下这3个部分的功能和特性。
Entity
Entity就是我们的游戏世界的各个实体,如人物、枪支、建筑等等最直接的物体原型。无数个Entity构成勒我们整个游戏世界,同样,我们的游戏世界,也就是安装各个规则去运营各个Entity。在传统的面向对象中,一个实体一般是有各种属性,各种功能函数,各种继承、联合等关联的繁杂存在,在ECS框架中,Entity的作品相对简单,ECS会把各种数据拆分对应到各个Component中,Entity根据需求仅需挂载对应属性的Component,而不用关系具体的数据,且各种Component的逻辑处理拆分到对应各个System中,这样,Entity仅是一个包含多个Component的载体。
举个例子,游戏世界中一辆车,它是一个Entity,它能移动、碰撞、买卖,我们只需挂载对应moveComponent、crashComponent、tradeComponent组件即可,至于各个Component到底存了什么数据,怎么维护的,这都不是Entity需要考虑的,我们只要指定其能移动、碰撞、交易就好了,是不是很简单。
Component
做游戏开发的同学肯定对组件比较熟悉,Component是指具有某一个特定功能特性的组件,传统框架中,我们给一个对象挂载一个组件,通常是对对象附加对应的逻辑处理,从而来让对象具有对应的功能,而在ECS中,则把特定的功能特性划分得更彻底,更加强调解耦。对应这个思路,ECS中的Component可理解为专门存储对应特性的数据集合,而对应特性的逻辑,则放到对应的System中去处理。
仍然以上面的汽车的例子来讨论,汽车要移动,移动的这个特性对应游戏的实现其实是坐标的变化,那对应的数据就有position(x, y),这里我们以二维坐标为例,moveComponent则存放position数据,具体的position的坐标变化逻辑,则放到对应的moveSystem中去处理,我们给汽车挂载moveComponent,这样汽车就能移动勒。
这里另外要强调一点,在ECS的框架中,特定的Component是要整合到一起的,Component中通常会有一个所有component实例化对象的集合,这样处理的好处就是能达到功能解耦,也能做到利用内存cache,达到更好的性能。后文会再去强调这一点。
System
游戏世界中,肯定会有各种各样的规则,我们把各种规则,再细分成各个系统,划分依据是系统最好是功能专一,与其它系统不能耦合,ECS中把这些成为System,且与Component能对应,System主要处理对应Component的逻辑。
如上面汽车的例子,可能游戏世界,不止汽车会移动,玩家也能奔跑,其也能移动,总之,游戏世界中,所有挂载勒moveComponent的Entity都具有移动的功能,所有的moveComponent的移动逻辑,都归到moveSystem中去处理。
代码实例
有了以上的一些初步了解,那对比传统的面向对象,我们剖析一个案例,然后给出大致的代码框架,来更直接对比。
案例:一个简单的竞速比赛,参数的载具有汽车、摩托车,且每个选手之间能发生碰撞,并掉血,若血量为0,则直接出局。
传统OOB设计
class Racer {
hp: number; // 血量
crashDecHp: number; // 碰撞扣除血量
maxSpeed: number; // 最大速度
accSpeed: number; // 加速度
posX: number; // 横坐标
posY: number; // 纵坐标
// 处理碰撞逻辑
checkCrash(){ ...}
// 处理血量逻辑
updateHp() { ...}
// 处理移动逻辑
updatePos() { ...}
}
class GameManager {
allRacer: Racer[];
updateGameLogic() {
allRacer.forEach((racer) => {
racer.updatePos();
racer.checkCrash();
racer.updateHp();
});
}
}
ECS
// entity和component的映射关系,可以根据自己需求去设计,这里只给出最简单的设计,仅供参考
class Racer {
id: number
}
class MoveComponent: {
static allMoveCom: MoveComponent[]; // 所有移动组件集合
// 在entity上挂载component
static registerCom(entity: Racer) {
let newCom = new MoveComponent();
this.allMoveCom.push(newCom);
}
// 销毁整个Component
static destory();
target: Racer; // 挂载的Entity对象,可根据自己需求设计entity和component的映射关系
posX: number; // 横坐标
posY: number; // 纵坐标
speed: number; // 当前速度
// 注销组件
cancelComponent();
}
class MoveSystem {
static Instance(): MoveSystem;
updatePos(dt) {
MoveComponent.allMoveCom.forEach((com) => {
com.posX += com.speed * dt;
// ...
});
}
}
class GameManager {
// 注册System
registerSystem();
// 注销System
cancelSystem();
updateGameLogic() {
MoveSystem.Instance.updatePos();
// HPSystem,CrashSystem 类比MoveSystem
}
}
ECS的优势
- 将数据更有效的组织,提高CPU cache利用率
关于cache的利用率的好处,更多信息大家可以参考ECS 真的是「未来主流」的架构吗?的各位大牛的探讨。 - 逻辑更彻底的解耦,便于并行、扩展、整合
逻辑解耦是所以框架都希望的解决的。传统的框架也是力求将游戏能拆分更好更干净的模块。
总结
ECS的并不是一个突如其来的框架,其思想在传统的游戏开发中多少都会运用到,比如“输入指令”,“网络模块”,各种单例的Manager类等等,其思想很接近ECS,只不过他们更多的吧ECS整合到一个类中,即保存数据,又处理逻辑,能完成单一独立的逻辑。ECS的思想则是看到这种设计的好处,把其严格的扩展到项目的所有逻辑处理中,所有的逻辑都拆分成System,需要参与其中的Entity挂载对应Component,这样项目的逻辑就非常解耦,便于扩充,维护。
一个东西出现,很容易被其亮眼的地方所吸引,但同时我们也要多思考其是否真的那么完美,这里推荐大家阅读下《ECS 真的是「未来主流」的架构吗?》,不一定要为了ECS就强行套用,等自己发现项目的System逻辑臃肿,自己也分不清怎么去解耦时,给自己带来的反而是困扰。
ECS本人并没有实际的项目经验,对其的理解可能也是停留在很肤浅的表面,本文的论述仅供参考,欢迎探讨指正。