天天看點

在ECS系統中使用Entities.ForEach在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的一部分。

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 而不是存儲在緩沖區中的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
    }
}

           

擴充閱讀

  • 使用Job來修改Transform
  • ECS的核心概念
  • ECS中的Entity實體
  • ECS之Component元件
  • ECS之System系統
【擴充學習】在洪流學堂公衆号回複

DOTS

可以閱讀本系列所有文章,更有視訊教程等着你!

呼~ 今天小新絮絮叨叨的真是夠夠的了。沒講清楚的地方歡迎評論,咱們一起探索。

我是大智(vx:zhz11235),你的技術探路者,下次見!

别走!點贊,收藏哦!

好,你可以走了。

繼續閱讀