前言
伴随着守望先锋ECS概念的剔除,以及Unity DOTS里ECS的普及,ECS架构设计被广泛运用到了游戏开发行业,本章节核心是想学习ECS设计理念而非Unity DOTS一整套。学习理解ECS设计理念后,编写一套属于自己的简易版ECS用于常规游戏功能开发框架。
ECS
首先看一下维基百科的基础介绍:
Entity–component–system (ECS) is a software architectural pattern mostly used in video game development for the representation of game world objects. An ECS comprises entities composed from components of data, with systems which operate on the components.
ECS follows the principle of composition over inheritance, meaning that every entity is defined not by a type hierarchy, but by the components that are associated with it. Systems act globally over all entities which have the required components.
大概翻译一下就是:
ECS是一种用在游戏开发的架构设计模式。ECS由Entity,Component,System组成。Entity由Component组成,Component由数据组成,System负责处理Enity身上的Component数据。
ECS遵循组合大于继承的设计理念,System负责处理所有符合Component要求的Entity逻辑处理。
放一张Unity ECS的一张架构设计示意图:

Entity
An entity represents something discrete in your program that has its own set of data, such as a character, visual effect, UI element, or even something abstract like a network transaction. However, an entity acts as an ID which associates individual unique components together, rather than containing any code or serving as a container for its associated components.
大概理解就是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之间的关系:
实体(Entity)与组件(Component)是一个一对多的关系,实体(Entity)拥有怎样的能力,完全是取决于拥有哪些组件(Component),通过动态增加或删除组件,可以在(游戏)运行时改变实体的行为。
从上面可以看出,通过把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变化相关流程
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
|
public abstract class BaseEntity : IRecycle { public int Uuid { get; private set; }
public Type ClassType { get { if(mClassType == null) { mClassType = GetType(); } return mClassType; } } protected Type mClassType;
private Dictionary<Type, List<BaseComponent>> mComponentTypeMap;
******* }
|
上述的数据结构定义主要是针对以下几个需求:
- 实现Entity唯一对象的概念(Uuid)
- 实现Entity由Component组成的概念(mComponentTypeMap)
- 实现同一个Entity相同类型Component可以有多个(mComponentTypeMap)
上面没有展示Component相关的接口,详情直接参考源代码。
Component设计
BaseComponent.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
public abstract class BaseComponent : IRecycle { public BaseComponent() {
}
public virtual void OnCreate() {
}
public virtual void OnDispose() { ResetDatas(); }
protected virtual void ResetDatas() {
} }
|
上述的数据结构定义主要是针对以下几个需求:
- 实现Component只定义数据
可以看到BaseComponent唯一的接口就是逻辑对象的出池入池和数据重置接口(ResetDatas)流程,这也正是符合Component只有数据定义的设计。
System设计
BaseSystem.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184
|
public abstract class BaseSystem { public BaseWorld OwnerWorld { get; private set; }
public Type ClassType { get { if(mClassType == null) { mClassType = GetType(); } return mClassType; } } protected Type mClassType;
public bool Enable { get; set; }
public List<BaseEntity> SystemEntityList { get; private set; }
public BaseSystem() { SystemEntityList = new List<BaseEntity>(); }
public virtual void Init(BaseWorld ownerWorld) { OwnerWorld = ownerWorld; }
public virtual bool Filter(BaseEntity entity) { return false; } ****** public virtual void AddEvents() {
}
public virtual void OnAddToWorld() { Debug.Log($"世界名:{OwnerWorld.WorldName}的系统类型:{ClassType.Name}被添加到世界!"); }
public virtual void OnAdd(BaseEntity entity) {
}
public virtual void RemoveEvents() {
}
public virtual void OnRemove(BaseEntity entity) {
}
public virtual void OnRemoveFromWorld() { Debug.Log($"世界名:{OwnerWorld.WorldName}的系统类型:{ClassType.Name}被从世界移除!"); RemoveSystemAllEntity(); OwnerWorld = null; mClassType = null; Enable = false; }
public virtual void PreProcess(float deltaTime) {
}
public virtual void Process(BaseEntity entity, float deltaTime) {
}
public virtual void PostProcess(float deltaTime) {
}
public virtual void LogicUpdate() {
}
public virtual void FixedUpdate(float fixedDeltaTime) {
}
public virtual void LateUpdate(float deltaTime) {
} }
|
上述的数据结构定义主要是针对以下几个需求:
- 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112
|
public abstract class BaseWorld { #region World成员定义部分开始 public string WorldName { get; private set; }
protected GameObject mWorldRootGo; #endregion
#region System成员定义部分开始 protected Dictionary<Type, BaseSystem> mAllSystemTypeAndSystemMap;
protected List<BaseSystem> mAllSystems;
protected List<BaseSystem> mWaitAddSystems;
protected List<BaseSystem> mTempWaitAddSystems;
protected List<BaseSystem> mWaitRemoveSystems;
protected List<BaseSystem> mTempWaitRemoveSystems; #endregion
#region Entity成员定义开始 protected int mNextEntityUuid;
protected GameObject mEntityRootGo;
protected Dictionary<Type, Transform> mEntityClassTypeParentMap;
protected Dictionary<int, BaseEntity> mEntityMap;
protected List<BaseEntity> mAllEntity;
protected Dictionary<Type, List<BaseEntity>> mEntityTypeAndEntitiesMap;
protected List<BaseEntity> mWaitAddEntities;
protected List<BaseEntity> mTempWaitAddEntities;
protected List<BaseEntity> mWaitRemoveEntities;
protected List<BaseEntity> mTempWaitRemoveEntities; #endregion ****** }
|
WorldManager.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115
|
public class WorldManager : SingletonTemplate<WorldManager> { private Dictionary<string, BaseWorld> mAllWorldMap;
private List<BaseWorld> mAllWorlds;
private List<string> mAllUpdateWorldNames;
public WorldManager() { mAllWorldMap = new Dictionary<string, BaseWorld>(); mAllWorlds = new List<BaseWorld>(); mAllUpdateWorldNames = new List<string>(); }
public void Update(float deltaTime) { UpdateAllUpdateWorldNames(); foreach(var updateWorldName in mAllUpdateWorldNames) { var world = GetWorld<BaseWorld>(updateWorldName); world?.Update(deltaTime); } }
public void LogicUpdate() { UpdateAllUpdateWorldNames(); foreach (var updateWorldName in mAllUpdateWorldNames) { var world = GetWorld<BaseWorld>(updateWorldName); world?.LogicUpdate(); } }
public void FixedUpdate(float fixedDeltaTime) { UpdateAllUpdateWorldNames(); foreach (var updateWorldName in mAllUpdateWorldNames) { var world = GetWorld<BaseWorld>(updateWorldName); world?.FixedUpdate(fixedDeltaTime); } }
public void LateUpdate(float deltaTime) { UpdateAllUpdateWorldNames(); foreach (var updateWorldName in mAllUpdateWorldNames) { var world = GetWorld<BaseWorld>(updateWorldName); world?.LateUpdate(deltaTime); } }
public T CreateWrold<T>(string worldName, params object[] parameters) where T : BaseWorld, new() { var existWorld = GetWorld<T>(worldName); if(existWorld != null) { var worldType = typeof(T); Debug.LogError($"已存在世界类型:{worldType.Name}和世界名:{worldName}的世界,创建世界失败!"); return null; } var newWorld = new T(); newWorld.Init(worldName, parameters); var result = AddWorld(newWorld); if(result) { newWorld.OnCreate(); } return newWorld; } ****** }
|
上述的数据结构定义主要是针对以下几个需求:
- 实现同一个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的实战使用:
MapEditor
这里放个实战效果图:

Github
MapEditor
Reference
Entity component system
Entity component system introduction
tiny-ecs
游戏开发中的ECS 架构概述
Unity DOTS:入门简介(ECS,Burst Complier,JobSystem)