洪流学堂,让你快人几步。你好,我是跟着大智学Unity的萌新,我叫小新,最近在跟着大智学习DOTS。
SystemBase中提供的
Entities.ForEach
方法是遍历entity和component并执行逻辑的最简单的方式。
Entities.ForEach
方法会根据entity查询,在所有的查询结果上执行一个lambda方法。
在ECS系统中使用Entities.ForEach
要执行job的lambda方法,你可以使用
Schedule()
和
ScheduleParallel()
来调度job,也可以调用
Run()
方法在主线程上立即执行。你也可以使用
Entities.ForEach
中的多个方法来设置entity查询和job选项。
下面这段代码是一个简单的SystemBase实现,使用
Entities.ForEach
读取一个组件中的速度数据,然后写入另外一个组件。
class
ApplyVelocitySystem
:
SystemBase
{
protected
override
void
OnUpdate
(
)
{
Entities
.
ForEach
(
(
ref
Translation
translation
,
in
Velocity
velocity
)
=>
{
translation
.
Value
+=
velocity
.
Value
;
}
)
.
Schedule
(
)
;
}
}
注意lambda参数中的
ref
和
in
修饰符。
ref
代表了组件会被写入,
in
代表组件是只读的。将组件设置为只读可以让job调度更有效率。
选择Entity
Entities.ForEach
提供了自己的entity查询机制,用于筛选需要处理的entity。这个查询会自动包含lambda方法参数中的组件。你也可以使用
WithAll
(同时包含参数中所有组件),
WithAny
(包含参数中任意一个组件),
WithNone
(不包含参数中的组件)方法来设置更详细的筛选条件。
下面这个例子包含了多个筛选条件:
-
同时包含Destination, Source, LocalToWorld这三个组件,前两个组件在 ForEach 中,最后一个在 WithAll 中;
-
至少包含 Rotation, Translation, Scale 其中一个组件;
-
不包含 LocalToParent 组件
Entities
.
WithAll
<
LocalToWorld
>
(
)
.
WithAny
<
Rotation
,
Translation
,
Scale
>
(
)
.
WithNone
<
LocalToParent
>
(
)
.
ForEach
(
(
ref
Destination
outputData
,
in
Source
inputData
)
=>
{
// 执行一些任务
}
)
.
Schedule
(
)
;
访问EntityQuery对象
要想访问
Entities.ForEach
创建的EntityQuery对象,可以使用
WithStoreEntityQueryInField(ref query)
方法。
下面这段代码展示了如何获取
Entities.ForEach
创建的EntityQuery对象,这个例子用EntityQuery对象来调用
CalculateEntityCount()
方法,获取entity的数量,然后根据数量来创建对应长度的数组。
private
EntityQuery
query
;
protected
override
void
OnUpdate
(
)
{
int
dataCount
=
query
.
CalculateEntityCount
(
)
;
NativeArray
<
float
>
dataSquared
=
new
NativeArray
<
float
>
(
dataCount
,
Allocator
.
Temp
)
;
Entities
.
WithStoreEntityQueryInField
(
ref
query
)
.
ForEach
(
(
int
entityInQueryIndex
,
in
Data
data
)
=>
{
dataSquared
[
entityInQueryIndex
]
=
data
.
Value
*
data
.
Value
;
}
)
.
ScheduleParallel
(
)
;
Job
.
WithCode
(
(
)
=>
{
//Use dataSquared array...
var
v
=
dataSquared
[
dataSquared
.
Length
-
1
]
;
}
)
.
WithDisposeOnCompletion
(
dataSquared
)
.
Schedule
(
)
;
}
可选组件
虽然你可以在entity查询中筛选可选组件(使用
WithAny<T,U>
),但是没办法同时在lambda表达式中访问。如果你需要读取或写入这个可选组件,你可以用多个
Entities.ForEach
执行。例如,如果你有两个可选的组件,你需要3个ForEach:一个包含第一个可选组件,一个包含第二个可选组件,还有一个同时包含两个可选组件。
还有一个替代方法是使用IJobChunk来遍历,后面会讲到。
监听变化
当你想监听entity的组件发生变化时,你可以使用监听变化筛选器:
WithChangeFilter<T>
。这个筛选器中的组件类型必须作为lambda表达式的参数或者是WithAll<T>的一部分。
Entities
.
WithChangeFilter
<
Source
>
(
)
.
ForEach
(
(
ref
Destination
outputData
,
in
Source
inputData
)
=>
{
// 执行一些任务
}
)
.
ScheduleParallel
(
)
;
监听变化筛选器最多能支持两个组件类型。
注意
:监听变化筛选器是在chunk层面筛选的。如果任何代码使用
写入权限
访问了一个chunk中的组件,这个chunk中的这个组件就会被标记为
已修改
,即使代码并没有真正修改组件中的数据。
共享组件筛选
当entity上面有共享组件时,ECS会根据共享组件的值将相同值的entity分到同一个内存块中。你可以使用
WithSharedComponentFilter()
筛选出来具有特定共享组件值的entity。
比如下面这个例子根据共享组件Cohort值对entity进行了分组筛选:
ublic
class
ColorCycleJob
:
SystemBase
{
protected
override
void
OnUpdate
(
)
{
List
<
Cohort
>
cohorts
=
new
List
<
Cohort
>
(
)
;
EntityManager
.
GetAllUniqueSharedComponentData
<
Cohort
>
(
cohorts
)
;
foreach
(
Cohort
cohort
in
cohorts
)
{
DisplayColor
newColor
=
ColorTable
.
GetNextColor
(
cohort
.
Value
)
;
Entities
.
WithSharedComponentFilter
(
cohort
)
.
ForEach
(
(
ref
DisplayColor
color
)
=>
{
color
=
newColor
;
}
)
.
ScheduleParallel
(
)
;
}
}
}
这段代码先用EntityManager来获取了所有cohort值,然后筛选每个cohort值并安排一个job来执行逻辑。
定义Foreach方法
当你定义
Entities.ForEach
使用的lambda方法时,你可以给它传递一些当前entity的信息。
通常一个lambda方法如下:
Entities
.
ForEach
(
(
Entity
entity
,
int
entityInQueryIndex
,
ref
Translation
translation
,
in
Movement
move
)
=>
{
/* .. */
}
)
默认情况下,你可以最多传入8个参数给lambda方法。(如果你需要传入更多的参数,你可以定义自定义委托)
当使用标准委托时,参数必须按照如下顺序: 1、值传递的参数在最前面(无修饰符) 2、可写入的参数在中间(
ref
参数修饰符) 3、只读参数在最后(
in
参数修饰符)
所有的组件需要使用
ref
或
in
来修饰。否则,组件结构体会以值传递的方式传入一个copy而不是引用。这意味着只读的组件需要额外的内存,对需要写入的组件来说,任何修改在返回后都会丢失。
如果你的方法没有遵循上面的规则,并且你没有创建一个合适的自定义委托,编译器会报一个类似如下的错误:
error
CS1593
:
Delegate 'Invalid_ForEach_Signature_See_ForEach_Documentation_For_Rules_And_Restrictions' does
not
take N arguments
注意,如果是参数顺序的问题,也会提示上面这个错误。
组件参数
要访问与entity关联的组件,你必须使用该组件类型作为参数传递给lambda函数。编译器会自动将传递给lambda函数的所有组件作为必需组件添加到entity查询中。
要更新组件的值,必须使用
ref
修饰参数,传递给lambda函数。(如果没有
ref
关键字,会使用值传递,结果是将对组件的临时副本进行修改,任何在lambda内的修改在返回后都会丢失)
要将传递给lambda函数的组件指定为只读,在参数列表中使用
in
关键字。
注意:
使用
ref
修饰后就表示组件所在的内存块被标记为已修改,即使lambda函数实际上并未对其进行修改。为了提高效率,务必使用
in
关键字将lambda函数未修改的组件指定为只读。
以下示例将Source组件参数作为只读传递给job,并将Destination组件参数作为可写传递给job:
Entities
.
ForEach
(
(
ref
Destination
outputData
,
in
Source
inputData
)
=>
{
outputData
.
Value
=
inputData
.
Value
;
}
)
.
ScheduleParallel
(
)
;
注意:
目前还不能将块组件传递给Entities.ForEach lambda函数。
对于dynamic buffer,使用DynamicBuffer <T>而不是存储在缓冲区中的Component类型:
public
class
BufferSum
:
SystemBase
{
private
EntityQuery
query
;
// 调度两个有依赖关系的job
protected
override
void
OnUpdate
(
)
{
//这个query对象能访问的原因是因为下面使用了WithStoreEntityQueryInField(query)
int
entitiesInQuery
=
query
.
CalculateEntityCount
(
)
;
//创建一个nativearry来保存临时的求和结果
NativeArray
<
int
>
intermediateSums
=
new
NativeArray
<
int
>
(
entitiesInQuery
,
Allocator
.
TempJob
)
;
//调度第一个job
Entities
.
ForEach
(
(
int
entityInQueryIndex
,
in
DynamicBuffer
<
IntBufferData
>
buffer
)
=>
{
for
(
int
i
=
0
;
i
<
buffer
.
Length
;
i
++
)
{
intermediateSums
[
entityInQueryIndex
]
+=
buffer
[
i
]
.
Value
;
}
}
)
.
WithStoreEntityQueryInField
(
ref
query
)
.
WithName
(
"IntermediateSums"
)
.
ScheduleParallel
(
)
;
// Execute in parallel for each chunk of entities
//调度第二个job,依赖第一个job
Job
.
WithCode
(
(
)
=>
{
int
result
=
0
;
for
(
int
i
=
0
;
i
<
intermediateSums
.
Length
;
i
++
)
{
result
+=
intermediateSums
[
i
]
;
}
//Not burst compatible:
Debug
.
Log
(
"Final sum is "
+
result
)
;
}
)
.
WithDisposeOnCompletion
(
intermediateSums
)
.
WithoutBurst
(
)
.
WithName
(
"FinalSum"
)
.
Schedule
(
)
;
// Execute on a single, background thread
}
}
扩展阅读
【扩展学习】 在 洪流学堂 公众号回复 DOTS 可以阅读本系列所有文章,更有视频教程等着你!
呼~ 今天小新絮絮叨叨的真是够够的了。没讲清楚的地方欢迎评论,咱们一起探索。
我是大智(vx:zhz11235),你的技术探路者,下次见!
别走!
点赞
,
收藏
哦!
好,你可以走了。