ECS学习笔记(1)
关于ECS框架
在ECS中每个基本单位都是一个实体,一个实体由N个组件组成。然后拥有相同组件的实体会被特定的系统处理特定的逻辑。
E(ntity)——C(omponent)——S(ystem)
Entity 实体,组件的载体本身并无意义,拥有什么功能完全取决于拥有什么组件。但是可以通过在游戏中增加或删除组件来改变实体的行为。
Component 组件,包含了代表其对应特性的数据,所以组件中没有任何方法。
System 系统,用来处理一个或多个具有相同组件的实体,即系统中没有任何数据。
Jenny设置面板
Project Path C#项目工程文件路径。
Target Directory 自动生成代码的路径。
Assemblies 引用程序集的路径,这里的默认值也是Unity自动引用的路径,除非自己修改过。
Contexts 需要生成的上下文,后面会介绍上下文。
Ignore Namespaces 生成的代码文件名称是否忽略命名空间。
Entity实体
实体作为ECS的三大基本概念之一,实体作为组件的载体,本身并无实际意义,最核心的数据便是唯一ID,和Untiy的GameObject类似。在游戏世界中可以被创建,被销毁,可以添加组件,删除组件。
实体初始化
Entitas中的实体只能由Context的CreateEntity()方法创建。可以大概看下源码:
1 | //判断对象池是否有空闲对象 |
实体的销毁
实体销毁是调用Destroy()方法,但是这个方法并不是真正执行销毁逻辑,Context内部提供了用来管理Entity的对象池,调用Destroy()方法时只是将实体身上的组件,委托事件等移除然后放入对象池等待下次使用:
1 | public void Destroy() { |
Context 上下文
Context是Entity的上下文环境,用来管理当前环境下的所有Entity和Group的创建和回收。可以同时存在多个Context,Entitas默认会生成两个Context,分别是Game,Input。可以在Jenny的设置面板修改Context。
注意:设置面板中的第一个Context将会被作为默认的,会对之后生成组件产生影响,之后详细讲解。
生成的Context代码存放在Generated文件夹下。在生成Context时同时会生成Lookup类和一个Matcher类。
- Lookup 用来管理当前Context的所有组件组件信息。
1
2
3
4
5
6
7
8
9
10
11
12public static class InputComponentsLookup
{
public const int TotalComponents = 0;
public static readonly string[] componentNames = {
};
public static readonly System.Type[] componentTypes = {
};
} - Matcher 用来定义当前Context中的实体的筛选条件同时存在的多个Context每个之间互不影响。所有的Context由Contexts管理。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18public sealed partial class InputMatcher {
public static Entitas.IAllOfMatcher<InputEntity> AllOf(params int[] indices) {
return Entitas.Matcher<InputEntity>.AllOf(indices);
}
public static Entitas.IAllOfMatcher<InputEntity> AllOf(params Entitas.IMatcher<InputEntity>[] matchers) {
return Entitas.Matcher<InputEntity>.AllOf(matchers);
}
public static Entitas.IAnyOfMatcher<InputEntity> AnyOf(params int[] indices) {
return Entitas.Matcher<InputEntity>.AnyOf(indices);
}
public static Entitas.IAnyOfMatcher<InputEntity> AnyOf(params Entitas.IMatcher<InputEntity>[] matchers) {
return Entitas.Matcher<InputEntity>.AnyOf(matchers);
}
}Contexts
Contexts是个单例。通过Contexts.sharedInstance访问,内部持有所有Context的引用。是由Entitas代码生成器生成,无需手动实现也不能够手动实现。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24public partial class Contexts : Entitas.IContexts {
public static Contexts sharedInstance {
get {
if (_sharedInstance == null) {
_sharedInstance = new Contexts();
}
return _sharedInstance;
}
set { _sharedInstance = value; }
}
static Contexts _sharedInstance;
public GameContext game { get; set; }
public InputContext input { get; set; }
public Entitas.IContext[] allContexts { get { return new Entitas.IContext [] { game, input }; } }
public Contexts() {
game = new GameContext();
input = new InputContext();
}
}
Component组件
组件是ECS框架中基础的数据结构单元。
每个Compoent只有数据,不包含任何处理数据的方法。
在内存中相同类型的组件是紧密排列的,这样在System中遍历拥有相同组件的实体时大大的提高内存命中率。
这也是ECS框架用来做很多大型项目或者多物体的项目的原因之一。
Entitas中所有组件全部继承自IComponent接口。同时Entitas提供了各种特殊标签属性,在生成代码的时候为我们提供特殊功能。
例如 [Context] 指定将该组件添加一个或者多个Context中。
我们定义的组件在生成的代码的时候会自动生成所属的Context文件夹下的Components文件夹中。
一个Context可以包含多个组件,自动生成时实际上个是通过partial(不完全类)的方式为当前Context下的Entity提供相关接口。如果一个组件被添加到多个Context中,则每个Context下都会有该组件。
前面说过组件内部只有数据,但是我们创建组件的时候也可以不包含数据。如果包含任意类型的数据,生成代码时会自动帮我们定义 has,Add,Replace,Remove四个接口,如果没有任何数据则只会给我们提供一个bool的属性。
例 :
我们定义如下两个接口:
1 | using Entitas; |
组件一自动生成代码:
1 | public partial class GameEntity { |
组件二自动生成代码:
1 | public partial class GameEntity { |
添加组件
一个Entity在被调用CreateEntity()方法创建出来时没有任何组件信息,我们需要在合适的时机增加,删除或者修改组件来改变Entity的行为。
- 对于没有数据的组件我们调用bool型的属性来添加或者移除该组件
true 添加组件
false 移除移除 - 对于有数据的组组件
Add 添加组件
Replace 替换组件
Remove 移除移除
System
System是一个单纯的逻辑处理类,在特定的时间执行系统内部的逻辑,这些逻辑中可以改变Entity上Component的数据和状态, 原则上来说应该是只有逻辑没有数据。
Entitas给我们提供了五种系统类型:
- IInitializeSystem
- IExecuteSystem
- ICleanupSystem
- ITearDownSystem
- ReactiveSystem
一般来说一个System只会用来处理一种逻辑,每个System之间相互也不需要相互调用(独立减少耦合)。完全是通过框架外部或者Entity上Component的数据来驱动。Systems
一个System一般只用来处理一种逻辑,游戏中会有很多个System。那么就需要用到Systems来管理这些System。
Systems内部维护了4个不同的List来保存不同类型的System。
1 | public Systems() { |
我们需要通过Add(ISystem system)将System添加到Systems中。但是不用管System会被添加到那个List中,Entitas会自动帮我们处理。
1 | //将不同的System添加到对应的列表中 |
Systems继承了IInitializeSystem, IExecuteSystem, ICleanupSystem, ITearDownSystem,并在内部实现了Cleanup(),Execute(),Initialize(),TearDown()等接口用来执行4个List中的System。
所以我们需要只要再外部调用Systems的这4个接口即可调用了内部的所有System。
1 | //驱动初始化系统执行Initialize()方法 |
Feature
在实际开发过程中我们可能需要知道当前正在运行的有哪些System等调试信息,Entitas会为我们自动生成Feature这个类来帮我们调试。
Feature主要用于在编辑器模式下开启visual debugging时收集各个系统的数据,同时在Unity中展示。
所以在开启visual debugging时Feature继承自DebugSystems,而DebugSystems又是继承自Systems,并在内部做一些数据收集的工作与展示的工作。
当关闭visual debugging时Feature会直接继承自Systems。
visual debugging的开关在 菜单栏Tools->Entitas->Preferences
Matcher Collector
Group
一个Context中可能会同时存在很多个Entity,但是有些时候我们只需要处理某些Entity,那么我们可以通过Group来快速访问,
每个Context内部维护一个Group对象集合,调用GetGroup()方法可以拿到Group,相同的Matcher参数拿到的是相同得Group对象。
Group是实时更新的一个具有相同筛选条件的Entity的组合,它会自动添加具有筛选条件的Entity,并删除失去筛选条件的Entity,以提高下一次的访问速度。
1 | public IGroup<TEntity> GetGroup(IMatcher<TEntity> matcher) |
可以通过GetEntities()拿到Group中得所有Entity。通过ContainsEntity()接口判断Group中是否包含某个Entity。
Gorup提供了OnEntityAdded,OnEntityRemoved,OnEntityUpdated事件委托监听Group内部得Entity变化。
1 | gameContext.GetGroup(GameMatcher.Position).OnEntityAdded += (group, entity, index, component) => { |
Matcher
Matcher是Group的筛选条件,筛选条件就是Context中所拥有的Component。
例:
我们有MoveComponent和AttackComponent两个组件存在Game Context中。则我们可以有以下几种查询方式:
1 | //查找所有有MoveComponent的Entity |
Collector
通过Group和Matcher已经可以拿到所有符合条件的Entity,例如所有具有MoveComponent的Entity。
也说过了Group会实时更新,但是如果我们需要知道Group中当前哪些Entity是新加进来的。则可以通过Collector来找到。
例:
1 | //查找所有有MoveComponent的Entity |
GroupEvent
1 | + Added 添加到Gorup中的 |