ECS:Systems
Systems
提供logic,驱动数据的变更。
Unity ECS会自动在运行时收集所有的systems并进行实例化。
Systems在World中以group的方式来组织,你可以控制将一个system放到哪个group以及这个system在group中的执行顺序(通过system attributes)。
默认情况下,所有的systems都会被添加到default world的Simulation System Group里面,这些system的执行顺序确定但未指定。你可以使用system attribute来禁用这种默认行为。
一个system的update loop由其parent Component System Group来驱动。
Component System Group本身也是一种类型的system,用们用来更新其child systems。
生命周期回调函数,在main thread调用:
回调
|
说明
|
OnCreate
|
system创建
|
OnStartRunning
|
在第一个OnUpdate调用之前
在每次resumes running之时
|
OnUpdate
|
每帧调用,只要system是enabled的,且有事可做
|
OnStopRunning
|
当query找不到entities时,会停止调用OnUpdate,这个时候会触发该回调
|
OnDestroy
|
system销毁
|
System类型:
System类型
|
说明
|
Component Systems
|
工作在main thread
|
Job Component Systems
|
在OnUpdate中调用IJobForEach或IJobChunk
这些Job工作在worker threads
|
Entity Command Buffer Systems
|
thread-safe的command buffer
用来缓存一些entities和components的操作
|
Component System Groups
|
用于提供systems的嵌套管理和执行顺序管理
|
Component Systems
运行在main thread的systems。
JobComponent Systems
自动化管理job依赖
依赖管理是非常复杂的,所以Unity ECS选择自动处理这一块。
规则很简单:
(1)多个来自不同的systems的jobs可以并行读取某个组件类型(IComponentData)的数据;
(2)如果有一个job正在写数据,则其他jobs则不能并行运行,并且会基于依赖关系进行调度。
实现原理:
所有的jobs和systems都会声明它们需要进行读或写的ComponentTypes。所以当JobComponentSystem返回一个JobHandle时,会自己将它要读写的信息注册到EntityManager。
所以,当一个system的job正在写某个component数据时,其他systems可以查询到它要读取的compoent列表的相关状态,然后在此时产生一个对于第一个sysem的job的依赖。
JobComponentSysem只在需要时才会产生依链,因此不会导致main thread阻塞。
但是如果有一个non-job的ComponentSystem需要访问同样的数据,会怎样呢?因为知道需要访问的所有ComponentTypes,所以ComponentSystem会在与这些data有关的jobs调用(System.OnUpdate())之前完成数据处理。
策略保守但确定:
当在一个system中调用多个jobs时,依赖关系也会被传递到所有的jobs,即使不同jobs产生依赖的可能性较低。
如果事实证明这样确实产生了效率问题,那解决方法就是将一个system拆分成两个。
同步点(sync point):
这些操作都会导致自动同步hard sync point(等待所有的jobs完成工作):
structure changes:CreateEntity、Instantiate、Destroy、AddComponent、RemoveComponent、SetSharedComponentData。
所以在一帧当中调用上述函数,会自动触发等待所有的JobComponentSystem调度的jobs完成工作,这可能会导致卡顿。
优化办法:使用EntityCommandBuffer。
多个Worlds:
每个World中的jobs依赖管理都是独立的,因为每个World都拥有自己的EntityManager。
一个World中的自动同步点(hard sync point)不会影响到其他World。
从程序平滑性的角度来说,可以在一个World中创建entities,然后在帧运行开始之前移动到另一个World,让他们之间保持相互的独立性。
Entity Command Buffers(ECB)
解决两个问题:
(1)job中不能访问EntityManager;
(2)structural change会导致sync point,等待所有的jobs完成工作。
使用EntityCommandBuffer(ECB)可以将这些changes(来自job或main thread)缓存在一个queue里面,以便使它们稍后再main thread中生效。
Command buffer可以用来在work threads中做一些很耗费的操作,然后下一帧在main thread中生效。
EntityCommandBufferSystem:
一个为其他system提供ECB对象的system。
默认的World提供了3个每帧按顺序执行的system groups:
- initialization
- simulation
- presentation
在一个group内部,在所有其他systems运行之前和之后分别有一个entity command buffer system会运行。
ECB具体的实现类有如下几个:
- BeginInitializationEntityCommandBufferSystem
- BeginPresentationEntityCommandBufferSystem
- BeginSimulationEntityCommandBufferSystem
- EndInitializationEntityCommandBufferSystem
- EndSimulationEntityCommandBufferSystem
建议是直接使用现有的command buffer systems,而不是自己额外创建,这样可以最小化同步点的开销。
ToConcurrent:同时发生的ECB。
如果你要在parallel job中使用ECB,必须确保将其转化为concurrent ECB来使用。另外为了确保commands的顺序,还必须传入entity的index,而不是依赖这些并行job的work实际的执行顺序。
struct Lifetime : IComponentData { public byte Value; } class LifetimeSystem : JobComponentSystem { EndSimulationEntityCommandBufferSystem m_EndSimulationEcbSystem; protected override void OnCreate() { base.OnCreate(); // Find the ECB system once and store it for later usage m_EndSimulationEcbSystem = World .GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>(); } protected override JobHandle OnUpdate(JobHandle inputDeps) { // Acquire an ECB and convert it to a concurrent one to be able // to use it from a parallel job. var ecb = m_EndSimulationEcbSystem.CreateCommandBuffer().ToConcurrent(); var jobHandle = Entities .ForEach((Entity entity, int entityInQueryIndex, ref Lifetime lifetime) => { // Track the lifetime of an entity and destroy it once // the lifetime reaches zero if (lifetime.Value == 0) { // pass the entityInQueryIndex to the operation so // the ECB can play back the commands in the right // order ecb.DestroyEntity(entityInQueryIndex, entity); } else { lifetime.Value -= 1; } }).Schedule(inputDeps); // Make sure that the ECB system knows about our job m_EndSimulationEcbSystem.AddJobHandleForProducer(jobHandle); return default; } }
System Update Order
ComponentSystemGroup:
一个group包含了一系列的按顺序执行的systems,group本身也是一个system,但是它只用来组织其他的systems,而不应该包含游戏逻辑(虽然你也可以在其OnUpdate里面添加逻辑,但不建议这样做)。
System Update Order:
基本方案:使用ComponentSystemGroup来组织所有的systems,定制和确保它们执行顺序。
按照group组织起来的groups最终形成了一个hierarchy结构,它的执行顺序采用深度优先遍历的方式。
Default System Groups:
ECS为默认World创建了一系列的default system groups:
System属性标签:
(1)[UpdateInGroup]
标记system被添加到那个group,如果不填,system将自动添加到默认World的SimulationSystemGroup。
(2)[UpdateBefore] && [UpdateAfter]
标记systems之间的相对顺序,相关联的两个systems必须属于同一个group,不能跨group使用。因为跨group的system的相对顺序只和group本身执行的先后顺序有关。
(3)[DisableAutoCreation]
标记system不在默认World的initialization阶段自动创建。
你必须手动创建和update这个sysem。但是,你仍然可以将合格system添加到某个group的update列表,这样它也将会自动update,就和列表中其他的systems一样。
Multiple Worlds:
同一个system可以在不同的多个Worlds中分别实例化,并相互保持独立的执行顺序和更新频率。
public interface ICustomBootstrap
{
// Returns the systems which should be handled by the default bootstrap process.
// If null is returned the default world will not be created at all.
// Empty list creates default world and entrypoints
List<Type> Initialize(List<Type> systems);
}
提示和经验:
(1)明确指定Group:给每个自定义的system添加[UpdateInGroup]属性;
(2)手动方式:使用手动ticked的ComponentSystemGroups来更新Unity player loop之外的systems;
- 添加[DisableAutoCreation]来组织MySystem自动添加到默认World的group;
- 使用World.GetOrCreateSystem()来创建MySystem;
- 在主线程中合适的地方手动调用MySystem.Update()来运行MySystem。
(3)尽量使用现有的EntityCommandBufferSystems,而不是手动添加新的。
(4)不要在ComponentSystemGroup.OnUpdate()里面添加自定义逻辑。