文章目录
  1. 1. 前言
  2. 2. ECS
    1. 2.1. Entity
    2. 2.2. Component
    3. 2.3. System
    4. 2.4. World
    5. 2.5. ECS设计
  3. 3. tiny-ecs
  4. 4. 实战
    1. 4.1. Entity设计
    2. 4.2. Component设计
    3. 4.3. System设计
    4. 4.4. World设计
  5. 5. Github
  6. 6. Reference

前言

伴随着守望先锋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的一张架构设计示意图:

ECSDesignPatternPreview

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:

  1. 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的一对多设计。

设计要点:

  1. tiny-ecs里的System和Entity的添加移除都统一到下一帧去处理,这样有效避免了System和Entity在单帧里数量动态变化的访问问题。

疑问点:

  1. 因为System和Entity都是下一帧才处理添加移除,那么System和Entity从逻辑上来说单帧没有立刻添加到World里,假设上一行代码写添加Entity,下一行去获取这个Entity是得不到的,System同理,或许这也是tiny-ecs没有设计直接获取Entity和System接口的原因吧。
  2. tiny-ecs并没有设计获取特定Entity相关的接口,也没有设计获取System的接口,那么在一个System里如果想访问特定Entity对象时,我们并没有方法可以获取到,这种情况应该如何处理了?(个人觉得应该暴露出获取特定符合条件Entity相关的接口)

实战

了解了ECS基础概念和tiny-ecs的核心设计,接下来实战设计一版简易的C# ECS框架,重点要实现以下几个重要概念:

  1. 实现Entity唯一对象的概念
  2. 实现Entity由Component组成的概念
  3. Component的组合决定了Entity有哪些功能
  4. 实现Component只定义数据,System负责逻辑处理的概念
  5. 实现System更新逻辑支持过滤Component类型概念
  6. 实现System支持获取特定Component类型的Entity
  7. 实现同一个System可以有无数个相同类型的Entity
  8. 实现同一个Entity相同类型Component可以有多个
  9. 实现同一个World相同类型System只能有一个
  10. 实现World对Entity和System的统一添加和删除流程
  11. 相同World名字只能同时存在一个
  12. 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

/// <summary>
/// BaseEntity.cs
/// Entity基类(逻辑对象抽象,相当于ECS里的E部分)
public abstract class BaseEntity : IRecycle
{
/// <summary>
/// Entity Uuid
/// </summary>
public int Uuid
{
get;
private set;
}

/// <summary>
/// Entity类型信息
/// </summary>
public Type ClassType
{
get
{
if(mClassType == null)
{
mClassType = GetType();
}
return mClassType;
}
}
protected Type mClassType;

/// <summary>
/// 组件类型和组件列表映射Map
/// </summary>
private Dictionary<Type, List<BaseComponent>> mComponentTypeMap;

*******
}

上述的数据结构定义主要是针对以下几个需求:

  1. 实现Entity唯一对象的概念(Uuid)
  2. 实现Entity由Component组成的概念(mComponentTypeMap)
  3. 实现同一个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
/// <summary>
/// ECS裡Component基类抽象
/// </summary>
public abstract class BaseComponent : IRecycle
{
public BaseComponent()
{

}

/// <summary>
/// 出池
/// </summary>
public virtual void OnCreate()
{

}

/// <summary>
/// 入池
/// </summary>
public virtual void OnDispose()
{
ResetDatas();
}

/// <summary>
/// 重置数据(子类重写)
/// </summary>
protected virtual void ResetDatas()
{

}
}

上述的数据结构定义主要是针对以下几个需求:

  1. 实现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
