Unity ECS+Jobs System笔记 ECS——System(四)

来源:https://docs.unity3d.com/Packages/[email protected]/manual/index.html
我会对官方文档内容略作整理,有需要可以查看官方文档
这一部分需要大家了解有关Jobs方面的内容,之后我也会再出文章分析一下:
https://docs.unity3d.com/Manual/JobSystem.html

3、系统——System

提供了改变组件中数据状态的逻辑,比如:一个系统会根据两帧之间的间隔时间更新所有会动的实体的位置

3.1、ComponentSystem

Unity中的ComponentSystem(也是标准ECS系统中的一个System)对一个实体而言,它是只包括了方法,他不能包含数据;对比旧的Unity系统而言,他有点像旧的Component系统,但是是一个只包含方法的Component。一个ComponentSystem负责更新一组匹配组件(在一个名为EntityQuery的结构中定义)的实体
UnityECS提供了一个ComponentSystem接口供使用

3.2、JobComponentSystem

自动Job依赖管理

管理依赖关系很难,这就是JobComponentSystem为您自动完成的原因,规则很简单:来自不同系统的Jobs可以并行读取相同类型的IComponentData,如果其中一个作业正在写入数据,那么它们将无法并行运行,并将按作业之间的依赖关系进行调度

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);
    } 
}

它是如何运作的?

所有的Jobs以及Systems会声明他们要读取或写入什么样类型的组件,因此,当JobComponentSystem返回一个JobHandle时,它会自动通过EntityManager进行注册——包括有关读取或写入的信息
当一个系统正在对组件A写入数据,而另一个系统过一会要读取A的数据,JobComponentSystem就会查看它正在读取的类型列表,从而从第一个系统传递对Job的依赖性
JobComponentSystem简单的将Jobs作为依赖链接起来,从而不会导致主线程停顿,但是如果当非Job的ComponentSystem访问相同的数据会发生什么?因为声明了所有的访问权限,所以ComponentSystem会再系统调用OnUpdate之前完成所有的Jobs及其系统所依赖的组件

依赖管理是保守和确定的

依赖管理是保守的,ComponentSystem简单的追踪EntityQuery中使用过的实体,并根据它来存储正在写入或读取的类型
此外,在单个系统中调度多个Job时,必须将依赖关系传递给所有Job,尽管不同的Job可能需要较少的依​​赖关系,如果这被证明是性能问题,那么最好的解决方案是将系统分成两部分
依赖管理提供了确定的行为并且提供了一个非常简单的API。

同步点

所有结构改变都有硬同步点,CreateEntity,Instantiate,Destroy,AddComponent,RemoveComponent,SetSharedComponentData都有一个硬同步点,这意味着所有经过JobComponentSystem安排的Job都会在创建实例之前完成,例如:(这会自动发生)EntityManager.CreateEntity在帧的中间调用可能会导致一个大的帧停顿——去等待之前World中所有已经安排好的Jobs完成

多个世界

每个World都有自己的EntityManager,都有自己的一组独立的JobHandle依赖管理,因此,一个World中的硬同步点不会影响另一个World。对于流式传输和程序生成方案,在一个世界中创建实体,然后在一个事务的第一帧将其移动到另一个世界是很有用的。

3.3、Entity Command Buffer

EntityCommandBuffer解决了两个重要的问题:

  • 在Job中无法访问EntityManager
  • 当访问EntityManager时(例如,创建实体),将使所有注入的数组和EntityQuery对象无效

EntityCommandBuffer抽象允许你排列这些改变(无论是从Job中还是从主线程中),这可以使得这些改变在主线程之后执行。这里有两种使用EntityCommandBuffer的方法:
ComponentSystem在主线程上更新的子类有一个可自动调用的子类PostUpdateCommands,要使用它,只需引用属性并更改队列即可。它们将在系统Update函数返回之后,立刻自动应用于World
这有一个例子:

