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).
在创建系统时,可以实现一组系统生命周期事件函数. Unity ECS 按照以下顺序执行:
OnUpdate()
-- 只要system有工作要做,就每一帧都执行 (see ShouldRunSystem()) 。且系统是开启的 ,注意 OnUpdate 是在ComponentSystemBase 的子类中定义的; 每种类型的系统类都可以定义它自己的update behavior.所有这些函数都是在主线程上执行的. 但是你可以在OnUpdate(JobHandle) 中定义一个继承于JobComponentSystem 的 Jobs ,来让它在子线程中执行
Unity ECS 提供了几种类型的系统.:通常,您编写的用于实现游戏行为和数据转换的系统将扩展ComponentSystem或JobComponentSystem.其他系统类有专门的用途;你通常会使用 Entity Command Buffer System和 Component System Group 类的实例.
ComponentSystem
(在标准ECS术语中也称为系统)执行 entities上的操作. ComponentSystem
不能包含实例数据. 类似于原来unity中的 Component类,但是它只含有方法 , ComponentSystem
负责来更新所有满足条件的实体 ,通过 EntityQuery这个结构体来查询).
Unity ECS提供了一个你可以在你的代码中扩展的抽象类 ComponentSystem
See file: /Packages/com.unity.entities/Unity.Entities/ComponentSystem.cs.
管理依赖关系很困难. 这也是 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);
}
}
所有的 jobs和 systems都声明它们可以读写的组件类型 ComponentTypes . 当一个JobComponentSystem 返回一个 JobHandle,它会在EntityManager中自动注册,包括它可以读写的组件类型的所有信息
因此如果一个 system 写入 component A
, 另一个系统稍后读取component A
,然后JobComponentSystem查看它正在读取的类型列表 ,因此,从第一个系统向您传递对作业的依赖项。
JobComponentSystem
只需将job作为依赖项链到需要的地方,这样就不会在主线程上造成任何阻塞.但是如果一个non-job ComponentSystem
访问相同的数据? 因为所有的访问都是已经声明过的, 所以 ComponentSystem
在调用OnUpdate之前,自动完成所有针对组件类型的jobs
Dependency management是保守的. ComponentSystem
之追踪所有 EntityQuery
对象的使用 ,存贮基于查找的对象 ,正在写入或读取的类型
在单一系统中调度多个jobs时也是如此, 即使不同的jobs可能需要较少的依赖项,也必须将所有的依赖项传递给所有job.如果这被证明是一个性能问题,那么最好的解决方案是将一个系统一分为二。
所有的结构变化都有严格的同步点. 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.
没一个 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.
The EntityCommandBuffer
类解决了两个问题·:
EntityManager
.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将会根据他们创建的顺序执行,可以集中管理内存,并保证生成的实体和组件的确定性。
默认的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.
当从 ParallelFor jobs中使用EntityCommandBuffer
来解决 EntityManager
的命令,EntityCommandBuffer.Concurrent
接口用于保证线程安全和确定性.此接口中的公共方法需要额外的jobIndex
参数, 用来确定记录的顺序. jobIndex
对每一个job来说必须是一个唯一的 ID. 由于性能的原因, jobIndex
应该是(增加) index
值传递给 IJobParallelFor.Execute()
. 除非你真的知道你在做什么, using 将该index
用作jobIndex是最安全的选择.使用其他jobIndex值会有正确的结果输出,但在某些情况下可能会产生严重的性能影响
使用 Component System Groups来确定系统更新的顺序. 你可以在系统类声明的时候添加[UpdateInGroup]属性,从而把系统放到组内. 然后使用 [UpdateBefore] 和 [UpdateAfter] 属性来确定在组内执行的顺序
ECS创建了一套 default system groups, 您可以使用它在框架的正确阶段更新系统.您可以将一个组嵌套到另一个组中,以便在正确的阶段更新组中的所有系统,然后,还可以根据组中的顺序进行更新。
ComponentSystemGroup类表示应按特定顺序一起更新的相关component systems组件系统的列表。 ComponentSystemGroup 继承自ComponentSystemBase, 所以它在所有重要的方面都像一个组件系统 component system ,比如 它可以相对于其他系统进行排序, 含有 OnUpdate() 方法, etc. 最重要的是,这意味着component system groups 可以嵌套在其他component system groups, 形成一个层次结构。
默认情况下,当一个 ComponentSystemGroup’s Update()
方法被调用,它调用在其已排序的成员系统列表中的每个系统上的Update() ,如果任何成员系统本身就是系统组system groups, 它们将递归地更新自己的成员. 结果的系统排序遵循树的深度优先遍历。
现有维持系统执行顺序的属性,语义和限制略有不同
默认 World包含一个 ComponentSystemGroup 层级的实例. 只有三个根级系统组被添加到Unity播放器循环(下面的列表还显示了每个组中预定义的成员系统):
Initialization
循环的末尾执行)
Update循环的末尾执行)
PreLateUpdate
循环末尾执行)
请注意,此列表的具体内容可能会更改。
你可以创建多个 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()
:
group.AddSystemToUpdateList()添加到world中
Note: the ECS framework 通过反射自动查找你的 ICustomBootstrap 实现
EntityCommandBufferSystem
s,而不是重新创建一个. 一个 EntityCommandBufferSystem
代表一个同步点,需要主线程等待工作线程访问完所有的外部 EntityCommandBuffer
s,再继续工作. 重复利用预先定义的Begin/End systemsComponentSystemGroup.OnUpdate()中放入自己的逻辑
. 因为ComponentSystemGroup
功能上是一个组件系统,因为从外部不能够马上就知道你写的逻辑是否在组内成员更新的时候执行了 ,最好把系统组限制在一个分组机制中 keep system groups limited to a grouping mechanism,并在一个单独的组件系统中实现逻辑,然后在组内给这个系统显示的排序