//// <summary>
/// BaseSystem.cs
/// 逻辑系统基类抽象(相当于ECS里的S部分)
/// </summary>
public abstract class BaseSystem
{
/// <summary>
/// 所属世界
/// </summary>
public BaseWorld OwnerWorld
{
get;
private set;
}

/// <summary>
/// 系统类型信息
/// </summary>
public Type ClassType
{
get
{
if(mClassType == null)
{
mClassType = GetType();
}
return mClassType;
}
}
protected Type mClassType;

/// <summary>
/// 系统系统激活
/// </summary>
public bool Enable
{
get;
set;
}

/// <summary>
/// 系统相关Entity列表
/// </summary>
public List<BaseEntity> SystemEntityList
{
get;
private set;
}

public BaseSystem()
{
SystemEntityList = new List<BaseEntity>();
}

/// <summary>
/// 初始化
/// </summary>
/// <param name="ownerWorld"></param>
public virtual void Init(BaseWorld ownerWorld)
{
OwnerWorld = ownerWorld;
}

/// <summary>
/// Entity过滤
/// </summary>
/// <param name="entity"></param>
/// <returns></returns>
public virtual bool Filter(BaseEntity entity)
{
return false;
}

******


/// <summary>
/// 添加所有事件
/// </summary>
public virtual void AddEvents()
{

}

/// <summary>
/// 响应系统添加到世界
/// </summary>
public virtual void OnAddToWorld()
{
Debug.Log($"世界名:{OwnerWorld.WorldName}的系统类型:{ClassType.Name}被添加到世界!");
}

/// <summary>
/// 响应Entity添加
/// </summary>
/// <param name="entity"></param>
public virtual void OnAdd(BaseEntity entity)
{

}

/// <summary>
/// 移除所有事件
/// </summary>
public virtual void RemoveEvents()
{

}

/// <summary>
/// 响应Entity移除
/// </summary>
/// <param name="entity"></param>
public virtual void OnRemove(BaseEntity entity)
{

}

/// <summary>
/// 响应系统从世界移除
/// </summary>
public virtual void OnRemoveFromWorld()
{
Debug.Log($"世界名:{OwnerWorld.WorldName}的系统类型:{ClassType.Name}被从世界移除!");
RemoveSystemAllEntity();
OwnerWorld = null;
mClassType = null;
Enable = false;
}

/// <summary>
/// PreUpdate
/// </summary>
/// <param name="deltaTime"></param>
public virtual void PreProcess(float deltaTime)
{

}

/// <summary>
/// Entity Update
/// </summary>
/// <param name="entity"></param>
/// <param name="deltaTime"></param>
public virtual void Process(BaseEntity entity, float deltaTime)
{

}

/// <summary>
/// PostUpdate
/// </summary>
/// <param name="deltaTime"></param>
public virtual void PostProcess(float deltaTime)
{

}

/// <summary>
/// LogicUpdate
/// </summary>
public virtual void LogicUpdate()
{

}

/// <summary>
/// FixedUpdate
/// </summary>
/// <param name="fixedDeltaTime"></param>
public virtual void FixedUpdate(float fixedDeltaTime)
{

}

/// <summary>
/// LateUpdate
/// </summary>
/// <param name="deltaTime"></param>
public virtual void LateUpdate(float deltaTime)
{

}
}

上述的数据结构定义主要是针对以下几个需求:

  1. System负责逻辑处理的概念(PreProcess,Process,PostProcess,LogicUpdate,FixedUpdate,LateUpdate等接口支持逻辑编写)
  2. 实现System更新逻辑支持过滤Component类型概念(Filter接口支持了自定义对Entity的过滤)
  3. 实现System支持获取特定Component类型的Entity(Filter接口支持了自定义对Entity的过滤)
  4. 实现同一个World相同类型System只能有一个(ClassType和OwnerWorld设计配合BaseWorld实现1对1)
  5. 实现同一个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
