在ECS系统中使用Entities.ForEach

洪流学堂,让你快人几步。你好,我是跟着大智学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),你的技术探路者,下次见!
别走! 点赞 , 收藏 哦!
好,你可以走了。
在Unity的ECS中实现物体拖尾效果的基本步骤如下: 1. 创建一个自定义的拖尾组件,该组件包含一个数组来存储每个帧的位置和旋转信息,以及一个变量来指定拖尾的长度。 2. 在Update系统中,将当前帧的位置和旋转信息添加到拖尾组件的数组中,并删除超出拖尾长度的旧帧信息。 3. 使用渲染系统在每个帧之间绘制线条,将拖尾效果呈现出来。可以使用LineRenderer组件或Graphics.DrawMeshInstanced来实现。 以下是一个简单的示例代码,用于演示如何在Unity的ECS中实现物体拖尾效果: ```csharp using Unity.Entities; using Unity.Mathematics; using Unity.Transforms; using UnityEngine; public struct TrailComponent : IComponentData { public float3[] positions; public quaternion[] rotations; public int currentIndex; public int maxTrailLength; } public class TrailSystem : SystemBase { protected override void OnUpdate() { Entities.ForEach((Entity entity, ref Translation translation, ref Rotation rotation, ref TrailComponent trail) => { // Add current position and rotation to trail trail.positions[trail.currentIndex] = translation.Value; trail.rotations[trail.currentIndex] = rotation.Value; trail.currentIndex = (trail.currentIndex + 1) % trail.maxTrailLength; // Draw trail var lineRenderer = EntityManager.GetComponentObject<LineRenderer>(entity); lineRenderer.positionCount = trail.maxTrailLength; for (int i = 0; i < trail.maxTrailLength; i++) { int index = (trail.currentIndex + i) % trail.maxTrailLength; lineRenderer.SetPosition(i, trail.positions[index]); } }).Schedule(); } } public class TrailBootstrap : MonoBehaviour { public GameObject trailPrefab; private void Start() { var entityManager = World.DefaultGameObjectInjectionWorld.EntityManager; var trailEntity = entityManager.CreateEntity(); entityManager.AddComponentData(trailEntity, new TrailComponent { positions = new float3[10], rotations = new quaternion[10], currentIndex = 0, maxTrailLength = 10 }); entityManager.AddComponentData(trailEntity, new Translation { Value = Vector3.zero }); entityManager.AddComponentData(trailEntity, new Rotation { Value = Quaternion.identity }); var lineRenderer = trailPrefab.GetComponent<LineRenderer>(); entityManager.AddComponentObject(trailEntity, lineRenderer); } } ``` 在此示例代码中,我们创建了一个名为TrailComponent的自定义组件,该组件包含一个float3数组来存储位置信息,一个quaternion数组来存储旋转信息,以及一个整数变量来指定拖尾的最大长度。我们还创建了一个名为TrailSystem的系统,该系统在每个帧中将当前位置和旋转信息添加到TrailComponent中,并使用LineRenderer组件将拖尾效果呈现出来。最后,我们在TrailBootstrap脚本中创建了一个拖尾实体,并将LineRenderer组件添加到该实体中。 注意:在使用LineRenderer组件时,需要将其设置为使用“Local”空间,并将其材质的渲染模式设置为“Transparent”。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值