在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参数中的refin修饰符。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的一部分。

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参数修饰符)

所有的组件需要使用refin来修饰。否则,组件结构体会以值传递的方式传入一个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 而不是存储在缓冲区中的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),你的技术探路者,下次见!

别走!点赞收藏哦!

好,你可以走了。

UnityECS实现物体拖尾效果的基本步骤如下: 1. 创建一个自定义的拖尾组件,该组件包含一个数组来存储每个帧的位置和旋转信息,以及一个变量来指定拖尾的长度。 2. 在Update系统,将当前帧的位置和旋转信息添加到拖尾组件的数组,并删除超出拖尾长度的旧帧信息。 3. 使用渲染系统在每个帧之间绘制线条,将拖尾效果呈现出来。可以使用LineRenderer组件或Graphics.DrawMeshInstanced来实现。 以下是一个简单的示例代码,用于演示如何在UnityECS实现物体拖尾效果: ```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
发出的红包

打赏作者

大智_Unity玩家

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值