在ECS系统中使用IJobChunk作业
洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在跟着大智学习DOTS。你可以在系统内部实现IJobChunk,用于逐块遍历数据。当你在系统的OnUpdate()中安排IJobChunk作业时,该作业为每个符合entity查询条件的chunk调用一次Execute()。然后,你可以遍历每个chunk中的entity上的数据。使用IJobChunk与Entities.Fo
·
洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在跟着大智学习DOTS。
你可以在系统内部实现IJobChunk,用于逐块遍历数据。当你在系统的 OnUpdate() 中安排IJobChunk作业时,该作业为每个符合entity查询条件的chunk调用一次 Execute() 。然后,你可以遍历每个chunk中的entity上的数据。
使用IJobChunk
与Entities.ForEach相比,使用IJobChunk进行迭代需要更多的代码,但是也更直接。
按块进行迭代的另一个好处是,你可以使用 Archetype.Has<T>() 来检查每个块中是否存在可选组件,然后相应地处理块中的所有entity。
实现IJobChunk作业的步骤如下:
-
创建一个 EntityQuery 来标识要处理的entity。
-
定义job结构体,并包含 ArchetypeChunkComponentType 对象的字段,这些对象标识job必须直接访问的组件的类型。另外,指定作业是只读还是写入这些组件。
-
实例化作业并在系统 OnUpdate() 方法中安排作业。
-
在 Execute() 函数中,获取 NativeArray 作业读取或写入的组件的实例,然后在当前块上进行遍历以执行所需的工作。
后面会有实例演示如何使用 IJobChunk 。
使用EntityQuery查询数据
EntityQuery定义原型必须包含的一组组件类型,系统才能处理其关联的块和实体。原型中可以有其他组件,但是它必须至少有EntityQuery定义的组件。你还可以排除包含特定类型组件的原型。
对于简单查询,可以使用 SystemBase.GetEntityQuery() 函数并按如下所示传入组件类型:
public
class
RotationSpeedSystem
:
SystemBase
{
private
EntityQuery
m_Query
;
protected
override
void
OnCreate
(
)
{
m_Query
=
GetEntityQuery
(
ComponentType
.
ReadOnly
<
Rotation
>
(
)
,
ComponentType
.
ReadOnly
<
RotationSpeed
>
(
)
)
;
//...
}
对于更复杂的情况,你可以使用 EntityQueryDesc 。 EntityQueryDesc 提供了灵活的查询机制,以指定的组件类型:
-
All :此数组中的所有组件类型必须存在于原型中
-
Any :原型中必须至少存在此数组中的一种组件类型
-
None :原型中不能存在此数组中的任何组件类型
例如,以下查询包括包含 RotationQuaternion 和 RotationSpeed 组件的原型,但不包括包含 Frozen 组件的任何原型:
protected
override
void
OnCreate
(
)
{
var
queryDescription
=
new
EntityQueryDesc
(
)
{
None
=
new
ComponentType
[
]
{
typeof
(
Static
)
}
,
All
=
new
ComponentType
[
]
{
ComponentType
.
ReadWrite
<
Rotation
>
(
)
,
ComponentType
.
ReadOnly
<
RotationSpeed
>
(
)
}
}
;
m_Query
=
GetEntityQuery
(
queryDescription
)
;
}
查询时可以使用 ComponentType.ReadOnly<T> 而不是更简单的 typeof 表达式来指定系统不会写入 RotationSpeed 。
你还可以组合多个查询,传入 EntityQueryDesc 对象数组而不是单个实例。ECS使用逻辑或运算来组合每个查询。下面的示例选择包含 RotationQuaternion 或 RotationSpeed 组件(或两者同时都有)的所有原型:
protected
override
void
OnCreate
(
)
{
var
queryDescription0
=
new
EntityQueryDesc
{
All
=
new
ComponentType
[
]
{
typeof
(
Rotation
)
}
}
;
var
queryDescription1
=
new
EntityQueryDesc
{
All
=
new
ComponentType
[
]
{
typeof
(
RotationSpeed
)
}
}
;
m_Query
=
GetEntityQuery
(
new
EntityQueryDesc
[
]
{
queryDescription0
,
queryDescription1
}
)
;
}
注意: 请勿在 EntityQueryDesc 中包含可选组件。要处理可选组件,在 IJobChunk.Execute() 使用 chunk.Has<T>() 内部方法检查当前ArchetypeChunk是否有可选组件。因为同一块中的所有实体都具有相同的组件,所以你只需要一个块检查一次即可,不用每个实体检查一次。
为了提高效率并避免创建不必要地垃圾收集的引用类型,应在系统 OnCreate() 方法中创建 EntityQueries ,然后将结果存储在实例变量中。(在上面示例中, m_Query 变量就是这个用途)
定义IJobChunk结构体
IJobChunk结构体中定义了作业运行时需要的数据以及作业的 Execute() 方法。
要访问系统传给 Execute() 方法的块内的组件数组,必须为作业读取或写入的每种类型的组件创建一个 ArchetypeChunkComponentType<T> 对象。你可以使用这些对象获取 NativeArray 实例,通过这些 NativeArray 可以访问实体的组件。包括作业的 Execute() 方法读取或写入的EntityQuery中引用的所有组件。你还可以用 ArchetypeChunkComponentType 获取未包含在EntityQuery中的可选组件类型。
在访问当前块之前,必须检查确保当前块有可选组件。例如,HelloCube IJobChunk示例定义了一个作业结构体,该结构定义了两个组件的 ArchetypeChunkComponentType<T> 变量, RotationQuaternion 和 RotationSpeed :
[
BurstCompile
]
struct
RotationSpeedJob
:
IJobChunk
{
public
float
DeltaTime
;
public
ComponentTypeHandle
<
Rotation
>
RotationTypeHandle
;
[
ReadOnly
]
public
ComponentTypeHandle
<
RotationSpeed
>
RotationSpeedTypeHandle
;
public
void
Execute
(
ArchetypeChunk
chunk
,
int
chunkIndex
,
int
firstEntityIndex
)
{
// ...
}
}
系统为 OnUpdate() 函数中的这些变量赋值,ECS在运行作业时会在 Execute() 方法内使用这些变量。
这个作业还使用Unity的delta时间为3D对象的旋转设置动画。该示例使用struct字段将delta值传递给 Execute() 方法。
编写Execute方法
IJobChunk Execute() 方法的签名为:
public
void
Execute
(
ArchetypeChunk
chunk
,
int
chunkIndex
,
int
firstEntityIndex
)
chunk 参数是内存块的句柄,包含此作业的迭代时必须处理的实体和组件。因为块只能包含一个原型,所以块中的所有实体都具有相同的组件。
使用 chunk 参数获取组件的NativeArray实例:
var
chunkRotations
=
chunk
.
GetNativeArray
(
RotationTypeHandle
)
;
var
chunkRotationSpeeds
=
chunk
.
GetNativeArray
(
RotationSpeedTypeHandle
)
;
这些数组是对齐的,同个实体在所有数组中具有相同的索引。可以使用正常的for循环来遍历组件数组。使用 chunk.Count 得到存储在当前块的实体的数量:
var
chunkRotations
=
chunk
.
GetNativeArray
(
RotationTypeHandle
)
;
var
chunkRotationSpeeds
=
chunk
.
GetNativeArray
(
RotationSpeedTypeHandle
)
;
for
(
var
i
=
0
;
i
<
chunk
.
Count
;
i
++
)
{
var
rotation
=
chunkRotations
[
i
]
;
var
rotationSpeed
=
chunkRotationSpeeds
[
i
]
;
// Rotate something about its up vector at the speed given by RotationSpeed.
chunkRotations
[
i
]
=
new
Rotation
{
Value
=
math
.
mul
(
math
.
normalize
(
rotation
.
Value
)
,
quaternion
.
AxisAngle
(
math
.
up
(
)
,
rotationSpeed
.
RadiansPerSecond
*
DeltaTime
)
)
}
;
}
如果在EntityQueryDesc中有 Any 过滤器,或者可选的组件没有写在查询中,则可以在使用之前用 ArchetypeChunk.Has<T>() 函数测试当前块是否包含这些组件:
if
(
chunk
.
Has
<
OptionalComp
>
(
OptionalCompType
)
)
{
//...}
注意: 如果使用并发的entity command buffer,将 chunkIndex 参数作为 sortKey 参数传递给命令缓冲区函数。
跳过没有变化的实体的块
如果仅在组件值发生更改时才需要更新实体,可以将该组件类型添加到EntityQuery的更改筛选器中。例如,如果你的系统读取两个组件,并且仅在前两个组件中的一个已更改时才需要更新第三个组件,则可以按以下方式使用EntityQuery:
private
EntityQuery
m_Query
;
protected
override
void
OnCreate
(
)
{
m_Query
=
GetEntityQuery
(
ComponentType
.
ReadWrite
<
Output
>
(
)
,
ComponentType
.
ReadOnly
<
InputA
>
(
)
,
ComponentType
.
ReadOnly
<
InputB
>
(
)
)
;
m_Query
.
SetChangedVersionFilter
(
new
ComponentType
[
]
{
ComponentType
.
ReadWrite
<
InputA
>
(
)
,
ComponentType
.
ReadWrite
<
InputB
>
(
)
}
)
;
}
EntityQuery更改过滤器最多支持两个组件。如果你想进行更多检查或不使用EntityQuery,则可以手动进行检查。要进行这个检查,可以使用 ArchetypeChunk.DidChange() 函数将组件的块的更改版本与系统的 LastSystemVersion 进行比较。如果此函数返回false,则可以完全跳过当前块,因为自从上次系统运行以来,该类型的组件均未更改。
你必须使用一个struct字段将 LastSystemVersion 从系统传递到作业中,如下所示:
[
BurstCompile
]
struct
UpdateJob
:
IJobChunk
{
public
ComponentTypeHandle
<
InputA
>
InputATypeHandle
;
public
ComponentTypeHandle
<
InputB
>
InputBTypeHandle
;
[
ReadOnly
]
public
ComponentTypeHandle
<
Output
>
OutputTypeHandle
;
public
uint
LastSystemVersion
;
public
void
Execute
(
ArchetypeChunk
chunk
,
int
chunkIndex
,
int
firstEntityIndex
)
{
var
inputAChanged
=
chunk
.
DidChange
(
InputATypeHandle
,
LastSystemVersion
)
;
var
inputBChanged
=
chunk
.
DidChange
(
InputBTypeHandle
,
LastSystemVersion
)
;
// If neither component changed, skip the current chunk
if
(
!
(
inputAChanged
||
inputBChanged
)
)
return
;
var
inputAs
=
chunk
.
GetNativeArray
(
InputATypeHandle
)
;
var
inputBs
=
chunk
.
GetNativeArray
(
InputBTypeHandle
)
;
var
outputs
=
chunk
.
GetNativeArray
(
OutputTypeHandle
)
;
for
(
var
i
=
0
;
i
<
outputs
.
Length
;
i
++
)
{
outputs
[
i
]
=
new
Output
{
Value
=
inputAs
[
i
]
.
Value
+
inputBs
[
i
]
.
Value
}
;
}
}
}
与所有作业结构体字段一样,在计划作业之前,必须先赋值:
protected
override
void
OnUpdate
(
)
{
var
job
=
new
UpdateJob
(
)
;
job
.
LastSystemVersion
=
this
.
LastSystemVersion
;
job
.
InputATypeHandle
=
GetComponentTypeHandle
<
InputA
>
(
true
)
;
job
.
InputBTypeHandle
=
GetComponentTypeHandle
<
InputB
>
(
true
)
;
job
.
OutputTypeHandle
=
GetComponentTypeHandle
<
Output
>
(
false
)
;
this
.
Dependency
=
job
.
ScheduleParallel
(
m_Query
,
this
.
Dependency
)
;
}
注意: 为了提高效率,更改版本适用于整个块,而不是单个实体。如果另一个具有写入该类型组件功能的作业访问了块,ECS就会增加该组件的更改版本,并且 DidChange() 函数会返回true。即使声明对组件进行写如访问的作业实际上并未更改组件的值,ECS也会增加更改版本。
实例化并安排作业
要运行IJobChunk作业,必须创建作业结构体的实例,给结构体字段赋值,然后安排作业。当你在SystemBase的 OnUpdate() 方法中执行时,系统会将安排作业每帧运行一次。
protected
override
void
OnUpdate
(
)
{
var
job
=
new
RotationSpeedJob
(
)
{
RotationTypeHandle
=
GetComponentTypeHandle
<
Rotation
>
(
false
)
,
RotationSpeedTypeHandle
=
GetComponentTypeHandle
<
RotationSpeed
>
(
true
)
,
DeltaTime
=
Time
.
DeltaTime
}
;
this
.
Dependency
=
job
.
ScheduleParallel
(
m_Query
,
this
.
Dependency
)
;
}
调用 GetArchetypeChunkComponentType<T>() 函数设置组件类型变量时,确保将只需要读取不需要写入的组件的 isReadOnly 参数设置为true,这些参数可能会对ECS框架安排作业的效率产生重大影响。这些访问模式的设置,需要在结构体定义和EntityQuery中匹配。
不要在系统类变量中缓存 GetArchetypeChunkComponentType<T>() 返回值。你必须在每次系统运行时调用这个函数,并将更新后的值传给作业。
扩展阅读
【扩展学习】 在 洪流学堂 公众号回复 DOTS 可以阅读本系列所有文章,更有视频教程等着你!
呼~ 今天小新絮絮叨叨的真是够够的了。没讲清楚的地方欢迎评论,咱们一起探索。
我是大智(vx:zhz11235),你的技术探路者,下次见!
别走! 点赞 , 收藏 哦!
好,你可以走了。
更多推荐
所有评论(0)