/// <summary>
/// BaseWorld.cs
/// 逻辑世界基类抽象(相当于tiny里的World)
/// </summary>
public abstract class BaseWorld
{
#region World成员定义部分开始
/// <summary>
/// 世界名(唯一ID)
/// </summary>
public string WorldName
{
get;
private set;
}

/// <summary>
/// 世界根节点GameObject
/// </summary>
protected GameObject mWorldRootGo;
#endregion

#region System成员定义部分开始
/// <summary>
/// 所有系统类型和系统Map<系统类型,系统>
/// </summary>
protected Dictionary<Type, BaseSystem> mAllSystemTypeAndSystemMap;

/// <summary>
/// 所有系统列表
/// Note:
/// 用于确保确定性更新顺序
/// </summary>
protected List<BaseSystem> mAllSystems;

/// <summary>
/// 等待添加的系统列表
/// </summary>
protected List<BaseSystem> mWaitAddSystems;

/// <summary>
/// 临时等待添加系统列表
/// </summary>
protected List<BaseSystem> mTempWaitAddSystems;

/// <summary>
/// 等待移除的系统列表
/// </summary>
protected List<BaseSystem> mWaitRemoveSystems;

/// <summary>
/// 临时等待移除系统列表
/// </summary>
protected List<BaseSystem> mTempWaitRemoveSystems;
#endregion

#region Entity成员定义开始
/// <summary>
/// 下一个Entity Uuid
/// </summary>
protected int mNextEntityUuid;

/// <summary>
/// Entity根节点GameObject
/// </summary>
protected GameObject mEntityRootGo;

/// <summary>
/// Entity类型信息父节点Map
/// </summary>
protected Dictionary<Type, Transform> mEntityClassTypeParentMap;

/// <summary>
/// Entity Uuid Map<Entitiy Uuid, Entity>
/// </summary>
protected Dictionary<int, BaseEntity> mEntityMap;

/// <summary>
/// 所有的Entity
/// Note:
/// 用于确保有序访问
/// </summary>
protected List<BaseEntity> mAllEntity;

/// <summary>
/// Entity类型信息和Entity列表Map<Entity类型信息, Entity列表>
/// </summary>
protected Dictionary<Type, List<BaseEntity>> mEntityTypeAndEntitiesMap;

/// <summary>
/// 等待添加的Entity列表
/// </summary>
protected List<BaseEntity> mWaitAddEntities;

/// <summary>
/// 临时等待添加Entity列表
/// </summary>
protected List<BaseEntity> mTempWaitAddEntities;

/// <summary>
/// 等待移除的Entity列表
/// </summary>
protected List<BaseEntity> mWaitRemoveEntities;

/// <summary>
/// 临时等待移除Entity列表
/// </summary>
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

/// <summary>
/// WorldManager.cs
/// 世界管理单例类(相当于Tiny)
/// </summary>
public class WorldManager : SingletonTemplate<WorldManager>
{
/// <summary>
/// 所有世界Map<世界名, 世界>
/// </summary>
private Dictionary<string, BaseWorld> mAllWorldMap;

/// <summary>
/// 所有世界列表
/// Note:
/// 用于确保确定性更新顺序
/// </summary>
private List<BaseWorld> mAllWorlds;

/// <summary>
/// 所有更新的世界名列表
/// </summary>
private List<string> mAllUpdateWorldNames;

public WorldManager()
{
mAllWorldMap = new Dictionary<string, BaseWorld>();
mAllWorlds = new List<BaseWorld>();
mAllUpdateWorldNames = new List<string>();
}

/// <summary>
/// Update
/// </summary>
/// <param name="deltaTime"></param>
public void Update(float deltaTime)
{
UpdateAllUpdateWorldNames();
foreach(var updateWorldName in mAllUpdateWorldNames)
{
var world = GetWorld<BaseWorld>(updateWorldName);
world?.Update(deltaTime);
}
}

/// <summary>
/// LogicUpdate
/// </summary>
public void LogicUpdate()
{
UpdateAllUpdateWorldNames();
foreach (var updateWorldName in mAllUpdateWorldNames)
{
var world = GetWorld<BaseWorld>(updateWorldName);
world?.LogicUpdate();
}
}

/// <summary>
/// FixedUpdate
/// </summary>
/// <param name="fixedDeltaTime"></param>
public void FixedUpdate(float fixedDeltaTime)
{
UpdateAllUpdateWorldNames();
foreach (var updateWorldName in mAllUpdateWorldNames)
{
var world = GetWorld<BaseWorld>(updateWorldName);
world?.FixedUpdate(fixedDeltaTime);
}
}


/// <summary>
/// LateUpdate
/// </summary>
/// <param name="deltaTime"></param>
public void LateUpdate(float deltaTime)
{
UpdateAllUpdateWorldNames();
foreach (var updateWorldName in mAllUpdateWorldNames)
{
var world = GetWorld<BaseWorld>(updateWorldName);
world?.LateUpdate(deltaTime);
}
}

/// <summary>
/// 创建指定类型和名字的世界
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="worldName"></param>
/// <param name="parameters"></param>
/// <returns></returns>
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;
}

******
}

上述的数据结构定义主要是针对以下几个需求:

  1. 实现同一个World相同类型System只能有一个(BaseWorld.mAllSystemTypeAndSystemMap)
  2. 实现World对Entity和System的统一添加和删除流程(BaseWorld里无论是System还是Entity都实现了统一延迟添加和删除的逻辑,详情参考ManagerSystems()和ManagerEntities())
  3. 相同World名字只能同时存在一个(WorldManager.mAllWorldMap)
  4. World实现对Enity实体对象挂在父节点的统一(BaseWorld.mEntityClassTypeParentMap)

这里没有放完整代码,但BaseWorld核心是实现类似tiny-ecs里tiny的作用(统一规范System和Entity的添加删除以及生命周期流程)

感兴趣的欢迎去地图编辑器对这套ECS的实战使用:

MapEditor

这里放个实战效果图:

DynamicCreateMapData1

Github

MapEditor

Reference

Entity component system

Entity component system introduction

tiny-ecs

游戏开发中的ECS 架构概述

Unity DOTS:入门简介(ECS,Burst Complier,JobSystem)

文章目录
  1. 1. 前言
  2. 2. ECS
    1. 2.1. Entity
    2. 2.2. Component
    3. 2.3. System
    4. 2.4. World
    5. 2.5. ECS设计
  3. 3. tiny-ecs
  4. 4. 实战
    1. 4.1. Entity设计
    2. 4.2. Component设计
    3. 4.3. System设计
    4. 4.4. World设计
  5. 5. Github
  6. 6. Reference