ECS system 概述

Systems

System, ECS三要素之一,负责把组件当中的数据从一个状态转换到另一个状态的逻辑, 比如,一个system可以可能会根据所有移动实体的速度乘以自上次更新以来的时间间隔来更新它们的位置。

Unity ECS 提供了多种不同的systems.  main systems 主系统你可以实现转换实体数据的系统是 ComponentSystem 和JobComponentSystem.这两种系统类型都有助于根据实体上的组件类型来选择或者是遍历实体

Systems 提供了事件类型event-style callback的回调函数 ,比如OnCreate() and OnUpdate() 你可以在系统生命周期内地的正确时间段来运行你的代码,执行逻辑,这些函数是在主线程中进行的 main thread.在Job Component System, 你通常在OnUpdate() 函数内执行逻辑代码. Jobs 本身在工作线程上运行 worker threads. 通常, Job Component Systems 能够提供最佳性能,因为它充分利用了 CPU的多核结构. 甚至当你的jobs通过Burst Compiler来编译时,性能也能得到很大的改善

Unity ECS 会自动发现项目中的系统类 system classes,并在运行时实例化它们. Systems 在一个 World里面的分为一个组. 你可以控制system被添加到哪个组里面,也可以通过 system attributes来决定system在组内的顺序. 默认,所有的systems 都被添加到 default world的Simulation System Group内, 但是顺序不确定, 你可以使用system attribute 来禁用自动创建

一个系统的 system's update 循环是通过它的父组件Component System Group驱动的 . Component System Group 是一种专门来负责更新它的子系统的系统,就是它本身是一个系统,作用更新它的子系统

你可以查看正在运行的系统的配置 system configuration ,通过 Entity Debugger 窗口(menu: Window > Analysis > Entity Debugger).

System event functions:系统事件函数

在创建系统时,可以实现一组系统生命周期事件函数. Unity ECS 按照以下顺序执行:

  • OnCreate() -- 在系统创建时调用
  • OnStartRunning() -- 在第一次更新之前以及系统恢复运行时,类似与unity的 start函数
  • OnUpdate() -- 只要system有工作要做,就每一帧都执行 (see ShouldRunSystem()) 。且系统是开启的 ,注意 OnUpdate 是在ComponentSystemBase 的子类中定义的; 每种类型的系统类都可以定义它自己的update behavior.
  • OnStopRunning() -- 当系统 system 停止更新时updating ,因为它找不到匹配查询的实体. 经常在OnDestroy之前调用.
  • OnDestroy() -- 当系统销毁时调用destroyed.

所有这些函数都是在主线程上执行的. 但是你可以在OnUpdate(JobHandle) 中定义一个继承于JobComponentSystem 的 Jobs ,来让它在子线程中执行

System types:系统类型

Unity ECS 提供了几种类型的系统.通常,您编写的用于实现游戏行为和数据转换的系统将扩展ComponentSystem或JobComponentSystem.其他系统类有专门的用途;你通常会使用 Entity Command Buffer System和 Component System Group 类的实例.

  • Component Systems -- 实现一个 ComponentSystem的子类的系统,在主线程上运行逻辑,或者使用没有特别针对ECS进行优化的.
  • Job Component Systems -- 通过IJobForEach 或 IJobChunk实现一个JobComponentSystem自类来执行逻辑
  • Entity Command Buffer Systems --为系统提供一个 EntityCommandBuffer的实例,每个默认系统组在其子系统列表的开头和结尾都会有一个 Entity Command Buffer System
  • Component System Groups -- 为其他系统提供嵌套的组织和更新顺序. Unity ECS 默认创建几个l Component System Groups

 

Component Systems

  ComponentSystem  (在标准ECS术语中也称为系统)执行 entities上的操作.  ComponentSystem不能包含实例数据. 类似于原来unity中的 Component类,但是它只含有方法 , ComponentSystem负责来更新所有满足条件的实体 ,通过 EntityQuery这个结构体来查询).

Unity ECS提供了一个你可以在你的代码中扩展的抽象类 ComponentSystem

See file: /Packages/com.unity.entities/Unity.Entities/ComponentSystem.cs.

JobComponentSystem

Automatic job dependency management:自动管理job的依赖

管理依赖关系很困难. 这也是 JobComponentSystem 为你自动管理的原因. 规则很简单: 来自不同系统的jobs 可以并行读取相同类型的 IComponentData. 如果其中一个job正在写入数据,那么它们就不能并行运行,并将根据job之间的依赖关系进行调度。

