概念
在System中,我们就可以对Component中的数据进行处理了,例如我们每帧改变Translation Component的值,就可以使Entity移动起来。
ECS为我们提供了以下几种System类型,通常情况下,若我们要自己编写一个System用来处理Component数据的话,只需要继承SystemBase即可。其他的System类型用于提供一些特殊的功能,ECS已经为我们提供了几个EntityCommandBufferSystem和ComponentSystemGroup的实例,一般情况下使用它们即可。
SystemBase | 继承该类就可以实现一个System的创建 |
EntityCommandBufferSystem | 用于解决Sync points的问题,在下一篇会讲解到 |
ComponentSystemGroup | 用于将其他的System进行层次结构的划分以及排序,ECS提供了几个默认的Component System Group |
GameObjectConversionSystem |
注:ComponentSystem和JobComponentSystem两个类以及它们的IJobForEach接口,将会被DOTS弃用。请使用SystemBase类与对应的Entities.ForEach接口代替。
ECS会自动找到项目中所有的System,并在运行时实例化它们,并将它们添加进一个默认的组里面。
创建一个System(SystemBase)
我们可以通过继承抽象类SystemBase来创建System(Entity和Component都是struct类型,System为class类型)。同时还必须要重写其OnUpdate方法,而其他的基类方法可以在我们需要的时候去重写。
System就和我们平常使用的MonoBehaviour十分的相像,它也有相应的生命周期,执行顺序如下图
OnCreate | System被创建的时候调用 |
OnStartRunning | 在第一次OnUpdate之前和System恢复运行的时候调用 |
OnUpdate | System的Enabled为true时,每帧调用 |
OnStopRunning | System的Enabled为false时,或者没有找到相对应的Entity会调用,OnDestroy前也会调用 |
OnDestroy | System被销毁的时候调用 |
一个System Group下有多个System,System的OnUpdate方法是由System Group的OnUpdate方法调用的,若我们设置System Group的Enabled属性,同时会影响到该Group下所有的System的Enabled属性。(有关System Group的知识后面会讲解到)
System所有的事件都是在主线程上执行的,理想的情况下,我们OnUpdate里的逻辑应该使用Job来进行多线程处理,从而提高效率。我们可以使用下面方法来在System中调度Job
Entities.ForEach | 迭代ECS Component数据的最简单方法 |
Job.WithCode | 单独执行一个lambda方法,后台job |
IJobChunk | 根据一个个Chunk去查找Component中的数据 |
C# Job System | 自己编写Job,实例化并调度 |
注:不太了解Job System的可以参考下之前的文章:https://blog.csdn.net/wangjiangrong/article/details/107020789
我们可以在Entity Debugger中查看所有的System以及对应的Group信息等
Entities.ForEach
Entities.ForEach方法由SystemBase提供,它让我们很简单的就实现对Entity上Component的逻辑处理。Entities.ForEach的参数为一个lambda方法,其参数即为Entity的查询条件,查找到所有符合条件的Entity后会执行其方法体。
对于要执行的lambda方法,我们可以使用Schedule或者ScheduleParallel方法来调度,也可使用Run方法来在主线程上立即调用。
例如下面例子,ApplyVelocitySystem会根据我们设置的参数查询所有带有Translation和Velocity两个组件的Entity,然后每帧将Velocity组件的值加到Translation组件的值上。
class ApplyVelocitySystem : SystemBase
{
protected override void OnUpdate()
{
Entities.ForEach((ref Translation translation, in Velocity velocity) =>
{
translation.Value += velocity.Value;
}).Schedule();
}
}
注:如果我们要修改一个组件的值需要添加 ref 关键字,若只是读取一个组件的值,则使用 in 关键字,这样可以提高性能。
查询规则
前面我们说到会根据lambda表达式的参数去查询Entity,除此之外我们还可以使用下面几个方法来更详细的制定查询规则(类似于EntityQueryDesc)
WithAll | 除了包含lambda表达式参数的组件外,还要包含WithAll中所有的组件 |
WithAny | 至少包含WithAny中的一个组件 |
WithNone | 不能包含WithNone中的组件 |
看个例子:
Entities.WithAll<LocalToWorld>()
.WithAny<Rotation, Translation, Scale>()
.WithNone<LocalToParent>()
.ForEach((ref Destination outputData, in Source inputData) =>
{
}).Schedule();
就很好理解了,查询规则为,必须含有LocalToWorld,Destination和Souce组件,至少含有Rotation,Translation和Scale三个组件中的一个,不能含有LocalToParent组件。
若我们的ForEach中包含了Share Component,那么必须调用WithoutBurst方法和使用Run来执行,例如
Entities
.WithoutBurst()
.ForEach((ref Translation translation, in RenderMesh mesh) =>
{
}).Run();
获取EntityQuery
注:EntityQuery 的具体介绍可以查看前面介绍Entity的文章
我们可以通过 WithStoreEntityQueryInField(ref query) 方法来获取到EntityQuery对象来供我们使用,例如使用 EntityQuery.CalculateEntityCount() 方法可以知道查询到的Entity数量。
private EntityQuery query;
protected override void OnUpdate()
{
int dataCount = query.CalculateEntityCount();
Entities
.WithStoreEntityQueryInField(ref query)
.ForEach((in Translation data) =>
{
}).ScheduleParallel();
}
Filter
我们可以利用 WithChangeFilter<T>() 和 WithSharedComponentFilter() 方法来设置filter,如下
Entities
.WithChangeFilter<Translation>()
.WithSharedComponentFilter(new RenderMesh(){material = cubeMaterial})
.WithoutBurst()
.ForEach((ref Translation translation, in RenderMesh mesh) =>
{
translation.Value += new float3(0, time, 0);
}).Run();
System中会找到含有Translation 和 RenderMesh 两个组件,并且RenderMesh的material值为cubeMaterial,同时在别的System中Translation的值发生了变化的Entity。
注:若Entity中的Component被赋予了写的权限,那么这个Component的值就会被标记为发生了变化,即使它的值没有改变。
lambda方法参数定义
前面讲到Entities.ForEach的参数是一个lambda方法,而lambda方法的参数可以作为我们的查询条件,那么这些参数具体有哪些规则呢?
我们最多可以设置八个参数,规则如下:
- 最前面的为按值传递的参数(一些ECS定义好的特殊参数)
- 其次是可写的参数(ref Component)
- 最后为只读的参数(in Component)
代码示例:
Entities.ForEach(
(Entity entity,
int entityInQueryIndex,
ref Translation translation,
in Movement move) => {/* .. */})
特殊参数:
Entity entity | 当前的Entity对象(参数类型必须为Entity,参数名可以自定义) |
int entityInQueryIndex | entity在队列中的下标(参数名固定) |
int nativeThreadIndex | 执行当前操作的线程唯一下标(参数名固定) |
Job.WithCode
Job.WithCode由SystemBase提供,可以轻松的实现一个在后台运行的Job。我们也可以使其在主线程执行和使用Burst编译来提高执行效率。
public class TestSystem : SystemBase
{
protected override void OnUpdate()
{
NativeArray<float> randomNumbers = new NativeArray<float>(5, Allocator.TempJob);
Job.WithCode(() =>
{
for (int i = 0; i < randomNumbers.Length; i++)
randomNumbers[i] = i;
}).Schedule();
randomNumbers.Dispose();
}
}
本质上就是把编写一个Job并实例化的过程封装在了 Job.WithCode 中了。
IJobChunk Job(推荐使用)
IJobChunk其实是和JobSystem中IJob,IJobFor等一样的,ECS拓展的一个用于实现Job的接口。和其他的Job接口一样,需要实现一个Execute方法,IJobChunk的Execute方法如下:
void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex);
来看下这三个参数:
ArchetypeChunk chunk:在我们执行实现了IJobChunk的Job的时候,需要传入一个EntityQuery参数。而Entity根据其Archetype存放在不同的Chunk中,这个参数代表的就是这些Chunk。例如我们传入的EntityQuery中含有M个Entity,这些Entity分部在N个Chunk中,那个这个Execute方法会在每次调度Job的时候调用N次,这个参数即跟这些Chunk信息一一对应。
int chunkIndex:该Chunk的下标
int firstEntityIndex:该Chunk中第一个Entity的下标
实现IJobChunk的Job
在Execute方法中,我们可以利用 chunk.GetNativeArray(ArchetypeChunkComponentType type) 来获取到该Chunk下符合条件的所有Entity,返回的是一个NativeArray数组。Entity数量除了使用NativeArray.Length外还可使用chunk.count来获取。
在Execute方法外,我们除了定义一些需要的参数以为,还需要定义ArchetypeChunkComponentType<T>类型的参数用于获取数据,T即为Component类型。
例如我们要实现一个每帧移动带有Translation组件的Entity的功能,那么Job可以这么写
public struct CubeMoveJob : IJobChunk
{
public float deltaTime;
public ArchetypeChunkComponentType<Translation> translationType;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
var translationArray = chunk.GetNativeArray(translationType);
for (var i = 0; i < chunk.Count; i++)
{
var translation = translationArray[i];
translationArray[i] = new Translation(){Value = new float3(translation.Value.x, translation.Value.y + deltaTime, translation.Value.z)};
}
}
}
我们可以定义多个ArchetypeChunkComponentType,若有些Component只需要读取其数据不需要修改的话,记得在该ArchetypeChunkComponentType前面添加[ReadOnly]标签来提升性能。
如果我们在获取EntityQuery的时候用到了Any属性,例如
var desc = new EntityQueryDesc
{
Any = new ComponentType[] {typeof(Translation), typeof(NonUniformScale)},
};
query = GetEntityQuery(desc);
那么在我们的Execute方法中,Chunk内的Entity可能只拥有其中一个Component,我们可以利用 chunk.Has(ArchetypeChunkComponentType) 方法来判断是否含有对应Component,例如对上面的Job的Execute方法体进行修改:
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
if(!chunk.Has(translationType)) return;//新增
var translationArray = chunk.GetNativeArray(translationType);
......
}
使用Job
和普通的Job一样,我们需要实例化Job,然后设置对应的参数,最后调度即可。
我们可以利用SystemBase.GetArchetypeChunkComponentType<T>(bool isReadOnly)方法来获取到对应Component(T)的 ArchetypeChunkComponentType属性,其参数用于标记是否是只读的。
在调度Job的时候需要传入EntityQuery和SystemBase提供的Dependency
System的代码如下,即可实现利用IJobChunk来处理数据
public class CubeMoveSystem : SystemBase
{
EntityQuery query;
protected override void OnStartRunning()
{
base.OnStartRunning();
query = GetEntityQuery(typeof(Translation));
}
protected override void OnUpdate()
{
float time = Time.DeltaTime;
var job = new CubeMoveJob() {
translationType = GetArchetypeChunkComponentType<Translation>(),
deltaTime = time
};
Dependency = job.ScheduleParallel(query, Dependency);
}
}
实现类似ChangeFilter的功能
在某些情况下我们可能只需要处理修改过的Component,而不需要处理所有的。根据前面的知识,我们可以通过 EntityQuery.SetChangedVersionFilter() 进行过滤。但是此方法最多只支持过滤两个Component,若有时候我们需要过滤更多未改动的Component时,需要怎么办呢?
我们可以利用 ArchetypeChunk.DidChange(ArchetypeChunkComponentType type, uint lastSystemVersion) 方法来判断某个Component是否发生了修改。SystemBase为我们提供了LastSystemVersion属性,我们需要将这个值传递给Job,才能正确的做出判断。
简单的实现如下:
public struct CubeMoveJob : IJobChunk
{
......
public uint lastSystemVersion;//新增
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
//新增
bool isChange = chunk.DidChange(translationType, lastSystemVersion);
if(!isChange) return;
//原有逻辑
......
}
}
public class CubeMoveSystem : SystemBase
{
......
protected override void OnUpdate()
{
......
var job = new CubeMoveJob() {
translationType = GetArchetypeChunkComponentType<Translation>(),
deltaTime = time,
lastSystemVersion = LastSystemVersion//新增
};
......
}
}
这样若我们需要判断多个Component的时候,只需要调用多次DidChange方法并对比结果即可。
C# Job System
除了使用IJobChunk外,我们也可以直接使用普通的Job来实现。原理差不多就不做过多介绍了,直接看代码
public class CubeMoveSystem : SystemBase
{
[BurstCompile]
struct RotationSpeedJob : IJobParallelFor
{
[DeallocateOnJobCompletion] public NativeArray<ArchetypeChunk> chunks;
public ArchetypeChunkComponentType<Translation> translationType;
public float deltaTime;
public void Execute(int chunkIndex)
{
var chunk = chunks[chunkIndex];
var translationArray = chunk.GetNativeArray(translationType);
for (int i = 0; i < chunk.Count; i++)
{
var translation = translationArray[i];
translationArray[i] = new Translation(){Value = new float3(translation.Value.x, translation.Value.y + deltaTime, translation.Value.z)};
}
}
}
EntityQuery query;
protected override void OnStartRunning()
{
query = GetEntityQuery(typeof(Translation));
}
protected override void OnUpdate()
{
var chunks = query.CreateArchetypeChunkArray(Allocator.TempJob);
var rotationsSpeedJob = new RotationSpeedJob
{
chunks = chunks,
translationType = GetArchetypeChunkComponentType<Translation>(),
deltaTime = Time.DeltaTime,
};
Dependency = rotationsSpeedJob.Schedule(chunks.Length, 32, Dependency);
}
}
System的层次结构以及执行顺序(ComponentSystemGroup)
在前面的例子中,我们只需要编写一个System加好相关的逻辑后,运行的时候该System就会被执行。原来ECS会在运行时自动找到我们项目中的System,实例化它们并将其添加到默认的System Group中。这些System Group其实就是继承于我们的ComponentSystemGroup。
ComponentSystemGroup
ComponentSystemGroup类和SystemBase类一样都是ComponentSystemBase的子类,因此它也和SystemBase类似,有着OnUpdate等方法。ComponentSystemGroup中有一个List用于存放相关ComponentSystemBase类,然后在OnUpdate方法中遍历这个List调用它们的Update方法。所以我们System的Update方法就是在这些ComponentSystemGroup中被调用的。
由于ComponentSystemGroup也属于ComponentSystemBase类,因此可以形成嵌套的关系,例如ComponentSystemGroup A在ComponentSystemGroup B的List中,当A在B中被调用了Update之后,A中List中的数据也会跟着被调用Update。(大家可以看下源码,可以更好的理解)
在实际开发中,我们往往需要一个System在另一个System之后执行,来保证逻辑的正确,说白了就是ComponentSystemGroup中List的排序问题。ECS为我们提供 [UpdateBefore] and [UpdateAfter] 两个Attribute,可以让我们很轻松的解决System的排序问题。
一些常用的Attribute
[UpdateBefore] 和 [UpdateAfter]
字面意思,使一个System的Update方法在另一个System的前或者后执行(当然两个System需要在同一个Group中),在声明System Class的时候使用。例如:
public class aa : SystemBase
{
protected override void OnUpdate()
{
}
}
[UpdateBefore(typeof(aa))]
public class bb : SystemBase
{
protected override void OnUpdate()
{
}
}
这样就可以保证bb的OnUpdate在aa之前执行。
需要注意的是除了OnUpdate外,也会影响OnStartRunning和OnStopRunning的执行顺序(可以看下SystemBase的Update方法)但是不会影响OnCreate的执行顺序。
同时这两个Attribute还可以作用于ComponentSystemGroup,设置整个Group中所有System和另个Group中所有System的Update先后执行顺序。
[UpdateInGroup]
设置System归属的ComponentSystemGroup,如果不设置,ECS会将该System加到默认World的SimulationSystemGroup中。
[DisableAutoCreation]
前面说到我们的ECS会找到所有的System,实例化它们并将其添加到默认的Group中,设置此标签就可以阻止这一步操作。
默认的System Group
ECS在默认的World中已经为我们创建了一些ComponentSystemGroup实例,我们可以利用Entity Debugger看见这些Group,如下
Group和System的层次结构可以清晰明了,我们的自定义的System(如图中的CubeMoveSystem)也被添加在了SimulationSystemGroup当中
补充
System.Enabled
我们可以通过World获取到System,然后通过设置其Enable属性来控制System是否启用,如下
CustomSystem system = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<CustomSystem>();
system.Enabled = false;
Profiler
我们可以通过Window->Analysis->Profiler打开Profiler窗口,在CPU Usage栏中可以查看到我们CPU的使用情况。