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()里面添加自定义逻辑。
 
posted @ 2020-02-16 23:56  斯芬克斯  阅读(801)  评论(0编辑  收藏  举报