PostUpdateCommands.CreateEntity(TwoStickBootstrap.BasicEnemyArchetype);
PostUpdateCommands.SetComponent(new Position2D { Value = spawnPosition });
PostUpdateCommands.SetComponent(new Heading2D { Value = new float2(0.0f, -1.0f) });
PostUpdateCommands.SetComponent(default(Enemy));
PostUpdateCommands.SetComponent(new Health { Value = TwoStickBootstrap.Settings.enemyInitialHealth });
PostUpdateCommands.SetComponent(new EnemyShootState { Cooldown = 0.5f });
PostUpdateCommands.SetComponent(new MoveSpeed { speed = TwoStickBootstrap.Settings.enemySpeed });
PostUpdateCommands.AddSharedComponent(TwoStickBootstrap.EnemyLook);

如你所见,这个API与EntityManager的API十分相似,在这个模式下,将自动EntityCommandBuffer视作一种便利是非常有帮助的,它允许您在对世界进行更改的同时防止系统内部的Array失效
对于Jobs,必须从主线程上的实体命令缓冲区系统请求EntityCommandBuffer,并将它们传递给Jobs。当EntityCommandBufferSystem更新时,命令缓冲区将会依赖主线程按创建顺序去执行。而这个额外的步骤,便于我们集中存储和管理,并确保所生成的实体和组件的确定性。

实体命令缓冲系统

默认的World初始化提供了三个系统组,用于初始化、模拟和显示,每个帧按顺序更新。在系统组内,有一个实体命令缓冲系统,它在系统组中的任何其他系统之前运行,而另一个这样的系统在系统组中的所有其他系统之后运行。最好使用现有的命令缓冲区系统之一而不是创建自己的命令缓冲区系统,以便最小化同步点。

根据ParallelForJobs使用EntityCommandBuffers

当使用EntityCommandBuffer从ParallelForJobs发出EntityManager的命令时,该接口用于保证线程安全性和确定性回放,这个接口中的公共方法采用了一个额外的jobIndex参数,该参数用于以确定的顺序回放记录的命令,这个jobIndex参数对每个Job而言必须具有唯一ID。出于性能原因,jobIndex必须是增加的值传递给IJobParallelFor.Execute(),除非你确切的知道你正在做什么,使用index代替jobIndex是更安全的做法。使用其他jobIndex值会产生正确的输出,但在某些情况下会导致严重的性能影响

3.4、System Update Order

使用Component System Groups可以指定系统的更新顺序,你可以通过系统类中声明的[UpdateInGroup]属性将系统放置在一个Group中,您可以使用[UpdateBefore]和[UpdateAfter]属性指定组内的更新顺序
ECS框架创建一组默认系统组(default system groups),你可以在指定帧更新你的系统,你可以将一个组嵌套在另一个组中,以便组中的所有系统都在正确的阶段更新,然后根据组内的顺序进行更新

Component System Groups(组件系统组)

ComponentSystemGroup类代表了一组按特定顺序更新的相关组件系统的列表,ComponentSystemGroup派生自ComponentSystemBase,因此它在所有重要的方面都像组件系统一样——它可以相对于其他系统进行排序,具有OnUpdate()方法等,最重要的是,这意味着Component System Groups可以嵌套在其他Component System Groups中,形成层次结构
默认情况下,当一个ComponentSystemGroup调用Update()方法时,它会对其成员系统的每个系统上调用Update()。如果任何成员系统本身就是系统组,它们将递归更新自己的成员,生成的系统指令顺序遵循树的深度优先遍历

系统指令属性

目前所维护的System Ordering Attributes(系统指令属性)具有略微不同的语义和限制:

  • [UpdateInGroup]——指定一个必须是其成员的ComponentSystemGroup,如果这个属性被忽省略了,那么系统就会自动将其加入到默认World的SimulationSystemGroup中
  • [UpdateBefore] and [UpdateAfter]——相对于其他Component System Groups,被这两个属性所指定组必须与其本身在同一个组中,跨越了边界的指令由两个组能接触到的最深的组进行处理
    例子:如果SystemA在GroupA中且SystemB在GroupB中,而GroupA和GroupB都是GroupC的成员,则GroupA和GroupB的顺序隐式确定SystemA和SystemB的相对顺序; 没有必要明确排序
  • [DisableAutoCreation]——防止在默认世界初始化期间创建系统,必须显式创建和更新系统,但是可以将具有此标记的系统添加到ComponentSystemGroup的更新列表中,然后它将像该列表中的其他系统一样自动更新

