在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),你的技术探路者,下次见!
别走! 点赞 , 收藏 哦!
好,你可以走了。
内容概要:本文为《科技类企业品牌传播白皮书》,系统阐述了新闻媒体发稿、自媒体博主种草与短视频矩阵覆盖三大核心传播策略,并结合“传声港”平台的AI工具与资源整合能力,提出适配科技企业的品牌传播解决方案。文章深入分析科技企业传播的特殊性,包括受众圈层化、技术复杂性与传播通俗性的矛盾、产品生命周期影响及2024-2025年传播新趋势,强调从“技术输出”向“价值引领”的战略升级。针对三种传播方式,分别从适用场景、操作流程、效果评估、成本效益、风险防控等方面提供详尽指南,并通过平台AI能力实现资源智能匹配、内容精准投放与全链路效果追踪,最终构建“信任—种草—曝光”三位一体的传播闭环。; 适合人群:科技类企业品牌与市场负责人、公关传播从业者、数字营销管理者及初创科技公司创始人;具备一定品牌传播基础,关注效果可量化与AI工具赋能的专业人士。; 使用场景及目标:①制定科技产品全生命周期的品牌传播策略;②优化媒体发稿、KOL合作与短视频运营的资源配置与ROI;③借助AI平台实现传播内容的精准触达、效果监测与风险控制;④提升品牌在技术可信度、用户信任与市场影响力方面的综合竞争力。; 阅读建议:建议结合传声港平台的实际工具模块(如AI选媒、达人匹配、数据驾驶舱)进行对照阅读,重点关注各阶段的标准化流程与数据指标基准,将理论策略与平台实操深度融合,推动品牌传播从经验驱动转向数据与工具双驱动。
### ECS架构下粒子系统性能优化方法 在ECS架构中,粒子系统的性能优化可以通过合理设计组件、系统以及利用并行处理来实现。以下详细描述了具体的优化方法: #### 1. 数据布局优化 ECS架构的核心优势之一是其紧凑的数据布局。通过将粒子的属性(如位置、速度、生命周期等)拆分为独立的组件,并以连续数组的形式存储,可以显著提高CPU缓存命中率和数据访问效率[^1]。 ```csharp public struct ParticlePosition : IComponentData { public float3 Value; } public struct ParticleVelocity : IComponentData { public float3 Value; } public struct ParticleLifetime : IComponentData { public float Value; } ``` 这种设计使得每个粒子的属性被分散到不同的组件中,但整个粒子集合的数据仍然是连续存储的,便于SIMD指令集的高效处理[^2]。 #### 2. 并行计算 ECS框架通常支持多线程并行处理,这对于需要大量计算的粒子系统尤为重要。通过`ScheduleParallel()`方法,可以将粒子更新任务分配到多个线程上执行,从而充分利用现代CPU的多核特性[^1]。 ```csharp [UpdateInGroup(typeof(SimulationSystemGroup))] public class ParticleUpdateSystem : SystemBase { protected override void OnUpdate() { float deltaTime = Time.DeltaTime; Entities.ForEach((ref ParticlePosition position, ref ParticleVelocity velocity, ref ParticleLifetime lifetime) => { position.Value += velocity.Value * deltaTime; lifetime.Value -= deltaTime; if (lifetime.Value <= 0) { // 标记粒子为销毁状态 } }).ScheduleParallel(); } } ``` #### 3. 对象池管理 为了避免频繁的内存分配和释放带来的性能开销,可以使用对象池技术来复用粒子实例。ECS框架中的实体创建和销毁操作通常已经经过优化,但仍需注意避免不必要的实体生命周期变化[^3]。 ```csharp public class ParticlePoolSystem : SystemBase { protected override void OnUpdate() { // 从池中获取或回收粒子实体 Entities.WithoutBurst().ForEach((Entity entity, in ParticleLifetime lifetime) => { if (lifetime.Value <= 0) { EntityManager.DestroyEntity(entity); } }).Run(); } } ``` #### 4. 渲染批处理 为了减少Draw Call的数量,可以将具有相同材质和渲染属性的粒子分组,并通过批处理技术一次性绘制。ECS架构允许按组件类型对实体进行筛选,这为实现高效的渲染批处理提供了便利[^2]。 ```csharp [UpdateAfter(typeof(ParticleUpdateSystem))] public class ParticleRenderSystem : SystemBase { protected override void OnUpdate() { var particlesToRender = new NativeList<Particle>(Allocator.TempJob); Entities.ForEach((in ParticlePosition position, in ParticleVelocity velocity) => { particlesToRender.Add(new Particle { Position = position.Value, Velocity = velocity.Value }); }).Run(); // 调用GPU渲染接口 RenderParticles(particlesToRender.AsArray()); } private void RenderParticles(NativeArray<Particle> particles) { // 实现具体渲染逻辑 } } ``` #### 5. 参数化与动态调整 通过定义灵活的参数组件,可以在运行时动态调整粒子的行为,而无需修改代码逻辑。这种方式不仅提高了开发效率,还减少了因硬编码导致的性能瓶颈。 ```csharp public struct ParticleSettings : IComponentData { public float EmissionRate; public float3 InitialVelocity; public float LifetimeRange; } ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值