public class RotationSpeedSystem : JobComponentSystem
{
    [BurstCompile]
    struct RotationSpeedRotation : IJobForEach
    {
        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都声明它们可以读写的组件类型 ComponentTypes . 当一个JobComponentSystem 返回一个 JobHandle,它会在EntityManager中自动注册,包括它可以读写的组件类型的所有信息

因此如果一个 system 写入 component A, 另一个系统稍后读取component A,然后JobComponentSystem查看它正在读取的类型列表 ,因此,从第一个系统向您传递对作业的依赖项。

JobComponentSystem只需将job作为依赖项链到需要的地方,这样就不会在主线程上造成任何阻塞.但是如果一个non-job ComponentSystem 访问相同的数据? 因为所有的访问都是已经声明过的, 所以 ComponentSystem 在调用OnUpdate之前,自动完成所有针对组件类型的jobs

Dependency management is conservative & deterministic:依赖关系管理是保守和确定的

Dependency management是保守的. ComponentSystem 之追踪所有 EntityQuery对象的使用 ,存贮基于查找的对象 ,正在写入或读取的类型

在单一系统中调度多个jobs时也是如此, 即使不同的jobs可能需要较少的依赖项,也必须将所有的依赖项传递给所有job.如果这被证明是一个性能问题,那么最好的解决方案是将一个系统一分为二。

Sync points:同步点

所有的结构变化都有严格的同步点. CreateEntity, Instantiate, Destroy, AddComponent, RemoveComponent, SetSharedComponentData 它们都有一个同步点. 意味着所有的jobs通过JobComponentSystem都已经完成了在创建实体之前,.这是自动完成的,所以,调用 calling EntityManager.CreateEntity可能会导致一个大的暂停,等待world上所有预先安排的jobs完成。

See EntityCommandBuffer for more on avoiding sync points when creating entities during game play.

Multiple Worlds

没一个 World都有它自己的 EntityManager,这样就有了一组独立的JobHandle依赖项管理.一个world 中的同步点不会影响另一个 World.因此,对 streaming 和程序生成的场景, 在一个 World创建实体,并在开始时把他们移动到另一个world中,是很有用的

See ExclusiveEntityTransaction for more on avoiding sync points for procedural generation & streaming scenarios and System update order.

 

Entity Command Buffer

The EntityCommandBuffer 类解决了两个问题·:

  1. 当你在 job中,你不能访问EntityManager.
  2. 当你访问 EntityManager (比如创建一个entity) ,所有注入的数组和 EntityQuery 查询物体都是无效的

 EntityCommandBuffer 象允许您对排队进行更改 (不管从job还是从主线程) 这样它们就能在 main thread以后生效. EntityCommandBuffer使用下列两种方法:

ComponentSystem 的子类,在主线程中调用PostUpdateCommands自动更新,要使用它,只需引用属性并将其添加到队列当中排队. 它们会自动在系统的update函数返回之后执行

Here's an example:

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 作为一种很便携的工具,帮助你在系统内防止数组失效,同时仍然能够在改变world( it is helpful to think of the automatic EntityCommandBuffer as a convenience that allows you to prevent array invalidation inside your system while still making changes to the world.)

  对于jobs, 你必须在主线程上从一个entity command buffer system上请求EntityCommandBuffer ,并把它们传递给 jobs. 当EntityCommandBufferSystem 更新时, command buffers将会根据他们创建的顺序执行,可以集中管理内存,并保证生成的实体和组件的确定性。

Entity Command Buffer Systems

默认的World初始化提供了三个系统组, 分别是initialization, simulation,和 presentation, 它们在每一帧中按顺序更新.在一个组内, 有两个 entity command buffer system,一个会在组内其它系统执行之前运行 ,还有一个会在其它系统都执行完毕后运行. 更好的来说, 为了最小化同步点,您应该使用现有的 command buffer systems而不是创建一个新的. See Default System Groups for a list of the default groups and command buffer systems.

Using EntityCommandBuffers from ParallelFor jobs

当从 ParallelFor jobs中使用EntityCommandBuffer 来解决 EntityManager 的命令,EntityCommandBuffer.Concurrent 接口用于保证线程安全和确定性.此接口中的公共方法需要额外的jobIndex 参数, 用来确定记录的顺序.  jobIndex 对每一个job来说必须是一个唯一的 ID. 由于性能的原因, jobIndex应该是(增加) index 值传递给 IJobParallelFor.Execute(). 除非你真的知道你在做什么, using 将该index用作jobIndex是最安全的选择.使用其他jobIndex值会有正确的结果输出,但在某些情况下可能会产生严重的性能影响

 

System Update Order:系统更新顺序

使用 Component System Groups来确定系统更新的顺序. 你可以在系统类声明的时候添加[UpdateInGroup]属性,从而把系统放到组内. 然后使用 [UpdateBefore] 和 [UpdateAfter] 属性来确定在组内执行的顺序

 ECS创建了一套 default system groups, 您可以使用它在框架的正确阶段更新系统.您可以将一个组嵌套到另一个组中,以便在正确的阶段更新组中的所有系统,然后,还可以根据组中的顺序进行更新。

Component System Groups

