ECS架构
前言
伴随着守望先锋ECS概念的剔除,以及Unity DOTS里ECS的普及,ECS架构设计被广泛运用到了游戏开发行业,本章节核心是想学习ECS设计理念而非Unity DOTS一整套。学习理解ECS设计理念后,编写一套属于自己的简易版ECS用于常规游戏功能开发框架。
ECS
首先看一下维基百科的基础介绍:
大概翻译一下就是:
ECS是一种用在游戏开发的架构设计模式。ECS由Entity,Component,System组成。Entity由Component组成,Component由数据组成,System负责处理Enity身上的Component数据。
ECS遵循组合大于继承的设计理念,System负责处理所有符合Component要求的Entity逻辑处理。
放一张Unity ECS的一张架构设计示意图:
Entity
大概理解就是Entity是一个有唯一Id标识由Component组成,可以表达任何程序逻辑对象(比如角色,可视化特效,UI,网络等)的一个逻辑对象。
Component
components contain entity data that systems can read or write.
大概理解就是Component包含了Entity的数据,然后由System访问和改写。
System
A system provides the logic that transforms component data from its current state to its next state.
大概理解就是System是负责处理Entity的Component数据逻辑的(比如我们的游戏逻辑)。
World
A world is a collection of entities. An entity’s ID number is only unique within its own world.
大概理解就是World是所有Entity的集合,每个Entity在World都有唯一标识。
ECS设计
这里引用其他博主的话讲述ECS之间的关系:
从上面可以看出,通过把Component(数据)和System(逻辑)分离。同时因为System是由Entity的Component驱动是否生效的,所以Entity的行为是由Component决定的,而Component和System都是可以动态增删的,这样一来我们就能快速的把数据和逻辑都重用起来。
了解了ECS的大概设计,接下来让我们结合别人给Lua设计的tiny-ecs看看ECS在Lua里是如何运作起来的。
Note:
- Component只存数据不提供方法接口,System只提供逻辑不存数据
tiny-ecs
Github连接tiny-ecs
tiny-ecs代码不多,感兴趣的自行下载了解,这里直接学习tiny-ecs在lua里的设计理念。
tiny-ecs里依然有World,System,Entity的概念,但弱化了Component的概念,Component的概念在tiny-ecs里就是任何数据字段。
tiny-ecs里World依然有N个System+N个Entity组成,System和Entity的添加和移除在World里设计好了固定的生命周期,上层逻辑通过这些固定流程接口可以实现System过滤特定Entity后进行Entity数据访问逻辑编写修改,从而实现游戏逻辑功能。
World生命周期
tiny.world(创建一个world)(可直接初始化指定System和Entity)
System生命周期
tiny.processingSystem(定义一个System)
tiny.addSystem(添加一个待添加System)
tiny.tiny_manageSystems(第二帧处理System添加和移除)
- system.onAddToWorld(响应系统被添加到World)
- sytem.onAdd(所有符合条件的Entity响应添加)
tiny.tiny_manageEntities(第二帧处理System的Entity添加,移除和变化)
- system.onRemove(响应系统相关Entity移除)
- system.onAdd(响应系统相关Entity添加)
tiny.update(World每帧更新)
tiny.tiny_manageSystems(处理System添加和移除))
tiny.tiny_manageEntities(处理System的Entity添加,移除和变化))
tiny.processingSystemUpdate(System更新流程)
- system.preProcess(系统每帧预处理)
- system.process(系统每帧处理符合条件的entity)
- system.postProcess(系统每帧后处理)
tiny.removeSystem(添加一个待移除System)
- tiny.tiny_manageSystems(第二帧处理System添加和移除)
- system.onRemove(响应系统已添加Entity移除)
- system.onRemoveFromWorld(响应系统被从World移除)
Note:
1. 上述流程没包含Entity变化相关流程
Entity生命周期
tiny.addEntity(给World添加个待添加的Entity)
tiny.update(World每帧更新)
tiny.tiny_manageEntities(处理System的Entity添加,移除和变化))
tiny.removeEntity(给World添加个待移除的Entity)
tiny.update(World每帧更新)
tiny.tiny_manageEntities(处理System的Entity添加,移除和变化))
从tiny-ecs的源码可以看出,tiny-ecs对于ECS里Component的概念设计相对较弱,更多的是抽象规范出Enitity和System的统一更新和生命周期流程。
对于tiny-ecs而言,Component就是Entity里的任何数据,并没有考虑Component设计上的重用,以及E和C的一对多设计。
设计要点:
- tiny-ecs里的System和Entity的添加移除都统一到下一帧去处理,这样有效避免了System和Entity在单帧里数量动态变化的访问问题。
疑问点:
- 因为System和Entity都是下一帧才处理添加移除,那么System和Entity从逻辑上来说单帧没有立刻添加到World里,假设上一行代码写添加Entity,下一行去获取这个Entity是得不到的,System同理,或许这也是tiny-ecs没有设计直接获取Entity和System接口的原因吧。
- tiny-ecs并没有设计获取特定Entity相关的接口,也没有设计获取System的接口,那么在一个System里如果想访问特定Entity对象时,我们并没有方法可以获取到,这种情况应该如何处理了?(个人觉得应该暴露出获取特定符合条件Entity相关的接口)
实战
了解了ECS基础概念和tiny-ecs的核心设计,接下来实战设计一版简易的C# ECS框架,重点要实现以下几个重要概念:
- 实现Entity唯一对象的概念
- 实现Entity由Component组成的概念
- Component的组合决定了Entity有哪些功能
- 实现Component只定义数据,System负责逻辑处理的概念
- 实现System更新逻辑支持过滤Component类型概念
- 实现System支持获取特定Component类型的Entity
- 实现同一个System可以有无数个相同类型的Entity
- 实现同一个Entity相同类型Component可以有多个
- 实现同一个World相同类型System只能有一个
- 实现World对Entity和System的统一添加和删除流程
- 相同World名字只能同时存在一个
- World实现对Enity实体对象挂在父节点的统一
Entity设计
BaseEntity.cs
1 |
|
上述的数据结构定义主要是针对以下几个需求:
- 实现Entity唯一对象的概念(Uuid)
- 实现Entity由Component组成的概念(mComponentTypeMap)
- 实现同一个Entity相同类型Component可以有多个(mComponentTypeMap)
上面没有展示Component相关的接口,详情直接参考源代码。
Component设计
BaseComponent.cs
1 | /// <summary> |
上述的数据结构定义主要是针对以下几个需求:
- 实现Component只定义数据
可以看到BaseComponent唯一的接口就是逻辑对象的出池入池和数据重置接口(ResetDatas)流程,这也正是符合Component只有数据定义的设计。
System设计
BaseSystem.cs
1 | //// <summary> |
上述的数据结构定义主要是针对以下几个需求:
- System负责逻辑处理的概念(PreProcess,Process,PostProcess,LogicUpdate,FixedUpdate,LateUpdate等接口支持逻辑编写)
- 实现System更新逻辑支持过滤Component类型概念(Filter接口支持了自定义对Entity的过滤)
- 实现System支持获取特定Component类型的Entity(Filter接口支持了自定义对Entity的过滤)
- 实现同一个World相同类型System只能有一个(ClassType和OwnerWorld设计配合BaseWorld实现1对1)
- 实现同一个System可以有无数个相同类型的Entity(SystemEntityList定义支持无数个Entity)
可以看到System定义了Filter接口用于过滤符合条件的Entity才进入Process(BaseEntity entity, float deltaTime)流程,方便System只处理感兴趣的Entity对象。
同时System抽象了和World相关的流程接口(e.g. AddEvents,OnAddToWorld,OnAdd,RemoveEvents,OnRemove,OnRemoveFromWorld),方便上层编写系统逻辑
World设计
BaseWorld.cs
1 | /// <summary> |
WorldManager.cs
1 |
|
上述的数据结构定义主要是针对以下几个需求:
- 实现同一个World相同类型System只能有一个(BaseWorld.mAllSystemTypeAndSystemMap)
- 实现World对Entity和System的统一添加和删除流程(BaseWorld里无论是System还是Entity都实现了统一延迟添加和删除的逻辑,详情参考ManagerSystems()和ManagerEntities())
- 相同World名字只能同时存在一个(WorldManager.mAllWorldMap)
- World实现对Enity实体对象挂在父节点的统一(BaseWorld.mEntityClassTypeParentMap)
这里没有放完整代码,但BaseWorld核心是实现类似tiny-ecs里tiny的作用(统一规范System和Entity的添加删除以及生命周期流程)
感兴趣的欢迎去地图编辑器对这套ECS的实战使用:
这里放个实战效果图:
Github
Reference
Entity component system introduction
Unity DOTS:入门简介(ECS,Burst Complier,JobSystem)