Entity-Component-System(ECS)是一个gameplay框架,主要定义了一个模型来解决更新问题。
ECS系统遵守组合优于继承原则,通过动态添加删除Component改变Entity的行为。System定义了一个全局的更新函数,它遍历相应Component组合并执行Update函数。Entity对应于传统的GameObject,不过在ECS里面Entity只是一个ID,用于标识对象以及管理对象生命周期。Component定义和持有数据,可以认为这里Component持有之前的GameObject上的部分特定数据,通常我们把可以共用的数据单独定义成一个Component。
想象一下在一个MMO游戏中,我们有Player,Npc,Doodad,Pet等等。我们面对的对象种类非常少,但每个种类的行为又非常多与复杂。我们不需要定义Cat,Dog,Dragon继承Pet。我们有一个Pet能文会武,无所不能。然后我们的Pet可以采集Doodad,与Player交互,与Npc战斗。这里可以发现按传统的OO定义对象并没有带来多少便利性,最后所有的对象都趋向于变成Monster。可以发现传统的OO对于这样的MMO游戏是不适用的。ECS却是一种非常适合框架,对于任何一个行为,我们只需要获取想要的数据(Component组合),并执行相应的更新函数即可。
当然真实的游戏情况是复杂的,事物也并非是孤立的。区别于传统的EC架构,ECS中的System约定了一种全局Update方式,这带来较大的便利性。大部分情况下System只需要关心自身Component组合的行为,而不需要关心其他System。对于有交互的一些复杂行为,则可能需要明确下部分System之间的顺序。由于System是一个独立的全局行为,所以相对较好理解的。我们知道System的行为,自然也知道了相关联的System执行时序。这里还有一个难以解决的问题,就是存在部分行为需要两个System进行交互。如果通过在Component增加Flag的方式,然后交互的System读取Component上的Flag并进行检查每帧。这看起来是非常低效与繁琐的。一个比较好的解决方案是观察者模式,每个System支持一套Event机制,一般来说每个Event只执行一次。最后讨论下共享行为,存在一些类似的行为在不同的System里面,这里把这些行为抽离为全局Utility函数。如果在多处调用一个Utility,那么这个函数就应该依赖很少的组件,而且不应该带副作用或者很少的副作用。如果你的Utility函数依赖很多组件,那就试着限制调用点的数量。
使用ECS意味着需要思考如何用ECS解决问题,区别于OO的偏人性化的思维模式。可能会有一些不习惯,不过尝试学习这种思维模式本身就是一件很有意思的事情不是么?上面讨论了ECS以及一些ECS实现的细节,但是为什么要使用ECS呢?我觉的主要原因是ECS解决复杂问题的能力,对于一个高复杂度的问题,如果代码中耦合其他不需要的信息,将极大的提高编码与维护成本。ECS是一个不错的框架,不过前提是需要遵守一些约定,同时也并不是所有的事情都适合ECS来做。不过ECS有本身具有极高的兼容性,可以与其他系统共存。本身一个好的完整的游戏也应该是由多个架构构成,单一的架构都存在自身的优势与劣势。
最后再谈谈ECS的优势,ECS是一个近乎完美的解决方案,可以大规模的提升大部分MMO游戏的开发效率。而且近乎无限制去实现游戏逻辑,很多时候在现有的结构下面我们很难去满足一些策划的需求,ECS在这里有天然的优势。要知道一个MMO游戏上线只是开始,后续的快速迭代开发是常态,能否快速迭代开发也很大程度上决定了一个游戏的成功与失败。然后就是性能问题,性能问题容易变成主要问题,而且就算性能不是问题,如果性能足够高的话,我们可以做更多更复杂更有意思的行为。这里内存Cache Miss是主要原因,内存的性能和CPU差太远,这几乎是大部分游戏会碰到的问题。ECS类似于面向流编程,具有较好的内存友好性。ECS解决了内存管理与生命周期管理并且多线程友好,当然这些对于优秀的游戏开发者来说不是问题,但是如果所有的开发过程中都要考虑这么多问题是低效的,而且当团队有20多个程序员的时候,并不能要求没有人的代码都具有这些特征。