 ComponentSystemGroup类表示应按特定顺序一起更新的相关component systems组件系统的列表。 ComponentSystemGroup 继承自ComponentSystemBase, 所以它在所有重要的方面都像一个组件系统 component system ,比如 它可以相对于其他系统进行排序, 含有 OnUpdate() 方法, etc. 最重要的是,这意味着component system groups 可以嵌套在其他component system groups, 形成一个层次结构。

默认情况下,当一个 ComponentSystemGroup’s Update() 方法被调用,它调用在其已排序的成员系统列表中的每个系统上的Update() ,如果任何成员系统本身就是系统组system groups, 它们将递归地更新自己的成员. 结果的系统排序遵循树的深度优先遍历。

System Ordering Attributes:系统顺序属性

现有维持系统执行顺序的属性,语义和限制略有不同

  • [UpdateInGroup] — 指定一个 ComponentSystemGroup ,让该系统成为组内的一员. 如果省略此属性, the system 会自动添加到World’s SimulationSystemGroup
  • [UpdateBefore] and [UpdateAfter] — 相对于其他系统对系统进行排序. 这些属性指定的系统类型必须是同一组的成员。跨组系统的执行顺序,应当在最上层的组内比较,就像二叉树 :
    • 比如:  SystemA 在 GroupA, SystemB 在 GroupB, GroupA和 GroupB都是 GroupC中的成员,GroupA和GroupB的排序隐含地决定了SystemA和SystemB的相对排序; 不需要对系统进行显式排序。
  • [DisableAutoCreation] — 防止default world初始化的时候创建系统,您必须显式地创建和更新系统. 但是你可以把这个属性添加到 ComponentSystemGroup的系统的更新列表里,这样该列表中的系统就会向其他系统一样自动更新

Default System Groups

 默认 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

请注意,此列表的具体内容可能会更改。

Multiple Worlds:多个world

你可以创建多个 Worlds, 除了(或代替)上面描述的default World. 同一个组件系统类可以在多个世界中实例化,而且每个实例可以从更新顺序中的不同点以不同的速率更新.

目前还没有办法手动更新给定世界中的每个系统;相反,您可以控制在哪个世界中创建哪些系统,以及应该将它们添加到哪些现有系统组中.因此,一个自定义的WorldB可以实例化SystemX和SystemY,将SystemX添加到default World的SimulationSystemGroup,并将SystemY添加到default 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 Initialize(List systems);
}

当您实现这个接口时,组件系统类型的完整列表将被传递给Initialize()方法,在默认世界初始化之前.一个自定义的 bootstrapper 会遍历列表,并创建系统. 你可以从Initialize() 方法返回系统列表,它们将被创建为正常的default world初始化的一部分

下面是一个例子 MyCustomBootstrap.Initialize():

  1. 创建任意的worlds,和他们的组件系统组Create any additional Worlds and their top-level ComponentSystemGroups.
  2. 对于系统类型列表中的每个类型For each Type in the system Type list:
    1. 自动向上找到它的最顶级的ComponentSystemGroup(Traverse upward through the ComponentSystemGroup hierarchy to find this system Type’s top-level group.)
    2. 如果是在第一步创建的组,则把系统通过group.AddSystemToUpdateList()添加到world中
    3. 如果不是,则把这个类型添加到列表中,返回给DefaultWorldInitialization.
  3. 在新的顶级组上调用 group.SortSystemUpdateList()
    1. 选择性的把他们添加到一个default world groups默认世界组里
  4. 返回 DefaultWorldInitialization的没有unhandled 处理的系统列表.

Note: the ECS framework 通过反射自动查找你的 ICustomBootstrap 实现

Tips and Best Practices

  •  在声明system的时候,使用 [UpdateInGroup] 把系统分组.如果没有声明,则默认分到 SimulationSystemGroup组内.
  •  使用手动标记 ComponentSystemGroups 来决定什么时候更新系统. 给component system (or system group) 添加 [DisableAutoCreation] 属性,来阻止它被创建或者被添加到默认的 default system groups里面. 你可以通过World.GetOrCreateSystem() 手动创建system,然后通过MySystem.Update() 在主线程中更新它. 这是在unity 循环终插入系统的简单方法(比如:如果你有一个系统需要在一帧前或者后执行).
  •   使用已经存在的 EntityCommandBufferSystems,而不是重新创建一个. 一个 EntityCommandBufferSystem代表一个同步点,需要主线程等待工作线程访问完所有的外部 EntityCommandBuffers,再继续工作. 重复利用预先定义的Begin/End systems
  • 避免在 ComponentSystemGroup.OnUpdate()中放入自己的逻辑. 因为ComponentSystemGroup功能上是一个组件系统,因为从外部不能够马上就知道你写的逻辑是否在组内成员更新的时候执行了 ,最好把系统组限制在一个分组机制中  keep system groups limited to a grouping mechanism,并在一个单独的组件系统中实现逻辑,然后在组内给这个系统显示的排序

你可能感兴趣的:(ECS)