洪流學堂,讓你快人幾步。你好,我是跟着大智學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),你的技術探路者,下次見!
别走!點贊,收藏哦!
好,你可以走了。