概念

在System中,我们就可以对Component中的数据进行处理了,例如我们每帧改变Translation Component的值,就可以使Entity移动起来。

ECS为我们提供了以下几种System类型,通常情况下,若我们要自己编写一个System用来处理Component数据的话,只需要继承SystemBase即可。其他的System类型用于提供一些特殊的功能,ECS已经为我们提供了几个EntityCommandBufferSystemComponentSystemGroup的实例,一般情况下使用它们即可。

SystemBase 继承该类就可以实现一个System的创建
EntityCommandBufferSystem 用于解决Sync points的问题,在下一篇会讲解到
ComponentSystemGroup 用于将其他的System进行层次结构的划分以及排序,ECS提供了几个默认的Component System Group
GameObjectConversionSystem  

注:ComponentSystemJobComponentSystem两个类以及它们的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方法的参数可以作为我们的查询条件,那么这些参数具体有哪些规则呢?

我们最多可以设置八个参数,规则如下:

  1. 最前面的为按值传递的参数(一些ECS定义好的特殊参数)
  2. 其次是可写的参数(ref Component)
  3. 最后为只读的参数(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外,也会影响OnStartRunningOnStopRunning的执行顺序(可以看下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的使用情况。

ECS的简单入门(四):System相关推荐

  1. ECS的简单入门(六):传统GameObject模式转换到ECS模式

    在前面的介绍中,我们都是通过代码来实现物体在场景中的显示,例如我们要显示一个Cube,通过创建Entity,添加RenderMesh等Component,设置相应的Material和Mesh值即可.这 ...

  2. ECS的简单入门(一):概念

    官方文档:https://docs.unity3d.com/Packages/com.unity.entities@0.9/manual/ecs_core.html 视频资料:UUG Online直播 ...

  3. ECS的简单入门(三):Component

    Component概念 ECS中的Component和我们以往MonoBehaviour的Component不同,ECS的Component中,只用于存放数据,并不会有行为.操纵数据的行为我们交给了S ...

  4. 【我的区块链之路】- Hyperledger fabric的简单入门(四)链码的编写及调试

    [我的区块链之路]- Hyperledger fabric的简单入门(四)链码的编写及调试       之前的文章中我们有讲过了fabric的一些核心的配置文件的选项说明,讲过fabric 的网络启动 ...

  5. 分布式锁简单入门以及三种实现方式介绍(滴滴)

    很多小伙伴在学习Java的时候,总是感觉Java多线程在实际的业务中很少使用,以至于不会花太多的时间去学习,技术债不断累积!等到了一定程度的时候对于与Java多线程相关的东西就很难理解,今天需要探讨的 ...

  6. Okio 1.9简单入门

    Okio 1.9简单入门 Okio库是由square公司开发的,补充了java.io和java.nio的不足,更加方便,快速的访问.存储和处理你的数据.而OkHttp的底层也使用该库作为支持. 该库极 ...

  7. [WinForm]Windows程序(非网页) #2 -- 跨平台的 ADO.NET程序(简单入门)

    这是我的文章备份,有空请到我的网站走走, http://www.dotblogs.com.tw/mis2000lab/ 才能掌握我提供的第一手信息,谢谢您. http://www.dotblogs.c ...

  8. 算法入门四:函数与函数体:

    文章目录 算法入门四:函数与函数体: 引言: 函数与函数体: 章节介绍: 1.洛谷 p5735 Java 距离函数 2.洛谷 p5737 Java 闰年展示 3.洛谷 p5739 Java 计算阶乘 ...

  9. 这就是你要找的Spring-ioc简单入门!

    Spring简单入门 - ioc 大二下学期了,发现身边的同学,工作室的下伙伴最近都准备考研,迷茫中不晓得怎么选择,学长考研复试中说这个CSDN写博客的阅读量是个加分点.之前学长一直叫我们写博客,记录 ...

最新文章

  1. html5自带表单验证-美化改造
  2. day002-HTML知识点总结:浏览器兼容性之指定IE浏览器使用chrome内核渲染页面
  3. 计算机目录读取,从项目目录中读取SQL查询文件(Read SQL query file from project directory)...
  4. 职场社交是一个真需求吗?
  5. javascript--弹出对话框 四种对话框 获得用户输入值 .
  6. IPM: Partner determination in Acquisition contract containing IP Product
  7. 商汤科技62篇论文入选CVPR 2019!多个竞赛项目夺冠!
  8. echo linux命令_Linux echo命令示例
  9. Struts2表单验证的xml配置
  10. android接推流sdk,Android-SDK推流端说明
  11. 【时间序列预测】基于matlab最小均方(LMS)算法时间序列预测【含Matlab源码 1335期】
  12. C#(winform)为button添加背景图片,并去掉各种边框
  13. 小白如何开始学习计算机编程?
  14. java平均数函数_java求平均数的函数
  15. 2018.11.27 元器件选型(1)- 蜂鸣器,电池,电阻,电容,电感
  16. NYOJ 小明的存钱计划
  17. ubuntu Pathon 目录
  18. Vue中分页组件的用法
  19. 618营销新动向:Swisse年轻化产品教育成电商战场新突破口!
  20. must be of typexxx解决方法

热门文章

  1. Mac中 wps如何将自定义模板添加到模板库中
  2. Unity资源文件创建对应的MD5
  3. 从一座瑞典风机的倒塌看VDI2230用于螺栓连接精确计算的重要性
  4. NXP JN5169使用滴答定时器进行精准延时
  5. js隐藏部分手机号码
  6. 解决virus:win32/ramnit.a病毒
  7. 达观智能RPA机器人助力完善医疗行业信息化建设,解放医疗资源
  8. 数字信号处理知识点总结(二):傅里叶级数与变换
  9. 【Windows Server 2019】组策略的配置与管理——配置基于本地的组策略
  10. 周报|UBTC项目7月份进展更新