ECS的简单入门(四):System
概念
在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的使用情况。
ECS的简单入门(四):System相关推荐
- ECS的简单入门(六):传统GameObject模式转换到ECS模式
在前面的介绍中,我们都是通过代码来实现物体在场景中的显示,例如我们要显示一个Cube,通过创建Entity,添加RenderMesh等Component,设置相应的Material和Mesh值即可.这 ...
- ECS的简单入门(一):概念
官方文档:https://docs.unity3d.com/Packages/com.unity.entities@0.9/manual/ecs_core.html 视频资料:UUG Online直播 ...
- ECS的简单入门(三):Component
Component概念 ECS中的Component和我们以往MonoBehaviour的Component不同,ECS的Component中,只用于存放数据,并不会有行为.操纵数据的行为我们交给了S ...
- 【我的区块链之路】- Hyperledger fabric的简单入门(四)链码的编写及调试
[我的区块链之路]- Hyperledger fabric的简单入门(四)链码的编写及调试 之前的文章中我们有讲过了fabric的一些核心的配置文件的选项说明,讲过fabric 的网络启动 ...
- 分布式锁简单入门以及三种实现方式介绍(滴滴)
很多小伙伴在学习Java的时候,总是感觉Java多线程在实际的业务中很少使用,以至于不会花太多的时间去学习,技术债不断累积!等到了一定程度的时候对于与Java多线程相关的东西就很难理解,今天需要探讨的 ...
- Okio 1.9简单入门
Okio 1.9简单入门 Okio库是由square公司开发的,补充了java.io和java.nio的不足,更加方便,快速的访问.存储和处理你的数据.而OkHttp的底层也使用该库作为支持. 该库极 ...
- [WinForm]Windows程序(非网页) #2 -- 跨平台的 ADO.NET程序(简单入门)
这是我的文章备份,有空请到我的网站走走, http://www.dotblogs.com.tw/mis2000lab/ 才能掌握我提供的第一手信息,谢谢您. http://www.dotblogs.c ...
- 算法入门四:函数与函数体:
文章目录 算法入门四:函数与函数体: 引言: 函数与函数体: 章节介绍: 1.洛谷 p5735 Java 距离函数 2.洛谷 p5737 Java 闰年展示 3.洛谷 p5739 Java 计算阶乘 ...
- 这就是你要找的Spring-ioc简单入门!
Spring简单入门 - ioc 大二下学期了,发现身边的同学,工作室的下伙伴最近都准备考研,迷茫中不晓得怎么选择,学长考研复试中说这个CSDN写博客的阅读量是个加分点.之前学长一直叫我们写博客,记录 ...
最新文章
- html5自带表单验证-美化改造
- day002-HTML知识点总结:浏览器兼容性之指定IE浏览器使用chrome内核渲染页面
- 计算机目录读取,从项目目录中读取SQL查询文件(Read SQL query file from project directory)...
- 职场社交是一个真需求吗?
- javascript--弹出对话框 四种对话框 获得用户输入值 .
- IPM: Partner determination in Acquisition contract containing IP Product
- 商汤科技62篇论文入选CVPR 2019!多个竞赛项目夺冠!
- echo linux命令_Linux echo命令示例
- Struts2表单验证的xml配置
- android接推流sdk,Android-SDK推流端说明
- 【时间序列预测】基于matlab最小均方(LMS)算法时间序列预测【含Matlab源码 1335期】
- C#(winform)为button添加背景图片,并去掉各种边框
- 小白如何开始学习计算机编程?
- java平均数函数_java求平均数的函数
- 2018.11.27 元器件选型(1)- 蜂鸣器,电池,电阻,电容,电感
- NYOJ 小明的存钱计划
- ubuntu Pathon 目录
- Vue中分页组件的用法
- 618营销新动向:Swisse年轻化产品教育成电商战场新突破口!
- must be of typexxx解决方法