默认系统组

默认World包含ComponentSystemGroup实例的层次结构,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

请注意,此列表的具体内容可能会发生变化

多个世界(Multi Worlds)

除了上述默认世界(也可以代替上述默认世界)之外,你还可以创建多个世界,可以在多个World中实例化相同的组件系统类,并且可以从更新顺序中的不同点以不同的速率更新每个实例
目前无法手动更新给定世界中的每个系统,相反,你可以控制在哪个World中创建哪些系统,以及应该将哪些系统组添加到哪个系统组中,因此,自定义WorldB可以实例化SystemX和SystemY,将SystemX添加到默认World的SimulationSystemGroup,并将SystemY添加到默认World的PresentationSystemGroup。这些系统可以像之前一样相对于它们的组指定自己的相对位置,并将与相应的组一起更新。
为了支持这个用例,现在提供了一个新的ICustomBootstrap接口:

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);
}

当你实现这个接口,在默认世界初始化之前,整个系统组件类型列表就会通过Initialize()函数传递给类,自定义引导程序可以遍历此列表并在其所需的任何World中创建系统,你可以通过Initialize()方法返回一组systems,它们将作为正常的默认世界初始化的一部分进行创建
例如,以下是自定义典型的MyCustomBootstrap.Initialize()实现过程

  1. 创建任何其他Worlds及其顶级ComponentSystemGroups
  2. 遍历系统类型列表中的每个类型
    2.1 向上遍历ComponentSystemGroup层次结构以查找此系统Type的顶级组
    2.2 如果它是在步骤1中创建的组之一,则在该World中创建系统并通过group.AddSystemToUpdateList()将其添加到层次结构
    2.3 如果不是,请将此类型附加到List并返回到DefaultWorldInitialization
  3. 在新的顶级组上调用group.SortSystemUpdateList()
    3.1 (可选)将它们添加到其中一个默认世界组
  4. 将未处理的系统列表返回到DefaultWorldInitialization

注意: ECS框架通过反射找到您的ICustomBootstrap实现

提示和最佳实践

  • 使用[UpdateInGroup]为编写的每个系统指定系统组。如果未指定,则隐式默认组为SimulationSystemGroup
  • 使用手动勾选的ComponentSystemGroups更新在Unity循环中其他位置的系统,将[DisableAutoCreation]属性添加到组件系统(或系统组)可防止将其创建或添加到默认系统组,你仍然可以使用World.GetOrCreateSystem()手动创建系统并通过从主线程手动调用MySystem.Update()来更新它。这是在Unity循环中的其他位置插入系统的简单方法(例如,如果需要系统应在帧中稍后或更早运行)
  • 如果可以的话,使用现有的EntityCommandBufferSystems代替增加新的,一个EntityCommandBufferSystem代表了一个主线程需要等待其他工作线程EntityCommandBuffers完成的同步点,在每个根级别系统组中重用一个预定义的Begin/End系统,比起采用一个新的"bubble"更有可能创建一个新的“bubble”
  • 避免在ComponentSystemGroup.OnUpdate()使用自定义逻辑,由于ComponentSystemGroup在功能上是一个组件系统,它可能很容易为其OnUpdate()方法添加自定义处理和工作等,但我们建议不要这样做,因为它不是立即从外部清楚得知——自定义逻辑是在组成员更新之前还是之后执行,所以最好将系统组限制为分组机制,并在单独的组件系统中实现所需的逻辑,并明确组的排序顺序

你可能感兴趣的:(Unity笔记)