Table of Contents
Systems
System作为ECS核心部分,提供了将组件数据从其当前状态转换为下一个状态的逻辑。例如,系统可能会以其速度 乘以 上次更新以来的时间间隔来更新所有移动身体的位置。
Unity ECS 提供了许多不同类型的系统。可以用来转换实体数据的主要系统是 ComponentSystem和JobComponentSystem。这两种系统类型都有助于根据身体的关联组件来选择和遍历一组实体。
系统提供事件样式的回调函数,例如 OnCrate() 和 OnUpdate() ,你可以实现这些函数以在系统生命周期中正确的正确时间运行代码。这些函数在主线程上调用。在JobComponentSystem中,通常可以在OnUpdate中安排Job。Job本身在工作线程上运行。通常,JobComponentSystem利用多个CPU内核,因此,可以提供最佳性能。当使用Burst编译器编译Jobs时,性能甚至可以进一步提高。
Unity ECS 会自动发现项目工程中的系统类,并在运行时实例化它们。系统在一个World中按组进行组织。你可以使用系统属性来控制将系统添加到哪一个组以及该系统在组中的顺序。默认情况下,所有系统都已确定的但非指定的顺序添加到默认的World的Simulation System Group 中。你也可以通过系统属性来禁用自动创建。
系统的更新循环由其父组件系统 组 驱动。一个组件系统组本身就是一种系统,专门负责更新其子系统。
你可以使用 EntityDebugger 窗口来查看运行时的系统配置(menu: Window > Analysis > Entity Debugger)
系统事件函数
实现系统时,可以实现一组系统生命周期事件函数,Unity ECS按照以下顺序调用这些函数
- OnCreate() —— 系统创建时调用
- OnStartRunning() —— 在第一次OnUpdate前调用以及系统恢复运行时。
- OnUpdate() —— 只要系统有工作要做(参阅
ShouldRunSystem()
)且系统已启用,就会每帧调用。(注意,OnUpdate函数是在ComponentSystemBase
的之类中定义的,每个类型的系统类都可以定义自己的更新行为) - OnStopRunning() —— 当系统因为找不到匹配其查询的实体时而停止更新时
- OnDestroy() —— 系统销毁时
所有这些函数都在主线程上执行。 注意: 你可以从JobComponentSystem的OnUpdate(JobHandle)函数安排Job来在后台线程中运行。
系统类型
Unity ECS 提供了几种类型的系统。通常,为了实现游戏行为和数据转换而编写的系统可以扩展ComponentSystem或JobComponentSystem。其他类型的系统具有特殊用途,通常,你会用到Entity Command Buffer System 和Component System Group 类的现有实例(Unity 提供的,不用自己实现的)
- Component Systems —— 实现
ComponentSystem
的系统,在主线程上执行器工作,或者使用专门针对ECS优化的 Jobs - Job Component Systems —— 实现
JobComponentSystem
的系统, 可以使用 IJobForEach 或 IJobChunk 执行工作 - Entity Command Buffer Systems —— 提供 EntityComandBuffer 实例给其他系统。每个默认系统组都在其子系统列表的开头和末尾都维护一个“实体命令缓存系统。
- Component System Groups —— 为其他系统提供嵌套的组织结构和更新顺序。Unity ECS 默认创建了几个 Component System Groups。
Component Systems
Unity中的ComponentSystem对实体进行操作。ComponentSystem不能包含实例数据。对比旧的Unity系统,ComponentSystem有点类似于旧的Component类,但是里面只包含方法,并没有其他数据。
一个ComponentSystem负责使用一组匹配的组件(这组组件在 EntityQuery
结构中定义)来更新所有实体。
Unity ECS 提供一个ComponentSystem
抽象类 来给你扩展自己的代码。
可以查阅文件 : /Packages/com.unity.entities/Unity.Entities/ComponentSystem.cs
Job Component Systems
Job 自动依赖管理
因为管理依赖关系很困难。因此,Unity帮助我们在JobComponentSystem中自动进行。规则很简单:来自不同系统中的Job可以并行的从相同类型的IComponentData中读取。如果其中一个Job在写入数据,则它们不能并行运行,并且根据Job之间的依赖关系进行调度。
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);
}
}
工作原理
所有的Job及其系统都声明其读取或写入的ComponentType
。当JobComponentSystem返回JobHandle时,它将自动使用EntityManager 以及其正在读取或写入的所有类型的信息来注册。
因此,如果一个 系统A 写入组件componentA,然后另一个 系统B 随后从组件componentA读取,则JobComponentSystem将遍历其正在读取的列表类型,从而将对第一个系统A的Job的依赖传递过来(即 JobHandle inputDesps)
JobComponentSystem 只是在需要的时候将Job作为依赖项进行链接,因此不会在主线程上造成卡顿。但是,如果是非 Job ComponentSystem访问相同的数据怎么办?因为已经声明了所有访问权限,所以ComponentSystem在调用OnUpdate之前会自动完成所有针对本系统使用的组件类型的jobs。
- 依赖性管理是保守的(非频繁变化的)以及确定的
依赖性管理是保守的,ComponentSystem只是跟踪所有曾经使用过的EntityQuery对象,并存储基于该对象正在写入或读取的类型。 同样,在单个系统中调度多个Job时,即使不同的Job可能需要比较少的依赖关系,也必须将依赖关系传递给所有Jobs。如果最后发现是这里的性能问题,那么最好的解决方案就是把该系统分为两个部分。 依赖性管理的方式是保守的。它提供正确性以及正确的行为,同时提供非常简单的API。
- 同步点
所有结构更改都具有硬同步点。
CreateEntity
,Instantiate
,Destroy
,AddComponent
,RemoveComponent
,SetSharedComponentData
都具有硬同步点。意味着通过JobComponent计划的所有Job都将在实体创建前完成,这些都是自动发生的。例如,在框架中间调用 EntityManager.CreateEntity可能会导致大量停顿,等待World中所有先前的计划的Job完成。 有关在游戏过程中创建实体时避免同步点的更多信息,参考Entity Command Buffers - 多个Worlds
每一个
World
都有自己的EntityManager
,因此有单独的JobHandler
依赖关系管理集。一个世界的硬同步点不会影响另一个世界。因此,对于流和程序生成方案,建议在帧开始时,先在一个World
生成Entity,然后通过一个事务把它移动到另一个World
。 有关避免在程序生成和流传输方案,以及系统更新顺序中避免同步点的更多信息,请参考ExclusiveEntityTransaction
Entity Command Buffers
EntityComandBuffer 类解决类两个重要的问题:
- 在Job中无法访问EntityManager
- 在访问EntityManager(如,创建实体)时,会使得所有注入的数组和EntityQuery无效。
EntityCommandBuffer
概念允许你更改队列(来自job或主线程的队列)以便以后可以在主线程上生效。有两种方式来使用EntityCommandBuffer
:
其API与EntityManager API 非常相似。方便起见,我们把自动EntityCommandBuffer
看成一种便利,它允许在更改环境的同时防止系统内部数组失效。
对于Job,必须从主线程上的 实体命令缓存系统请求 EntityCommandBuffer
,并将它们传递给Jobs。当EntityCommandBufferSystem
更新时,命令缓存将按照添加顺序在主线程上播放。这一步是必须的,以便可以集中进行内存管理,并可以确保所生成实体和组件的确定性。
实体命令缓存系统(Entity Command Buffer Systems)
默认的初始化的World提供了三个系统分组,分别用来初始化(initialization),模拟(simulation),和演示(presentation),并每帧按顺序进行更新。在一个分组中,有一个实体命令缓存系统在该组其他所有系统之前运行,还有另一个实体命令缓存系统在该组其他所有系统之后运行。除非有必要,你应该所有现有的这两个命令缓存系统之一,而不是创建自己的命令缓冲系统,以最大程度的减少同步点。
有关默认分组和命令缓存的列表,请参考 默认系统分组
在ParallelFor jobs中使用EntityCommandBuffer
当使用EntityCommandBuffer 从ParallelFor jobs 发出EntityManager命令时,EntityCommandBuffer.Concurrent
接口用于保证线程安全和确定性播放。此接口的public 方法中带有一个而外的jobIndex参数,该参数用于按确定的顺序来播放记录的命令。jobIndex必须是每个job唯一的ID,处于性能原因,jobIndex应该是传递给IJobParallelFor.Execute()
的(递增)索引值。除非你真的知道自己在做什么,否则将索引作为jobIndex是最安全的选择。使用其它jobIndex值将产生正确的输出,但是在某些情况下,可能会严重影响性能。
System Update Order
使用 Component System Group 来指定系统的更新顺序。你可以在声明系统类时使用[UpdateInGroup]特性来将系统放在一个组中。然后,你可以所有[UpdateBefore]和[UpdateAfter]特性来指定在组中的更新顺序。
ECS框架会创建一组默认的系统组,可用于在正确的帧阶段更新你的系统。你可以将一个组嵌套在另一个组中,以便你的分组中所有的系统都在正确的阶段进行更新,并且根据它们所在组内部的顺序进行更新。
Component System Group
ComponentSystemGroup
类表示应该按照特定顺序一起更新的相关组件系统的列表。ComponentSystemGroup
继承自ComponentSystemBase
,因此,它在所有主要方面都和其他组件系统一样工作——可以相对于其他系统进行排序,具有OnUpdate()方法等等。最重要的是,以为着它也可以和普通系统一样,放到其它Component System Group中,形成嵌套的层次结构。
默认情况下,当调用一个ComponentSystemGroup
的Update()方法时,它会调用存储在排序列表上的每个成员系统的Update()方法。如果该成员系统自身也是一个系统组,就会递归更新自身成员系统。最终的系统更新顺序遵循树的深度优先遍历方式。
系统顺序特性(System Ordering Attributes)
- [UpdateInGroup] —— 指定此系统所属的
ComponentSystemGroup
。如果省略此属性,此系统将默认添加到World
的SimulationSystemGroup
中。 - [UpdateBefore] 和[UpdateAfter] —— 相对于其他系统的顺序。所指定的系统必须是在统一个组内。跨组排序应该在包含两个组的最深组中进行的:
- 例如,如果系统A在分组A中,系统B在分组B中,且分组A和分组B都是分组C的成员,然后,分组A和分组B在分组C中的顺序,隐式的确定了系统A相对系统B的顺序。因此,跨组排序,无需明确的对两个系统进行排序,而是通过所在组之间的相对顺序来确定。
- [DisableAutoCreation] —— 防止在默认世界初始化期间创建此系统。你必须自己显式的创建和更新系统。不过,你也可以将带有此标签的系统添加到某个
ComponentSystemGroup
的更新列表中,然后它就会和其它系统一样自动进行更新。
默认系统组(Default System Groups)
默认的World包含一个ComponentSystemGroup
实例的层次结构。只有三个根级别的系统组添加到 Unity Player loop中(下面列表还显示了每个组中的预定义成员系统):
- InitializationSystemGroup (在 player loop的初始化阶段结束时更新)
- BeginInitializationEntityCommandBufferSystem
- CopyInitialTransformFromGameObjectSystem
- SubSceneLiveLinkSystem
- SubSceneStreamingSystem
- EndInitializationEntityCommandBufferSystem
- SimulationSystemGroup (在player loop的更新阶段结束时更新)
- BeginSimulationEntityCommandBufferSystem
- TransformSystemGroup
- EndFrameParentSystem
- CopyTransformFromGameObjectSystem
- EndFrameTRSToLocalToWorldSystem
- EndFrameTRSToLocalToParentSystem
- EndFrameLocalToParentSystem
- CopyTransformToGameObjectSystem
- LateSimulationSystemGroup
- EndSimulationEntityCommandBufferSystem
- PresentationSystemGroup (在player loop的PreLateUpdate阶段结束时更新)
- BeginPresentationEntityCommandBufferSystem
- CreateMissingRenderBoundsFromMeshRenderer
- RenderingSystemBootstrap
- RenderBoundsUpdateSystem
- RenderMeshSystem
- LODGroupSystemV1
- LodRequirementsUpdateSystem
- EndPresentationEntityCommandBufferSystem
注意,此列表的内容可能会更改
多个Worlds
除了(或代替)上述默认World,你可以创建多个世界。同一个组件系统可以在多个Worlds中实例化,并且每个实例可以在不同的更新顺序下以不同的速率进行更新。
当前,无法手动更新给定世界中的每个系统。但是,你可以控制在哪些世界中创建哪些系统,以及应该将哪些系统添加到哪些现有系统组中。如,自定义WorldB 可以实例化SystemX和SystemY,将SystemX添加到默认World的 SimulationSystemGroup中,将SystemY添加到默认World的PresentationSystemGroup中。
为了支持该例子,Unity提供了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);
}
当实现此接口时,组件系统类的完整列表将在默认World初始化之前传递给类的Initialize()
方法。自定义的引导程序可以遍历此列表,并在所需任何World中创建系统。你可以从Initialize()
方法返回一个系统列表,它们将作为常规,默认世界初始化的一部分创建。
例如 ,这是实现自定义MyCustomBootstrap.Initialize()
的典型过程:
- 创建任何其他Worlds及其顶级
ComponentSystemGroups
。 - 遍历系统类型列表
2.1 向上遍历
ComponentSystemGroups
层次结构以找到此系统类型的顶级组 2.2 如果是在步骤1中中创建的组,在World在创建系统,然后使用group.AddSystemToUpdateList()
把它添加到层次结构中。 2.3 如果不是,将此类型附加到列表以返回到 DefaultWorldInitialization - 在新的顶级组中调用
group.SortSystemUpdateList()
3.1 (可选)将它们添加到默认世界组之一 - 将未处理系统列表返回到 DefaultWorldInitialization
注意:ECS框架通过反射查找你的ICustomBootstrap 实现
Tips 和最佳实践
- 使用[UpdateInGroup]为你编写的每个系统指定系统组。如果未指定,则默认系统组为
SimulationSystemGroup
- 使用手动选中的
ComponentSystemGroups
来更新 Unity player loop 中其它位置的系统。将[DisableAutoCreation]特性添加给组件系统或(系统组)可以防止将其创建或添加到默认系统组中。你仍然可以使用World.GetOrCreateSystem()
手动创建系统,并通过从主线程调用MySystem.Update()
进行更新。这是在Unity player loop中其他位置插入系统的简便方法(如果你的旧系统是运行在Unity 更早的版本中,可以使用这种方式逐步更改) - 尽可能的使用现有的
EntityCommandBufferSystem
而不是添加新的。EntityCommandBufferSystem
表示一个同步点,在该同步点中,主线程会等待所有EntityCommandBuffers
工作线程完成。与创建新的“泡沫”相比,在每个根级系统组中重用预定义的 Begin/End 系统中的其中一个,不太可能引入新的“泡沫”。(这里的泡沫,可能指的是一些不稳定因素) - 避免将自定义逻辑放在
ComponentSystemGroup.OnUpdate()
中。由于ComponentSystemGroup 从功能上来说就是一个组件系统,因此可能会尝试把自定义逻辑添加到它的OnUpdate()
方法中,来执行,产生一些jobs等等。不建议这么做,因为从外部尚不清楚是在组成员更新之前还是字后执行自定义逻辑。最好将系统组限制为一种分组机制,并在相对于该组显式排序的单独组件系统中实现所需逻辑。