洪流學堂,讓你快人幾步。你好,我是跟着大智學Unity的萌新,我叫小新,最近在跟着大智學習DOTS。
你可以在系統内部實作IJobChunk,用于逐塊周遊資料。當你在系統的
OnUpdate()
中安排IJobChunk作業時,該作業為每個符合entity查詢條件的chunk調用一次
Execute()
。然後,你可以周遊每個chunk中的entity上的資料。
使用IJobChunk
與Entities.ForEach相比,使用IJobChunk進行疊代需要更多的代碼,但是也更直接。
按塊進行疊代的另一個好處是,你可以使用
Archetype.Has<T>()
來檢查每個塊中是否存在可選元件,然後相應地處理塊中的所有entity。
實作IJobChunk作業的步驟如下:
- 建立一個
來辨別要處理的entity。EntityQuery
- 定義job結構體,并包含
對象的字段,這些對象辨別job必須直接通路的元件的類型。另外,指定作業是隻讀還是寫入這些元件。ArchetypeChunkComponentType
- 執行個體化作業并在系統
方法中安排作業。OnUpdate()
- 在
函數中,擷取Execute()
作業讀取或寫入的元件的執行個體,然後在目前塊上進行周遊以執行所需的工作。NativeArray
後面會有執行個體示範如何使用
IJobChunk
。
使用EntityQuery查詢資料
EntityQuery定義原型必須包含的一組元件類型,系統才能處理其關聯的塊和實體。原型中可以有其他元件,但是它必須至少有EntityQuery定義的元件。你還可以排除包含特定類型元件的原型。
對于簡單查詢,可以使用
SystemBase.GetEntityQuery()
函數并按如下所示傳入元件類型:
public class RotationSpeedSystem : SystemBase
{
private EntityQuery m_Query;
protected override void OnCreate()
{
m_Query = GetEntityQuery(ComponentType.ReadOnly<Rotation>(),
ComponentType.ReadOnly<RotationSpeed>());
//...
}
對于更複雜的情況,你可以使用
EntityQueryDesc
。
EntityQueryDesc
提供了靈活的查詢機制,以指定的元件類型:
-
:此數組中的所有元件類型必須存在于原型中All
-
:原型中必須至少存在此數組中的一種元件類型Any
-
:原型中不能存在此數組中的任何元件類型None
例如,以下查詢包括包含
RotationQuaternion
和
RotationSpeed
元件的原型,但不包括包含
Frozen
元件的任何原型:
protected override void OnCreate()
{
var queryDescription = new EntityQueryDesc()
{
None = new ComponentType[]
{
typeof(Static)
},
All = new ComponentType[]
{
ComponentType.ReadWrite<Rotation>(),
ComponentType.ReadOnly<RotationSpeed>()
}
};
m_Query = GetEntityQuery(queryDescription);
}
查詢時可以使用
ComponentType.ReadOnly<T>
而不是更簡單的
typeof
表達式來指定系統不會寫入
RotationSpeed
。
你還可以組合多個查詢,傳入
EntityQueryDesc
對象數組而不是單個執行個體。ECS使用邏輯或運算來組合每個查詢。下面的示例選擇包含
RotationQuaternion
或
RotationSpeed
元件(或兩者同時都有)的所有原型:
protected override void OnCreate()
{
var queryDescription0 = new EntityQueryDesc
{
All = new ComponentType[] {typeof(Rotation)}
};
var queryDescription1 = new EntityQueryDesc
{
All = new ComponentType[] {typeof(RotationSpeed)}
};
m_Query = GetEntityQuery(new EntityQueryDesc[] {queryDescription0, queryDescription1});
}
**注意:**請勿在
EntityQueryDesc
中包含可選元件。要處理可選元件,在
IJobChunk.Execute()
使用
chunk.Has<T>()
内部方法檢查目前ArchetypeChunk是否有可選元件。因為同一塊中的所有實體都具有相同的元件,是以你隻需要一個塊檢查一次即可,不用每個實體檢查一次。
為了提高效率并避免建立不必要地垃圾收集的引用類型,應在系統
OnCreate()
方法中建立
EntityQueries
,然後将結果存儲在執行個體變量中。(在上面示例中,
m_Query
變量就是這個用途)
定義IJobChunk結構體
IJobChunk結構體中定義了作業運作時需要的資料以及作業的
Execute()
方法。
要通路系統傳給
Execute()
方法的塊内的元件數組,必須為作業讀取或寫入的每種類型的元件建立一個
ArchetypeChunkComponentType<T>
對象。你可以使用這些對象擷取
NativeArray
執行個體,通過這些
NativeArray
可以通路實體的元件。包括作業的
Execute()
方法讀取或寫入的EntityQuery中引用的所有元件。你還可以用
ArchetypeChunkComponentType
擷取未包含在EntityQuery中的可選元件類型。
在通路目前塊之前,必須檢查確定目前塊有可選元件。例如,HelloCube IJobChunk示例定義了一個作業結構體,該結構定義了兩個元件的
ArchetypeChunkComponentType<T>
變量,
RotationQuaternion
和
RotationSpeed
:
[BurstCompile]
struct RotationSpeedJob : IJobChunk
{
public float DeltaTime;
public ComponentTypeHandle<Rotation> RotationTypeHandle;
[ReadOnly] public ComponentTypeHandle<RotationSpeed> RotationSpeedTypeHandle;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
// ...
}
}
系統為
OnUpdate()
函數中的這些變量指派,ECS在運作作業時會在
Execute()
方法内使用這些變量。
這個作業還使用Unity的delta時間為3D對象的旋轉設定動畫。該示例使用struct字段将delta值傳遞給
Execute()
方法。
編寫Execute方法
IJobChunk
Execute()
方法的簽名為:
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
chunk
參數是記憶體塊的句柄,包含此作業的疊代時必須處理的實體群組件。因為塊隻能包含一個原型,是以塊中的所有實體都具有相同的元件。
使用
chunk
參數擷取元件的NativeArray執行個體:
var chunkRotations = chunk.GetNativeArray(RotationTypeHandle);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedTypeHandle);
這些數組是對齊的,同個實體在所有數組中具有相同的索引。可以使用正常的for循環來周遊元件數組。使用
chunk.Count
得到存儲在目前塊的實體的數量:
var chunkRotations = chunk.GetNativeArray(RotationTypeHandle);
var chunkRotationSpeeds = chunk.GetNativeArray(RotationSpeedTypeHandle);
for (var i = 0; i < chunk.Count; i++)
{
var rotation = chunkRotations[i];
var rotationSpeed = chunkRotationSpeeds[i];
// Rotate something about its up vector at the speed given by RotationSpeed.
chunkRotations[i] = new Rotation
{
Value = math.mul(math.normalize(rotation.Value),
quaternion.AxisAngle(math.up(), rotationSpeed.RadiansPerSecond * DeltaTime))
};
}
如果在EntityQueryDesc中有
Any
過濾器,或者可選的元件沒有寫在查詢中,則可以在使用之前用
ArchetypeChunk.Has<T>()
函數測試目前塊是否包含這些元件:
if (chunk.Has<OptionalComp>(OptionalCompType))
{//...}
**注意:**如果使用并發的entity command buffer,将
chunkIndex
參數作為
sortKey
參數傳遞給指令緩沖區函數。
跳過沒有變化的實體的塊
如果僅在元件值發生更改時才需要更新實體,可以将該元件類型添加到EntityQuery的更改篩選器中。例如,如果你的系統讀取兩個元件,并且僅在前兩個元件中的一個已更改時才需要更新第三個元件,則可以按以下方式使用EntityQuery:
private EntityQuery m_Query;
protected override void OnCreate()
{
m_Query = GetEntityQuery(
ComponentType.ReadWrite<Output>(),
ComponentType.ReadOnly<InputA>(),
ComponentType.ReadOnly<InputB>());
m_Query.SetChangedVersionFilter(
new ComponentType[]
{
ComponentType.ReadWrite<InputA>(),
ComponentType.ReadWrite<InputB>()
});
}
EntityQuery更改過濾器最多支援兩個元件。如果你想進行更多檢查或不使用EntityQuery,則可以手動進行檢查。要進行這個檢查,可以使用
ArchetypeChunk.DidChange()
函數将元件的塊的更改版本與系統的
LastSystemVersion
進行比較。如果此函數傳回false,則可以完全跳過目前塊,因為自從上次系統運作以來,該類型的元件均未更改。
你必須使用一個struct字段将
LastSystemVersion
從系統傳遞到作業中,如下所示:
[BurstCompile]
struct UpdateJob : IJobChunk
{
public ComponentTypeHandle<InputA> InputATypeHandle;
public ComponentTypeHandle<InputB> InputBTypeHandle;
[ReadOnly] public ComponentTypeHandle<Output> OutputTypeHandle;
public uint LastSystemVersion;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
var inputAChanged = chunk.DidChange(InputATypeHandle, LastSystemVersion);
var inputBChanged = chunk.DidChange(InputBTypeHandle, LastSystemVersion);
// If neither component changed, skip the current chunk
if (!(inputAChanged || inputBChanged))
return;
var inputAs = chunk.GetNativeArray(InputATypeHandle);
var inputBs = chunk.GetNativeArray(InputBTypeHandle);
var outputs = chunk.GetNativeArray(OutputTypeHandle);
for (var i = 0; i < outputs.Length; i++)
{
outputs[i] = new Output { Value = inputAs[i].Value + inputBs[i].Value };
}
}
}
與所有作業結構體字段一樣,在計劃作業之前,必須先指派:
protected override void OnUpdate()
{
var job = new UpdateJob();
job.LastSystemVersion = this.LastSystemVersion;
job.InputATypeHandle = GetComponentTypeHandle<InputA>(true);
job.InputBTypeHandle = GetComponentTypeHandle<InputB>(true);
job.OutputTypeHandle = GetComponentTypeHandle<Output>(false);
this.Dependency = job.ScheduleParallel(m_Query, this.Dependency);
}
**注意:**為了提高效率,更改版本适用于整個塊,而不是單個實體。如果另一個具有寫入該類型元件功能的作業通路了塊,ECS就會增加該元件的更改版本,并且
DidChange()
函數會傳回true。即使聲明對元件進行寫如通路的作業實際上并未更改元件的值,ECS也會增加更改版本。
執行個體化并安排作業
要運作IJobChunk作業,必須建立作業結構體的執行個體,給結構體字段指派,然後安排作業。當你在SystemBase的
OnUpdate()
方法中執行時,系統會将安排作業每幀運作一次。
protected override void OnUpdate()
{
var job = new RotationSpeedJob()
{
RotationTypeHandle = GetComponentTypeHandle<Rotation>(false),
RotationSpeedTypeHandle = GetComponentTypeHandle<RotationSpeed>(true),
DeltaTime = Time.DeltaTime
};
this.Dependency = job.ScheduleParallel(m_Query, this.Dependency);
}
調用
GetArchetypeChunkComponentType<T>()
函數設定元件類型變量時,確定将隻需要讀取不需要寫入的元件的
isReadOnly
參數設定為true,這些參數可能會對ECS架構安排作業的效率産生重大影響。這些通路模式的設定,需要在結構體定義和EntityQuery中比對。
不要在系統類變量中緩存
GetArchetypeChunkComponentType<T>()
傳回值。你必須在每次系統運作時調用這個函數,并将更新後的值傳給作業。
擴充閱讀
- ECS中的Entity實體
- ECS之Component元件
- ECS之System系統
- 在ECS系統中使用Entities.ForEach
- 在ECS系統中使用Job.WithCode
【擴充學習】在洪流學堂公衆号回複 DOTS
可以閱讀本系列所有文章,更有視訊教程等着你!
呼~ 今天小新絮絮叨叨的真是夠夠的了。沒講清楚的地方歡迎評論,咱們一起探索。
我是大智(vx:zhz11235),你的技術探路者,下次見!
别走!點贊,收藏哦!
好,你可以走了。