Systems
System在ECS中执行逻辑,将component data从一个状态变换到下个状态,比如一个系统,可以对所有移动实体执行运算,根据其方向,速度,时间计算新的位置。
ECS支持多种不同的systems,你可以实现ComponentSystem和JobComponentSystem来为entities执行逻辑。这两种Systems可以容易地选择和遍历拥有指定components的entities。
系统提供时间类型的回调,像OnCreate()和OnUpdate(),可以实现相关系统生命周期的代码。这些方法是在主线程中调用的。在Job Component System中,你还可以在OnUpdate()中Schedule Jobs,Jobs本身是运行在工作线程(相对于主线程)中的。通常,Job Component Systems的性能更好,因为它能充分利用多核优势。如果Jobs以Burst编译,则能进一步提升性能。
ECS会自动发现System类,并在运行时创建。Systems被World以Group组织。你可以通过System Attributes控制System被添加到哪个Group,以及在该Group中它们以什么样的顺序执行。默认的,所有的systems被创建到default world的Simulation System Group中,但是为指定执行顺序。你可以同构system attribute禁止自动创建Systems。
系统的执行,由父Component System Group驱动。一个Component System Group本身也是个System,用来执行子Systems。
可以查看运行时System配置信息(菜单: Window > Analysis > Entity Debugger)。
System event functions
通过实现一组System的生命周期回调接口,来管理生命周期。ECS按照下面的顺序调用相关接口:
- OnCreate() 系统创建时调用
- OnStartRunning() 第一次调用OnUpdate之前,以及每次回复执行时调用
- OnUpdate() 当系统是Enabled状态,并且有任务时(参考ShouldRunSystem()),每帧调用。OnUpdate函数定义在基类ComponentSystemBase中,所有类型的system都可以实现自己的逻辑行为。
- OnStopRunning() 当System的Query没有匹配到entities,或者系统调用OnDestroy()前,被调用。
- OnDestroy() 系统销毁时调用。
以上函数都运行在主线程中。可以在JobComponentSystem类System的OnUpdate中调度Jobs来执行多线程任务。
System Types
ECS提供了几种Systems类型。实现游戏逻辑行为的系统,通常派生类ComponentSystem或者JobComponentSystem。其它的系统有特殊用途,比如可以用Command Buffer System 和 Component System Group类的实例。
- Component Systems 实现该基类,在主线程中执行游戏逻辑,也可以使用非ECS优化的Jobs(直接使用Job System)。
- Job Component Systems 实现该基类,可以使用IJobForEach或者IJobChunk来执行逻辑。
- Entity Command Buffer Systems 提供EntityCommandBuffer实例给其它系统使用。每个默认的system groups,在其子系统列表的头尾,都有一个EntityCommandBufferSystem。
- Component System Groups 为其它系统提供管理和更行顺序控制。默认ECS会创建几个Component System Groups。
Component Systems
Component System 执行entities的逻辑。它不能包含数据。可以看作老版本的Component,只是它只有方法,没有数据。一个Component System负责更新所有的匹配的entities(由EntityQuery进行匹配)。
ComponentSystem是个纯虚类,通过实现相关接口执行我们的逻辑。
JobComponentSystem
Automatic job dependency management
自动管理Job依赖。
管理Job的依赖是件很麻烦的事。而JobComponentSystem帮我们自动处理了。规则很简单:不同系统的Jobs可以并行地访问同样类型的IComponentData。但是如果有一个Job在向该数据写操作,则这些Jobs不能并行执行,必须根据依赖调度Jobs。
public class RotationSpeedSystem : JobComponentSystem
{
[BurstCompile]
struct RotationSpeedRotation : IJobForEach<Rotation, RotationSpeed>
{
public float dt;
public void Execute(ref Rotation rotation, [ReadOnly]ref RotationSpeed speed)
{
rotation.value = math.mul(math.normalize(rotation.value), quaternion.axisAngle(math.up(), speed.speed * dt));
}
}
// Any previously scheduled jobs reading/writing from Rotation or writing to RotationSpeed
// will automatically be included in the inputDeps dependency.
protected override JobHandle OnUpdate(JobHandle inputDeps)
{
var job = new RotationSpeedRotation() { dt = Time.deltaTime };
return job.Schedule(this, inputDeps);
}
}
How does this work?
所有这些Jobs和Systems声明了它们要读或写的ComponentType,因此当JobComponentSystem返回JobHandle时,它自动将对这些ComponentType的读写访问注册到EntityManager中。
因此当一个系统向A写操作,其它系统之后读取A,那么JobComponentSystem会检查列表中的读取操作,并将传递一个对第一个系统的依赖。
JobComponentSystem简单地将Jobs按照依赖关系连接到一起,简化了我们在主线程中的依赖关系管理。但是,如果一个非Job的ComponentSystem要访问这些数据呢?因为所有的数据访问时事先声明好的,因此ComponentSystem会在与其它有共同依赖的component的所有的jobs完成后,再调用它的OnUpdate。
Dependency management is conservative & deterministic
依赖管理是保守且确定的
依赖管理倾向于保守。ComponentSystem简单地跟踪用到过的EntityQuery对象并记录它们对组件类型的读写配置。
而且,当在一个System中调度多个jobs时,无论这些Jobs是否是依赖的,都要讲依赖对象传递给它们。如果证明这导致效率问题,那么尝试把该系统拆分成2个系统。
依赖管理被设计的保守,这样能够用简单的API保证逻辑的确定性和正确性。
Sync points
所有对结构的改变,都会导致“强制同步点”,如,CreateEntity,Instantiate,Destroy,AddComponent,RemoveComponent,SetSharedComponentData。这意味着这些调用发生时,之前通过JobComponentSystem调度的所有的Jobs都要完成。这是自动的。比如,在逻辑帧的执行中间,调用EntityManager.CreateEntity,会导致逻辑停下来,等待之前的Jobs 完成。
可以通过EntityCommandBuffer来避免实体创建类操作导致的Sync Points。
Multiple Worlds
每个World都有自己的EntityManager,以及互相独立的JobHandle依赖管理。一个World的Hard Sync Point,不会对另一个World产生影响。因此,对于序列化和创建对象,一个有用的建议是,在一个World中创建对象,创建好后,在一帧的开始将他们移动到另一个World中。
可以参考System update order 以及 ExclusiveEntityTransaction来避免实体创建和序列化导致的Sync Points。
Entity Command Buffer
EntityCommandBuffer解决了2个问题:
- 当你再Job中时,不可以访问EntityManager
- 当访问EntityManager时(比如,创建一个Entity)导致导致访问的Components数组失效,及EntityQuery对象失效。
Entity Command Buffer抽象允许我们将改变保存到队列(从Job或者主线程),然后在稍后在主线程中应用。有2中应用EntityCommandBuffer的方法:
ComponentSystem的子类PostUpdateCommands可以在主线程中执行。可以简单地调用方法来将改变队列化,当world执行完该系统的Update后,会立即应用它们。
例子:
PostUpdateCommands.CreateEntity(TwoStickBootstrap.BasicEnemyArchetype);
PostUpdateCommands.SetComponent(new Position2D{Value= spawnPosition});
PostUpdateCommands.SetComponent(new Heading2D{Value = new float2(0.0f,-10f)});
PostUpdateCommands.SetComponent(default(Enemy));
PostUpdateCommands.SetComponent(new Helath{Value=TwoStickBootstrap.Settings.enemyInitialHealth});
PostUpdateCommands.SetComponent(new EnemyShootState{Cooldown=0.5f});
PostUpdateCommands.SetComponent(new MoveSpeed(speed=TwoStickBootstrap.Setting.enemySpeed));
PostUpdateCommands.AddSharedComponent(TwoStickBootstrap.EnemyLook);
对Jobs来说,必须在主线程中从entity command buffer system请求EntityCommandBuffer,然后传递给jobs。当EntityCommandBufferSystem更新时,command buffer会在主线程中按照创建的顺序执行它们。
Entity Command Buffer Systems
默认World初始化时创建了3个system groups,用来初始化,模拟,显示,它们在每一帧顺序执行。在每个group中,有一个entity command buffer system,在groups内所有其它system执行前执行一次,并且在所有其它system执行完后再执行一次。我们建议使用已有的command buffer systems,而不是自己创建,这样可以最小化同步点数量(Command buffer system越多,Sync Points越多)。
Using EntityCommandBuffers from ParallelFor jobs
当在ParallelFor jobs中使用EntityCommandBuffer来执行EntityManager命令时,EntityCommandBuffer.Concurrent接口可以保证线程安全和执行的确定性。该方法需要一个额外的jobIndex作为参数,用来保证命令回放执行的顺序是确定的。JobIndex必须是每个job唯一的。为了提升效率,jobIndex应该是传递给IJobParallelFor.Execute()的自增的index值。除非你帧地了解它们的顺序关系,否则用Execute()的index 参数是最佳选择。使用其它的jobIndex即使正确执行了,也可能带来效率方面的其它问题。
System Update Order
通过Component System Groups来指定systems的执行顺序。可以通过在系统的类的定义上使用[UpdateInGroup] Attribute来指定该系统在哪个group中执行。还可以使用[UpdateBefore] [UpdateAfter]来指定在该Group内的执行顺序。
ECS框架创建了一组默认的system groups,可以利用这些groups来控制我们的系统的执行顺序。还可以将一个Group创建到另一个Group内,这样来进一步控制系统的执行顺序(Group是可以嵌套的)。
Component System Groups
ComponentSystemGroup类组织若干相关的Systems到一起,并以一定的顺序执行。ComponentSystemGroup是ComponentSystemBase的派生类,因此它具有一个componenty system应该有的特性,可以跟其它系统关联,有OnUpdate方法,等。最主要的,这意味着Component System Croups可以嵌套到其它Component System Group中,建立层次关系。
默认地,当ComponentSystemGroup的Update调用时,它会调用所有保存的系统列表里的系统的Update()。如果一个子Component System是group时,会递归调用它的子系统列表,按照深度优先遍历整棵层次树。
System Ordering Attributes
目前系统维护了一套顺序Attributes:
- [UpdateInGroup] — 为该系统指定是哪个group的成员。如果没有该修饰,默认会添加到World的SimulationSystemGroup中。
- [UpdateBefore] [UpdateAfter] — 控制组内系统的相对执行顺序。使用这些修饰的系统类型,必须在同一个group中。对于跨group的顺序,按照包含2个systems的最深的group决定:
- 例如:SystemA在GroupA,SystemB在GroupB,GroupA和GroupB都在GroupC,那么SystemA和SystemB的隐含顺序为Group A和GroupB的顺序。
- [DisableAutoCreation] — 禁止默认 World初始化时自动创建System。之后需要自己明确的创建和更新这些系统。当然,也可以创建完系统后,添加到一个ComponentSystemGroup中,那么系统的Update会被Group自动调用。
Default System Groups
默认的World包含了一个ComponentSystemGroup实例的层级树。仅三个根级system groups被添加到Unity的逻辑循环中。下面的列表展示了这三个组中预定义的子系统:
- InitializationSystemGroup(在主循环的Initialization阶段结束时执行)
- BeginInitializationEntityCommandBufferSystem
- CopyInitialTransformFromGameObjectSystem
- SubSceneLiveLinkSystem
- SubSceneStreamingSystem
- EndInitializationEntityCommandBufferSystem
- SimulationSystemGroup(主循环的Update结束时执行)
- BeginSimulationEntityCommandBufferSystem
- TransformSystemGroup
- EndFrameParentSystem
- CopyTransformFromGameObjectSystem
- EndFrameTRSToLocalToWorldSystem
- EndFrameTRSToLocalToParentSystem
- EndFrameLocalToParentSystem
- CopyTransformToGameObjectSystem
- LateSimulationSystemGroup
- EndSimulationEntityCommandBufferSystem
- PresentationSystemGroup(主循环的PreLateUpdate结束时调用)
- BeginPresentationEntityCommandBufferSystem
- CreateMissingRenderBoundsFromMeshRenderer
- RenderingSystemBootstrap
- RenderBoundsUpdateSystem
- RenderMeshSystem
- LODGroupSystemV1
- LodRequirementsUpdateSystem
- EndPresentationEntityCommandBufferSystem
这个列表的内容目前还在优化中,会发生变化。
Multiple Worlds
可以创建除了默认World的多个其它Worlds,或者替换默认World。同样的Component System可以创建到多个World中,并且可以以不同的更新频率,在不同的时间点更新。
现在,不支持手动地更新World内的系统;你只能控制某个系统被创建到哪个World中的哪个System Group中。也即是,自定义的WorldB可以实例化SystemX和SystemY,并将SystemX添加到默认World的SimulationSystemGroup,将SystemY添加到默认World的PresentationSystemGroup中。这些Systems的顺序关系在它们所在的Group中被管理,并按照Group的顺序更新。
为了实现这种用法,提供了ICustomBootstrap 接口类:
public interface ICustomBootstrap
{
List<Type> Initialize(List<Type> systems);
}
实现该接口时,一个完整的System类型的列表被传递到Initialize()方法中,而且它在默认World初始化前调用。自定义的启动加载器可以遍历这个列表并且创建需要的System。可以返回一个System列表,该列表内的系统将被默认World初始化。(不返回则不创建)
例如,下面是个典型的MyCustomBootstrap.Initialize()的实现过程:
- 创建一个World和它的最上层的ComponentSystemGroups。
- 对所有的System类型:
a) 遍历ComponentSystemGroup层次,找到该System应该被添加到哪个Group中。
b) 如果找到Group,在该World中创建System并调用group.AddSystemToUpdateList()接口添加到层次结构中。
c) 如果没有找到要添加到的Group,那么,添加到返回的System类型列表中,返回到DefaultWorldInitialization。 - 调用所有顶层的groups的group.SortSystemUpdateList()。
a) Optionally add them to one of the default world groups - 返回未处理的系统的列表给DefaultWorldInitializtion。
注:ECS框架通过反射用户自定义的ICustomBootstrap实现。
建议:
- 用[UpdateInGroup]来指定你的系统在哪个Group中执行。如果不指定,默认是SimulationSystemGroup。
- 用手动更新的ComponentSystemGroups来在Unity主循环的其它阶段更新Systems。为ComponentSystem或者ComponentSystemGroup添加[DisableAutoCreation]修饰来避免被自动添加到默认的SystemGroups中,可以手动地通过接口World.GetOrCreateSystem()来创建System并在主线程中调用MySystem.Update()来执行。这是将System添加到主循环其它阶段去执行的简单的办法。(例如,你可以创建一个系统,在一帧的前面或者后面执行。)
- 尽可能的使用已有的EntityCommandBufferSystem,而不要自己创建。一个EntityCommandBufferSystem代表一个SyncPoint,主线程要等待所有工作线程的工作完成,以执行它的缓存的EntityCommandBuffers。重复利用预定义在顶层SystemGroup中的Begin/End Systems来避免SyncPoints,提升效率。
- 避免将自定义逻辑写到ComponentSystemGroup.OnUpdate()中。因为ComponentSystemGroup本身是个ComponentSystem,让人想要将用户逻辑添加到它的OnUpdate()中来执行一些逻辑,创建Jobs,等。我们反对这么做,因为从表面上很难马上确定,它的Execute是在group‘s的其它成员System之前还是之后。最好就是让SystemGroups来执行grouping机制,而在其它的System中实现我们的逻辑,并明确地指定它们的相对执行顺序。