来源:https://docs.unity3d.com/Packages/[email protected]/manual/index.html
我会对官方文档内容略作整理,有需要可以查看官方文档
这一部分需要大家了解有关Jobs方面的内容,之后我也会再出文章分析一下:
https://docs.unity3d.com/Manual/JobSystem.html
提供了改变组件中数据状态的逻辑,比如:一个系统会根据两帧之间的间隔时间更新所有会动的实体的位置
Unity中的ComponentSystem(也是标准ECS系统中的一个System)对一个实体而言,它是只包括了方法,他不能包含数据;对比旧的Unity系统而言,他有点像旧的Component系统,但是是一个只包含方法的Component。一个ComponentSystem负责更新一组匹配组件(在一个名为EntityQuery的结构中定义)的实体
UnityECS提供了一个ComponentSystem接口供使用
管理依赖关系很难,这就是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。对于流式传输和程序生成方案,在一个世界中创建实体,然后在一个事务的第一帧将其移动到另一个世界是很有用的。
EntityCommandBuffer解决了两个重要的问题:
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初始化提供了三个系统组,用于初始化、模拟和显示,每个帧按顺序更新。在系统组内,有一个实体命令缓冲系统,它在系统组中的任何其他系统之前运行,而另一个这样的系统在系统组中的所有其他系统之后运行。最好使用现有的命令缓冲区系统之一而不是创建自己的命令缓冲区系统,以便最小化同步点。
当使用EntityCommandBuffer从ParallelForJobs发出EntityManager的命令时,该接口用于保证线程安全性和确定性回放,这个接口中的公共方法采用了一个额外的jobIndex参数,该参数用于以确定的顺序回放记录的命令,这个jobIndex参数对每个Job而言必须具有唯一ID。出于性能原因,jobIndex必须是增加的值传递给IJobParallelFor.Execute(),除非你确切的知道你正在做什么,使用index代替jobIndex是更安全的做法。使用其他jobIndex值会产生正确的输出,但在某些情况下会导致严重的性能影响
使用Component System Groups可以指定系统的更新顺序,你可以通过系统类中声明的[UpdateInGroup]属性将系统放置在一个Group中,您可以使用[UpdateBefore]和[UpdateAfter]属性指定组内的更新顺序
ECS框架创建一组默认系统组(default system groups),你可以在指定帧更新你的系统,你可以将一个组嵌套在另一个组中,以便组中的所有系统都在正确的阶段更新,然后根据组内的顺序进行更新
ComponentSystemGroup类代表了一组按特定顺序更新的相关组件系统的列表,ComponentSystemGroup派生自ComponentSystemBase,因此它在所有重要的方面都像组件系统一样——它可以相对于其他系统进行排序,具有OnUpdate()方法等,最重要的是,这意味着Component System Groups可以嵌套在其他Component System Groups中,形成层次结构
默认情况下,当一个ComponentSystemGroup调用Update()方法时,它会对其成员系统的每个系统上调用Update()。如果任何成员系统本身就是系统组,它们将递归更新自己的成员,生成的系统指令顺序遵循树的深度优先遍历
目前所维护的System Ordering Attributes(系统指令属性)具有略微不同的语义和限制:
默认World包含ComponentSystemGroup实例的层次结构,Unity循环中只添加了三个根级别系统组(以下列表还显示了每个组中预定义的成员系统):
请注意,此列表的具体内容可能会发生变化
除了上述默认世界(也可以代替上述默认世界)之外,你还可以创建多个世界,可以在多个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()实现过程
注意: ECS框架通过反射找到您的ICustomBootstrap实现