Unity DOTS:Systems部分
Systems
一个system,也就是ECS里的S,提供了将component的数据从其当前状态变换到其下一个状态的逻辑-例如,一个system可以通过velocity乘以Time.deltaime来更新所有可移动entities的位置。
Instantiating systems
Unity ECS自动在您的项目中发现system类型,并在运行时实例化它们。它将每个发现的system添加到默认system groups之一中。您可以使用system attributes来指定system的父组以及该system在该group中的顺序。如果未指定父项,则Unity将以确定性的,但并未指定顺序的将system添加到默认世界的Simulation system group中。您也可以使用attribute禁用自动创建。
system的更新循环由其父ComponentSystemGroup驱动。ComponentSystemGroup本身是一种特殊的system,负责更新其child systems。group可以嵌套。system从运行的world获取time数据;time由UpdateWorldTimeSystem更新。
您可以使用Entity Debugger window(menu: Window > Analysis > Entity Debugger)查看system configuration。
System类型
Unity ECS提供了几种类型的systems。通常,为实现游戏行为和数据转换而编写的system将扩展SystemBase。其他system类具有特殊目的。比如,通常情况下,您使用EntityCommandBufferSystem和ComponentSystemGroup类的现有实例,而不是自己再进行拓展。
- SystemBase-创建自定义system时要实现的基类。
- EntityCommandBufferSystem-为其他systems提供EntityCommandBuffer实例。每个默认system group在其child system列表的开头和结尾都维护一个“ Entity Command Buffer System”。这使您可以对结构更改进行分组,以使它们在框架中产生更少的syncronization points。
- ComponentSystemGroup-为其他systems提供嵌套的组织和更新顺序。默认情况下,Unity ECS创建多个Component System Groups。
- GameObjectConversionSystem-将游戏的GameObject的转换为高效的entityGame conversion systems在Unity编辑器中运行。
重要提示:ComponentSystem和JobComponentSystem类,以及IJobForEach,这些都是被淘汰的DOTS API,但是还没有官宣。请改用SystemBase和Entities.ForEach。
创建一个system
实现抽象类SystemBase创建来ECS中的systems。
要创建system,需要实现必要的一些生命周期函数回调。使用SystemBase OnUpdate()函数执行system必须在每一帧中完成的工作。其他回调函数是可选的。例如,您可以使用OnCreate()来初始化system,但并非每个系统都需要初始化。
system回调函数按以下顺序调用:
- OnCreate() -创建system时调用。
- OnStartRunning() -在第一个OnUpdate()之前以及每当system恢复运行时调用。
- OnUpdate() -system有工作要做的每一帧(请参见ShouldRunSystem())并且系统已启用时调用。
- OnStopRunning()-每当system停止更新时调用,这可能是由于将Enabled设置为false或因为找不到与它的query匹配的entities。在OnDestroy()之前也会调用。
- OnDestroy() -销毁system时调用。
system的OnUpdate()函数由其父系统组自己的OnUpdate()函数触发。同样,当group更改状态时,例如,如果您设置了该group的Enabled属性,它也会更改其子systems的状态。但是,子systems也可以独立于parent group而改变状态。有关更多信息,请参见system update order。
所有system生命周期函数都在主线程上运行。理想情况下,您的OnUpdate()函数可以schedules jobs以执行大部分工作。要从system schedules jobs,您可以使用以下机制之一:
- Entities.ForEach-迭代ECS component数据的最简单方法。
- Job.WithCode-将lambda函数作为单个后台job执行。
- IJobChunk-一种“底层”机制,用于逐chunk迭代ECS components数据。
- C#Job System -创建和规划通用的C# Job。
以下示例说明了使用Entities.ForEach来实现一种system,该system根据一个component来更新另一个component的值:
1 | public struct Position : IComponentData |
使用Entities.ForEach创建systems
使用SystemBase类提供的Entities.ForEach构造作为在entities及其components上定义和执行算法的简洁方法。Entities.ForEach在由entity query选择出的的所有entities上执行您定义的lambda函数。
要执行job Lambda函数,您可以使用Schedule()
和ScheduleParallel()
来schedule job,或者使用Run()
来立即执行该job(在主线程上)。您可以使用在Entities.ForEach上定义的其他方法来设置entity query以及各种job选项。
下面的示例说明了一个简单的SystemBase实现,该实现使用Entities.ForEach读取一个entity的component(为Velocity)并写入另一个component(Translation):
1 | class ApplyVelocitySystem : SystemBase |
请注意ForEach lambda函数的参数关键字ref
以及in
的使用。使用ref
修饰的component,可读写,使用in
修饰的component,只读。将component标记为只读可帮助job scheduler程序更有效地执行job。
选择entities
Entities.ForEach提供了自己的机制,用于定义用于选择要处理的entities的entity query。该query自动包括您符合lambda函数参数的所有components。你也可以使用WithAll
,WithAny
及WithNone
条款,以进一步细化哪些entities被选中。有关query选项的完整内容,请参见SystemBase.Entities。
下面的示例选择具有以下components的entities:Destination,Source和LocalToWorld。并具有Rotation,Translation或Scale中的至少一项;但没有LocalToParent。
1 | Entities.WithAll<LocalToWorld>() |
在此示例中,在lambda函数内部只能访问目标和源components,因为它们是参数列表中的唯一的components。
访问EntityQuery对象
要访问Entities.ForEach创建的EntityQuery对象,请使用[WithStoreEntityQueryInField(ref query)]和ref参数修饰符。此函数将query的引用分配给您提供的字段。
下面的示例说明如何访问为Entities.ForEach构造隐式创建的EntityQuery对象。在这种情况下,该示例使用EntityQuery对象调用CalculateEntityCount()方法。该示例使用此计数来创建一个本地化数组,该数组具有足够的空间来为query选择的每个entity存储一个值:
1 | private EntityQuery query; |
可选components
您无法创建指定可选component的query(使用WithAny<T,U>
),也无法在lambda函数中访问那些components。如果需要读取或写入可选component,则可以将Entities.ForEach构造拆分为多个job(每个可选components的组合是一个job)。
例如,如果您有两个可选components,有两种方案
- 需要三个ForEach构造:一个包含第一个可选component,一个包含第二个可选component,一个包含两个components
- 另一种选择是使用IJobChunk逐chunk进行迭代。
更改filtering(过滤选项)
如果自上次运行当前SystemBase实例以来,仅想在该component的另一个entity发生更改时处理该entity component,则可以使用WithChangeFilter <T>
启用更改filtering。更改filter中使用的component类型必须处于lambda函数参数列表中,或者必须是WithAll <T>
语句的一部分。
1 | Entities |
entity query最多支持两种component类型的change filtering。
请注意,change filtering应用于chunk级别。如果有任何代码通过写访问权限访问了chunk中的某个component,则该chunk中该component的类型将标记为已更改-即使该代码实际上并未更改任何数据。
Shared component filtering
具有shared component的entity与其他具有相同shared component值的entities分组在一起。您可以使用WithSharedComponentFilter()函数选择具有特定shared component值的entities group。
以下示例选择按Cohort ISharedComponentData分组的entities。此示例中的lambda函数根据entity的Cohort设置DisplayColor IComponentData组件:
1 | public class ColorCycleJob : SystemBase |
该示例使用EntityManager来获取所有不同的的Cohort值。然后,它为每个Cohort调度一个lambda job,将新颜色作为捕获变量传递给lambda函数。
定义ForEach函数
定义与Entities.ForEach一起使用的lambda函数时,可以声明参数,SystemBase类在执行该函数时使用该参数传递有关当前entity的信息。
典型的lambda函数如下所示:
1 | Entities.ForEach( |
默认情况下,您最多可以将八个参数传递给Entities.ForEach lambda函数。(如果需要传递更多参数,则可以定义自定义委托。)使用标准委托时,必须按以下顺序对参数进行分组:
1 | Parameters passed-by-value first(参数传递的值) (no parameter modifiers(无修饰符)) |
所有components都应使用ref
或in
参数修饰符。否则,传递给您的函数的components struct是副本而不是引用。这意味着为只读参数提供了额外的内存副本,并且意味着在函数返回后(复制的结构超出范围时)对要更新的component的任何更改都会被静默丢弃。
如果您的函数不遵守这些规则,并且您尚未创建合适的委托,则编译器将提供类似于以下内容的错误:
1 | error CS1593: Delegate 'Invalid_ForEach_Signature_See_ForEach_Documentation_For_Rules_And_Restrictions' does not take N argumentscs |
(请注意,即使问题是参数顺序,错误消息也会将会是这个报错。)
自定义委托
您可以在ForEach lambda函数中使用8个以上的参数。通过声明自己的委托类型和ForEach重载。这使您可以根据需要使用任意数量的参数,并以任意顺序放置ref / in / value参数。
你可以在任何地方声明有三个特殊,命名参数 entity
,entityInQueryIndex
和nativeThreadIndex
参数列表的委托。但请勿对这些参数使用ref
或in
修饰符。
1 | static class BringYourOwnDelegate |
**注意:**ForEach lambda函数默认八个参数限制,因为声明太多的委托和重载会对IDE性能产生负面影响。ref / in / value和参数数量的每种组合都需要唯一的委托类型和ForEach重载。
Component参数
要访问与entity关联的component,您必须将该component类型的参数传递给lambda函数。编译器会自动将传递给函数的所有components作为必需components添加到entity query中。
要更新component值,必须使用ref
参数列表中的关键字通过引用将其传递给lambda函数。(没有ref
关键字,将对component的临时副本进行任何修改,因为它将通过值进行传递。)
要将传递给lambda函数的components指定为只读,请在参数列表中使用in
关键字。
**注意:**使用ref
表示当前chunk中的components被标记为已更改,即使lambda函数实际上并未对其进行修改。为了提高效率,请始终使用in
关键字将lambda函数不会修改的components指定为只读。
以下示例将Source component作为只读参数传递给job,并将Destination component作为可写参数传递给作业:
1 | Entities.ForEach( |
**注意:**当前,您不能将chunk component传递给Entities.ForEach lambda函数。
对于dynamic buffers,请使用DynamicBuffer <T>
而不是存储在buffers中的Component类型:
1 | public class BufferSum : SystemBase |
特殊的命名参数
除了component之外,您还可以将以下特殊的命名参数传递给Entities.ForEach lambda函数,这些参数是根据job当前正在处理的entity分配的值:
Entity entity
—当前entity的Entity实例。(参数的名称可以是任何类型,只要类型是Entity。)int entityInQueryIndex
—该entity在query选择的所有entities的列表中的索引。当您有一个本地化数组需要为每个entity填充一个唯一值时,请使用entityInQueryIndex。您可以将entityInQueryIndex用作该数组中的索引。EntityInQueryIndex也应用作sortKey
将命令添加到并发EntityCommandBuffer。int nativeThreadIndex
—执行lambda函数当前迭代的线程的唯一索引。使用Run()执行lambda函数时,nativeThreadIndex始终为零。(不要将nativeThreadIndex
用作并发EntityCommandBuffer的sortKey
;请改为使用entityInQueryIndex
。)
捕获变量
您可以捕获Entities.ForEach lambda函数的局部变量。使用job执行函数时(通过调用Schedule几个函数之一而不是Run),对捕获的变量及其使用方式有一些限制:
- 只能捕获本地化容器和可漂白类型。
- job只能写入类型为本地化容器的捕获变量。(要“返回”单个值,请使用一个元素创建一个本地化数组。)
如果您读取了[本地化容器],但未写入该容器,请始终使用来指定只读访问权限WithReadOnly(variable)
。有关设置捕获变量的属性的更多信息,请参见SystemBase.Entities。您可以指定的属性包括NativeDisableParallelForRestriction
及其他。Entities.ForEach将这些作为函数提供,因为C#语言不允许在局部变量上使用attribute。
您还可以使用表示要在Entities.ForEach运行之后Dispose捕获的NativeContainer或包含NativeContainers的类型WithDisposeOnCompletion(variable)
。这将在lambda运行之后立即Dispose类型(对于Run()
),或者安排它们稍后通过Job进行Dispose并返回JobHandle(对于Schedule()
/ ScheduleParallel()
)。
**注意:**在通过Run()
执行函数时,您可以写入不是本地化容器的捕获变量。但是,您仍应尽可能使用blittable类型,以便可以使用Burst编译函数。
支持的功能
您可以使用在主线程上使用Run()
执行lambda函数,在单个后台线程使用Schedule()
执行job,或者使用ScheduleParallel()
来让多线程并行执行。这些不同的执行方法对访问数据的方式具有不同的约束。另外,Burst使用C#语言的受限子集,在此子集之外使用C#功能(包括访问托管类型)时您需要指定WithoutBurst()
。
下表显示了Entities.ForEach当前支持哪些功能,用于SystemBase中可用的各种计划方法:
支持功能 | Run | Schedule | ScheduleParallel |
---|---|---|---|
捕获局部值类型 | X | X | X |
捕获局部引用类型 | x(仅不带Burst) | ||
写入捕获的变量 | X | ||
System命名空间下的字段 | x(仅不带Burst) | ||
引用类型的方法 | x(仅不带Burst) | ||
Shared Components | x(仅不带Burst) | ||
Managed Components | x(仅不带Burst) | ||
结构体变化 | x(仅不带Burst和WithStructuralChanges) | ||
SystemBase.GetComponent | X | X | X |
SystemBase.SetComponent | X | X | |
GetComponentDataFromEntity | X | X | x(仅作为ReadOnly) |
HasComponent | X | X | X |
WithDisposeOnCompletion | X | X | X |
一个Entities.ForEach构造使用专门的中间语言(IL)编译后处理你写的代码来转换成正确的ECS的代码。这种自动翻译使您无需包含复杂的样板代码即可表达算法的意图。但是,这可能意味着不允许使用某些常见的代码编写方式。
当前不支持以下功能:
不支持的功能 |
---|
Dynamic code in .With invocations |
被ref修饰的SharedComponent参数 |
嵌套的Entities.ForEach lambda expressions |
标有[ExecuteAlways]的系统中的Entities.ForEach(当前已修复) |
使用存储在变量,字段或方法中的委托进行调用 |
具有lambda参数类型的SetComponent |
具有可写lambda参数的GetComponent |
Lambdas中的泛型参数 |
在具有泛型参数的systems中 |
Dependencies
默认情况下,系统使用其Dependency属性管理与ECS相关的依赖关系。默认情况下,系统将按Entities.ForEach和[Job.WithCode] 创建的每个job按它们在OnUpdate()函数中出现的顺序添加到Dependency job句柄中。您还可以通过将[JobHandle]传递给函数来手动管理job依赖关系,然后返回结果依赖关系。有关更多信息,请参见依赖性。Schedule
有关job依赖性的更多常规信息,请参见job依赖性。
使用Job.WithCode创建Systems
SystemBase类提供的Job.WithCode构造是一种将函数作为单个后台job运行的简便方法。您甚至可以在主线程上运行Job.WithCode,并且仍然可以利用Burst编译来加快执行速度。
以下示例使用一个Job.WithCode lambda函数用随机数填充本地化数组,并使用另一个job将这些数字加在一起:
1 | public class RandomSumJob : SystemBase |
**注意:**要运行并行的job,请实现IJobFor,您可以使用系统OnUpdate()函数中的ScheduleParallel()进行调度。
变量
您不能将参数传递给Job.WithCode lambda函数或返回一个值。取而代之的是,您可以在OnUpdate()函数中捕获局部变量。
当你在C#Job System中使用Schedule()
调度你的job时,还有额外的限制:
- 捕获的变量必须声明为 NativeArray-或其他本地化容器 -或blittable类型。
- 要返回数据,即使数据是单个值,也必须将返回值写入捕获的本地化数组。(请注意,使用
Run()
时,您可以写入任何捕获的变量。)
Job.WithCode提供了一组函数,以将只读属性和安全属性应用于捕获的本地化容器变量。例如,您可以用WithReadOnly
来指定您不更新容器,并用WithDisposeOnCompletion
在job结束后自动处理容器。(Entities.ForEach提供相同的功能。)
有关这些修饰符和属性的更多信息,请参见Job.WithCode。
执行函数
您有两种选择来执行lambda函数:
Schedule()
-将功能作为单个非并行job执行。调度job在后台线程上运行代码,因此可以更好地利用可用的CPU资源。Run()
-在主线程上立即执行功能。在大多数情况下,可以对Burst.WithCode进行Burst编译,因此即使Job.WithCode仍在主线程上运行,其执行代码也可以更快。
请注意,调用会Run()
自动完成Job.WithCode构造的所有依赖关系。如果未明确为Run()
system传入JobHandle对象,则假定当前Dependency属性表示该函数的依赖关系。(如果函数没有依赖关系,请传入新的JobHandle。)
依存关系
默认情况下,system使用其Dependency属性管理与ECS相关的依赖关系。system将按Entities.ForEach和Job.WithCode创建的每个job按它们在OnUpdate()函数中出现的顺序添加到Dependencyjob句柄中。您还可以通过将JobHandle传递给函数来手动管理job依赖性,然后将其返回结果依赖性。有关更多信息,请参见依赖性。Schedule
有关作业依赖性的更多常规信息,请参见作业依赖性。
使用IJobChunk jobs创建System
您可以在system内部实现IJobChunk,以逐chunk遍历数据。当您在OnUpdate()
功能中调度IJobChunk job时,该job为每个能匹配上由entity query传递给job Schedule()
的chunk调用Excute()
。然后,您可以逐entity地遍历每个chunk内的数据。
与Entities.ForEach相比,使用IJobChunk进行迭代需要更多的代码设置,但是也更明确,并且代表对数据的最直接访问,因为它才是真正被存储的对象。
按chunk进行迭代的另一个好处是,您可以使用Archetype.Has<T>()
来检查每个chunk中是否存在可选component,然后相应地处理chunk中的所有entities。
要实现IJobChunk job,请使用以下步骤:
- 创建一个
EntityQuery
以标识要处理的entities。 - 定义job结构,并包括
ArchetypeChunkComponentType
对象的字段,这些字段标识job必须直接访问的components的类型。另外,指定job是读还是写这些component。 - 实例化job结构并在system
OnUpdate()
函数中调度job。 - 在该
Execute()
函数中,获取job读或写的component的NativeArray
实例,然后在当前chunk上进行迭代以执行所需的工作。
有关更多信息,ECS samples repository包含一个简单的HelloCube示例,演示了如何使用IJobChunk
。
使用EntityQuery查询数据
EntityQuery定义了archetype必须包含的一组components类型,system才能处理其关联的chunks和entities。archetype可以具有其他components,但是它必须至少包含EntityQuery定义的component。您还可以排除包含特定类型components的archetype。
对于简单query,可以使用该SystemBase.GetEntityQuery()
函数并按如下所示传入component类型:
1 | public class RotationSpeedSystem : SystemBase |
对于更复杂的情况,您可以使用EntityQueryDesc
。一个EntityQueryDesc
提供了灵活的查询机制,以指定的组件类型:
All
:此数组中的所有components类型必须存在于archetype中Any
:archetype中必须存在此数组中的至少一种component类型None
:archetype中不能存在此数组中的任何component类型
例如,以下查询包括包含RotationQuaternion
和RotationSpeed
component的archetypes,但不包括包含Frozen
component的任何archetype:
1 | protected override void OnCreate() |
查询使用ComponentType.ReadOnly<T>
而不是更简单的typeof
表达式是为了指定system只读RotationSpeed
。
您还可以组合多个query。为此,请传递EntityQueryDesc
对象数组而不是单个实例。ECS使用逻辑或运算来组合每个query。下面的示例选择包含一个RotationQuaternion
或多个RotationSpeed
components(或两者都有)的任何archetype:
1 | protected override void OnCreate() |
**注意:**请勿在EntityQueryDesc
中完全包含可选components。要处理可选components,请使用在IJobChunk.Execute()
中的chunk.Has<T>()
方法确定当前ArchetypeChunk是否具有可选components。因为同一chunk中的所有entities具有相同的components,所以您只需要每个chunk检查一个可选component是否存在一次就可以了:而不是每个entity一次。
为了提高效率并避免不必要地创建会有GC的引用类型,应在systemOnCreate()
方法中为system创建EntityQueries
,然后将结果存储在实例变量中。(在以上示例中,m_Query
变量正是如此。)
定义IJobChunk结构
IJobChunk结构为job运行时所需的数据以及job的Execute()
方法定义字段。
要访问system传递给您的Execute()
方法的chunk内的component数组,必须为job读取或写入的每种类型的componnet创建一个ArchetypeChunkComponentType<T>
对象。您可以使用这些对象来获取NativeArray
实例,这些实例提供对entities components的访问。包括Execute()
方法读取或写入的job的EntityQuery中引用的所有components。您还可以为未包含在EntityQuery中的可选component类型提供ArchetypeChunkComponentType
变量。
在尝试访问当前chunk之前,必须检查以确保当前chunk具有可选component。例如,HelloCube IJobChunk示例声明了一个job结构,该结构定义了两个components的ArchetypeChunkComponentType<T>
变量。分别是RotationQuaternion
和RotationSpeed
:
1 | [ ] |
system在OnUpdate()
为函数中的这些变量分配值。ECS 在运行job时会使用Execute()
方法内部的变量。
该job还使用Unity delta时间为3D对象的旋转设置动画。该示例使用struct字段将此值传递给Execute()
方法。
编写Execute方法
IJobChunk Execute()
方法的签名为:
1 | public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex) |
chunk
参数是内存块的句柄,该内存块包含此job的迭代必须处理的entities和components。因为chunk只能包含一个archetype,所以chunk中的所有entities都具有相同的component集。
使用chunk
参数获取component的NativeArray实例:
1 | var chunkRotations = chunk.GetNativeArray(RotationTypeHandle); |
这些数组是对齐的,以便entities在所有数组中具有相同的索引。然后,您可以使用正常的for循环来遍历components数组。使用chunk.Count
得到存储在当前chunk的entities的数量:
1 | var chunkRotations = chunk.GetNativeArray(RotationTypeHandle); |
如果您在EntityQueryDesc中具有Any
过滤器,或是完全没有在query中出现的可选components,则可以在使用之前使用该函数测试当前chunk是否包含这些ArchetypeChunk.Has<T>()
组件之一:
1 | if (chunk.Has<OptionalComp>(OptionalCompType)) |
**注意:**如果使用并发形式的entity command buffer,请将chunkIndex
参数作为sortKey
参数传递给command buffer函数。
跳过那些内容都是未变化的entities的chunk
如果仅在components值更改后才需要更新entities,则可以将该component类型添加到EntityQuery的change filter中。例如,如果您的system读取两个components,并且仅在前两个components中的一个已更改时才需要更新第三个component,则可以按以下方式使用EntityQuery:
1 | private EntityQuery m_Query; |
EntityQuery的change filter最多支持两个componnets。如果您想进行更多检查或不使用EntityQuery,则可以手动进行检查。要进行此检查,请使用ArchetypeChunk.DidChange()
函数将component的chunk的change version与system的LastSystemVersion
进行比较。如果此函数返回false,则可以完全跳过当前chunk,因为自从上次system运行以来,该类型的component均未更改。
您必须使用一个struct字段将LastSystemVersion
从system传递到job中,如下所示:
1 | [ ] |
与所有job结构字段一样,在调度job之前,必须分配其值:
1 | protected override void OnUpdate() |
**注意:**为了提高效率,change version适用于整个chunk,而不是单个entity。如果另一个具有写入该类型component功能的job访问了chunk,则ECS会对该component的change version进行递增,并且DidChange()
函数将返回true。即使声明对component进行写访问的job实际上并未更改component,ECS也会递增change version。
实例化并调度job
若要运行IJobChunk job,必须创建job结构的实例,设置结构字段,然后调度job。在SystemBase的OnUpdate()
中执行此操作时,system会将每帧调度job。
1 | protected override void OnUpdate() |
调用GetArchetypeChunkComponentType<T>()
函数设置component类型变量时,请确保将job读取但不写入的components的isReadOnly
参数设置为true。正确设置这些参数可能会对ECS框架调度job的效率产生重大影响(害怕)。这些访问模式设置必须在结构定义和EntityQuery中都与它们的等效项匹配。
不要在system类的变量中缓存GetArchetypeChunkComponentType<T>()
的返回值。您必须在每次system运行时调用该函数,并将更新后的值传递给job。
通过手动迭代创建Systems
您可以在NativeArray中显式请求所有chunks,并使用诸如IJobParallelFor
的job来处理它们。如果适用于简化迭代EntityQuery中所有chunk的简化模型满足不了您的需求,则应使用此方法。以下是一个示例:
1 | public class RotationSpeedSystem : SystemBase |
手动迭代
您可以使用EntityManager类手动遍历entities或chunk,尽管这不是最佳实践。您只应在测试或调试代码中(或仅在进行实验时)或在您拥有完全可控的实体集的独立world中使用这些迭代方法。
例如,以下代码段循环访问活动世界中的所有entities:
1 | var entityManager = World.Active.EntityManager; |
此代码段循环遍历活动世界中的所有chunks:
1 | var entityManager = World.Active.EntityManager; |
Systems更新顺序
使用Component System Groups来指定system的更新顺序。您可以使用system类声明中的[UpdateInGroup]attribute将system放在group中。然后,您可以使用[UpdateBefore]和[UpdateAfter]attribute来指定其在group中的更新顺序。
ECS框架会创建一组default system groups,可用于在框架的恰当阶段更新systems。您可以将一个group嵌套在另一个group中,以便group中的所有systems在恰当的阶段进行更新,并根据其在group中的顺序进行更新。
Component System Groups
ComponentSystemGroup类表示应按特定顺序一起更新的相关Component Systems的列表。ComponentSystemGroup是从ComponentSystemBase派生的,因此在所有重要的方面它都像Component System一样工作-可以相对于其他systems进行排序,具有OnUpdate()方法等。最重要的是,这意味着可以将Component System Group嵌套在其他Component System Group中,形成一个层次结构。
默认情况下,当调用ComponentSystemGroup的Update()
方法时,它将在其成员system的排序列表中的每个system上调用Update()。如果任何成员system本身就是system group,则它们将递归更新自己的成员。这种情况下生成的system顺序遵循树的深度优先遍历规则。
System Ordering Attributes
现有的system ordering attribute会被保留,但语义和限制稍有不同。
- [UpdateInGroup]-指定此system应该是其成员的ComponentSystemGroup。如果省略此attribute,system将被自动添加到默认的World’s SimulationSystemGroup(请参见下文)。
- [UpdateBefore]和[UpdateAfter]-是相对于其他systems的system ordering。为这些attribute指定的system类型必须是同一组的成员。跨组的排序是在包含两个系统的适当的最深组中进行的:
- **例如:**如果SystemA在GroupA中,而SystemB在GroupB中,并且GroupA和GroupB都是GroupC的成员,则GroupA和GroupB的顺序将隐式确定SystemA和SystemB的相对顺序;无需对system进行明确排序。
- [DisableAutoCreation]-防止在默认World初始化期间创建system。您必须显式创建和更新system。但是,您可以将带有此attribute的system添加到ComponentSystemGroup的更新列表中,然后它将像该列表中的其他systems一样自动进行更新。
Default System Groups
默认的World包含ComponentSystemGroup实例的层次结构。只有三个根级别的system groups被添加到Unity Player循环(以下列表还显示了每个group中的预定义成员systems):
- 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
请注意,此列表的具体内容可能会更改(经典迭代)。
多个Worlds
除了(或代替上述)默认World,您可以创建多个World。同一component system类可以在多个world中实例化,并且每个实例可以在更新顺序的不同点以不同的速率进行更新。
当前还无法指定World中的每个system机进行手动更新。但是,您可以控制在哪个World中创建哪些systems,以及应将其添加到哪些现有system groups中。比如,自定义WorldB可以实例化SystemX和SystemY,将SystemX添加到默认的World’s SimulationSystemGroup,并将SystemY添加到默认的World’s PresentationSystemGroup。这些systems可以像往常一样相对于其group同级对其进行排序,并将与相应的groups一起进行更新。
为了支持此种情况,现在提供了新的ICustomBootstrap接口:
1 | public interface ICustomBootstrap |
当实现此接口时,component system类型的完整列表将在默认世界初始化之前传递给classes Initialize()
方法。自定义的引导程序可以遍历此列表,并在所需的任何World中创建systems。您可以从Initialize()方法返回systems列表,它们将作为常规的默认world初始化的一部分创建。
例如,以下是自定义MyCustomBootstrap.Initialize()
实现的典型过程:
- 创建任何其他Worlds及其顶层ComponentSystemGroups。
- 对于system类型列表中的每个类型:
- 向上遍历ComponentSystemGroup层次结构以找到此system Type的顶级group。
- 如果它是在步骤1中创建的groups之一,请在该world中创建system,然后使用
group.AddSystemToUpdateList()
将其添加到层次结构中。 - 如果不是,请将此类型附加到列表以返回到DefaultWorldInitialization。
- 在新的顶级组上调用group.SortSystemUpdateList()。
- (可选)将它们添加到默认世界组之一
- 将未处理systems的列表返回给DefaultWorldInitialization。
注意: ECS框架通过反射查找您的ICustomBootstrap实现。
提示和最佳实践
- **使用[UpdateInGroup]为您编写的每个system指定system group。**如果未指定,则隐式默认组为SimulationSystemGroup。
- **使用手动选定的ComponentSystemGroups来更新Unity播放器循环中其他位置的system。**将[DisableAutoCreation]属性添加到component system(或system group)可防止将其创建或添加到默认system group。您仍然可以使用World.GetOrCreateSystem手动创建system并通过从主线程手动调用MySystem.Update()进行更新。这是在Unity Player循环中的其他位置插入system的简便方法(例如,如果您的system应在框架中的更早或更晚的时候运行)。
- **如果可能的话,请使用现有的EntityCommandBufferSystem而不是添加新的。**An
EntityCommandBufferSystem
代表一个sync point,在该sync point,主线程在处理任何未完成的EntityCommandBuffer
s 之前等待工作线程完成。与创建新的“气泡”(这个东西暂时没想好怎么翻译,应该和冒泡的事件传递机制一个意思)相比,在每个顶级system group中重用预定义的Begin / End system之一不太可能在帧管线中引入新的“气泡”。 - 避免在
ComponentSystemGroup.OnUpdate()
中加入自定义逻辑。由于从ComponentSystemGroup
功能上来说本身就是一个component system,因此可能很想在其OnUpdate()方法中添加自定义处理,执行一些工作,生成一些工作等。我们通常建议不要这样做,因为从外部尚不清楚自定义逻辑是在更新组成员之前或之后执行。最好将system group限制为一种分组机制,并在相对于该组显式排序的单独的component system中实现所需的逻辑。
Job dependencies
Unity根据system读取和写入的ECS component分析每个system的数据依赖性。如果在框架中较早更新的system读取了较新system写入的数据,或写入了较新system读取的数据,则第二个system将依赖于第一个system。为避免出现竞争状况,job调度程序确保在运行system job之前,system所依赖的所有jobs均已完成。
system的Dependency属性是JobHandle,代表与system的ECS相关的依赖关系。在OnUpdate()之前,Dependency属性反映了system对先前job的传入依赖关系。默认情况下,system根据您在system中调度job时读取和写入的component来更新Dependency属性。
要覆盖此默认行为,请使用Entities.ForEach和Job.WithCode的重载版本,这些重载版本将job依赖项作为参数,并将更新后的依赖项作为JobHandle返回。使用这些构造的显式版本时,ECS不会自动将job handles与system的Dependency属性结合在一起。您必须在需要时手动组合它们。
请注意,system的Dependency属性不会跟踪job对通过NativeArrays或其他类似容器传递的数据可能具有的依赖关系。如果您在一个job中编写NativeArray并在另一个job中读取该数组,则必须手动添加第一个job的JobHandle作为第二个job的依赖项(通常使用JobHandle.CombineDependencies)。
当您调用Entities.ForEach.Run()时,作业调度程序会在开始ForEach迭代之前完成system所依赖的所有调度job。如果您还使用WithStructuralChanges()作为构造的一部分,则job调度程序将完成所有正在运行和待调度的jobs。结构更改还会使对component数据的任何直接引用无效。有关更多信息,请参见Sync Point。
有关更多信息,请参见JobHandle和依赖项。
查找数据
访问和修改ECS数据的最有效方法是使用带有实体查询和作业的系统。这样可以以最少的内存高速缓存未命中来最佳利用CPU资源。实际上,数据设计的目标之一应该是使用最有效,最快的路径来执行大部分数据转换。但是,有时您需要在程序的任意位置访问任意实体的任意组件。
给定一个Entity对象,您可以在其IComponentData和动态缓冲区中查找数据。该方法根据您的代码是在系统中使用Entities.ForEach还是使用IJobChunk作业还是在主线程上的其他位置执行而有所不同。
在Systems中查找entities数据
使用GetComponent (Entity)从system的Entities.ForEach或[Job.WithCode]函数内部查找存储在任意entities components中的数据。
例如,如果您的“目标”component的“实体”字段定义了目标entity,则可以使用以下代码将entity向其目标旋转:
1 | public class TrackingSystem : SystemBase |
访问存储在dynamic buffers中的数据需要额外的步骤。您必须在OnUpdate()方法中声明BufferFromEntity类型的局部变量。然后,您可以在lambda函数中“捕获”局部变量。
1 | public struct BufferData : IBufferElementData |
在IJobChunk中查找entity数据
要随机访问IJobChunk或其他job结构中的component数据,请使用以下类型之一来获取component的类似于数组的接口,并由Entity对象索引:
声明类型为ComponentDataFromEntity或BufferFromEntity的字段,并在调度job之前设置该字段的值。
例如,如果您的“目标”component的“实体”字段定义了目标entity,则可以将以下字段添加到job结构中以查找目标的世界位置:
1 | [ ] |
请注意,此声明使用ReadOnly属性。您应该始终声明ComponentDataFromEntity 除非您确实写入要访问的component,否则对象为只读。
您可以在调度job时按以下方式设置此字段:
1 | var job = new ChaserSystemJob(); |
在job的Execute()
函数内,您可以使用Entity对象查找component的值:
1 | float3 targetPosition = EntityPositions[targetEntity].Position; |
以下完整示例显示了一个system,该system将具有包含其目标的Entity对象的Target字段的entity移向目标的当前位置:
1 | public class MoveTowardsEntitySystem : SystemBase |
获取数据失败
如果您正在查找的数据与您直接在job中读取和写入的数据冲突,则随机访问会导致竞争状况和BUG。如果确定直接在job中读取或写入的特定entity数据与您随机读取或写入的特定entity数据之间没有重叠,则可以使用NativeDisableParallelForRestriction attribute标记访问器对象。
Entity Command Buffers
EntityCommandBuffer
(ECB)解决两个重要问题:
- 在job中,您无法访问
EntityManager
。 - 当执行structural change(如创建entity)时,您将创建一个Sync Point并且必须等待所有jobs完成。
EntityCommandBuffer
允许你将变动队列化(无论是从job或从主线程),使他们能够在主线程上后生效。
Entity command buffer systems
使您可以在一帧中明确定义的位置播放在ECB中排队的命令。这些system通常是使用ECB的最佳方法。您可以从同一entity Entity command buffer systems中获取多个ECB,并且system将按照更新时创建它们的顺序来播放所有ECB。这将在system更新时创建一个Sync Point,而不是每个ECB一个Sync Point,并确保确定性。
默认的World初始化提供了三个system group,分别用于初始化,模拟和执行,并按每帧的顺序进行更新。在一个组中,有一个Entity command buffer system在该组中的任何其他system之前运行,而另一个在该组中的所有其他system之后运行。最好,您应该使用现有的Entity command buffer systemss之一,而不是创建自己的Entity command buffer systems,以最大程度地减少Sync point。有关default groups和Entity command buffer systems的内容,请参见default system group。
如果要使用并行job中的ECB(例如,Entities.ForEach
中的),则必须确保首先通过调用ToConcurrent
将其转换为并发ECB 。为确保ECB中命令的顺序不取决于job在job之间的分配方式,还必须将当前query中entities的索引传递给每个操作。
您可以像这样获取和使用ECB:
1 | struct Lifetime : IComponentData |