概念
在System中,我們就可以對Component中的資料進行處理了,例如我們每幀改變Translation Component的值,就可以使Entity移動起來。
ECS為我們提供了以下幾種System類型,通常情況下,若我們要自己編寫一個System用來處理Component資料的話,隻需要繼承SystemBase即可。其他的System類型用于提供一些特殊的功能,ECS已經為我們提供了幾個EntityCommandBufferSystem和ComponentSystemGroup的執行個體,一般情況下使用它們即可。
SystemBase | 繼承該類就可以實作一個System的建立 |
EntityCommandBufferSystem | 用于解決Sync points的問題,在下一篇會講解到 |
ComponentSystemGroup | 用于将其他的System進行層次結構的劃分以及排序,ECS提供了幾個預設的Component System Group |
GameObjectConversionSystem |
注:ComponentSystem和JobComponentSystem兩個類以及它們的IJobForEach接口,将會被DOTS棄用。請使用SystemBase類與對應的Entities.ForEach接口代替。
ECS會自動找到項目中所有的System,并在運作時執行個體化它們,并将它們添加進一個預設的組裡面。
建立一個System(SystemBase)
我們可以通過繼承抽象類SystemBase來建立System(Entity和Component都是struct類型,System為class類型)。同時還必須要重寫其OnUpdate方法,而其他的基類方法可以在我們需要的時候去重寫。
System就和我們平常使用的MonoBehaviour十分的相像,它也有相應的生命周期,執行順序如下圖
OnCreate | System被建立的時候調用 |
OnStartRunning | 在第一次OnUpdate之前和System恢複運作的時候調用 |
OnUpdate | System的Enabled為true時,每幀調用 |
OnStopRunning | System的Enabled為false時,或者沒有找到相對應的Entity會調用,OnDestroy前也會調用 |
OnDestroy | System被銷毀的時候調用 |
一個System Group下有多個System,System的OnUpdate方法是由System Group的OnUpdate方法調用的,若我們設定System Group的Enabled屬性,同時會影響到該Group下所有的System的Enabled屬性。(有關System Group的知識後面會講解到)
System所有的事件都是在主線程上執行的,理想的情況下,我們OnUpdate裡的邏輯應該使用Job來進行多線程處理,進而提高效率。我們可以使用下面方法來在System中排程Job
Entities.ForEach | 疊代ECS Component資料的最簡單方法 |
Job.WithCode | 單獨執行一個lambda方法,背景job |
IJobChunk | 根據一個個Chunk去查找Component中的資料 |
C# Job System | 自己編寫Job,執行個體化并排程 |
注:不太了解Job System的可以參考下之前的文章:https://blog.csdn.net/wangjiangrong/article/details/107020789
我們可以在Entity Debugger中檢視所有的System以及對應的Group資訊等
Entities.ForEach
Entities.ForEach方法由SystemBase提供,它讓我們很簡單的就實作對Entity上Component的邏輯處理。Entities.ForEach的參數為一個lambda方法,其參數即為Entity的查詢條件,查找到所有符合條件的Entity後會執行其方法體。
對于要執行的lambda方法,我們可以使用Schedule或者ScheduleParallel方法來排程,也可使用Run方法來在主線程上立即調用。
例如下面例子,ApplyVelocitySystem會根據我們設定的參數查詢所有帶有Translation和Velocity兩個元件的Entity,然後每幀将Velocity元件的值加到Translation元件的值上。
class ApplyVelocitySystem : SystemBase
{
protected override void OnUpdate()
{
Entities.ForEach((ref Translation translation, in Velocity velocity) =>
{
translation.Value += velocity.Value;
}).Schedule();
}
}
注:如果我們要修改一個元件的值需要添加 ref 關鍵字,若隻是讀取一個元件的值,則使用 in 關鍵字,這樣可以提高性能。
查詢規則
前面我們說到會根據lambda表達式的參數去查詢Entity,除此之外我們還可以使用下面幾個方法來更詳細的制定查詢規則(類似于EntityQueryDesc)
WithAll | 除了包含lambda表達式參數的元件外,還要包含WithAll中所有的元件 |
WithAny | 至少包含WithAny中的一個元件 |
WithNone | 不能包含WithNone中的元件 |
看個例子:
Entities.WithAll<LocalToWorld>()
.WithAny<Rotation, Translation, Scale>()
.WithNone<LocalToParent>()
.ForEach((ref Destination outputData, in Source inputData) =>
{
}).Schedule();
就很好了解了,查詢規則為,必須含有LocalToWorld,Destination和Souce元件,至少含有Rotation,Translation和Scale三個元件中的一個,不能含有LocalToParent元件。
若我們的ForEach中包含了Share Component,那麼必須調用WithoutBurst方法和使用Run來執行,例如
Entities
.WithoutBurst()
.ForEach((ref Translation translation, in RenderMesh mesh) =>
{
}).Run();
擷取EntityQuery
注:EntityQuery 的具體介紹可以檢視前面介紹Entity的文章
我們可以通過 WithStoreEntityQueryInField(ref query) 方法來擷取到EntityQuery對象來供我們使用,例如使用 EntityQuery.CalculateEntityCount() 方法可以知道查詢到的Entity數量。
private EntityQuery query;
protected override void OnUpdate()
{
int dataCount = query.CalculateEntityCount();
Entities
.WithStoreEntityQueryInField(ref query)
.ForEach((in Translation data) =>
{
}).ScheduleParallel();
}
Filter
我們可以利用 WithChangeFilter<T>() 和 WithSharedComponentFilter() 方法來設定filter,如下
Entities
.WithChangeFilter<Translation>()
.WithSharedComponentFilter(new RenderMesh(){material = cubeMaterial})
.WithoutBurst()
.ForEach((ref Translation translation, in RenderMesh mesh) =>
{
translation.Value += new float3(0, time, 0);
}).Run();
System中會找到含有Translation 和 RenderMesh 兩個元件,并且RenderMesh的material值為cubeMaterial,同時在别的System中Translation的值發生了變化的Entity。
注:若Entity中的Component被賦予了寫的權限,那麼這個Component的值就會被标記為發生了變化,即使它的值沒有改變。
lambda方法參數定義
前面講到Entities.ForEach的參數是一個lambda方法,而lambda方法的參數可以作為我們的查詢條件,那麼這些參數具體有哪些規則呢?
我們最多可以設定八個參數,規則如下:
- 最前面的為按值傳遞的參數(一些ECS定義好的特殊參數)
- 其次是可寫的參數(ref Component)
- 最後為隻讀的參數(in Component)
代碼示例:
Entities.ForEach(
(Entity entity,
int entityInQueryIndex,
ref Translation translation,
in Movement move) => {/* .. */})
特殊參數:
Entity entity | 目前的Entity對象(參數類型必須為Entity,參數名可以自定義) |
int entityInQueryIndex | entity在隊列中的下标(參數名固定) |
int nativeThreadIndex | 執行目前操作的線程唯一下标(參數名固定) |
Job.WithCode
Job.WithCode由SystemBase提供,可以輕松的實作一個在背景運作的Job。我們也可以使其在主線程執行和使用Burst編譯來提高執行效率。
public class TestSystem : SystemBase
{
protected override void OnUpdate()
{
NativeArray<float> randomNumbers = new NativeArray<float>(5, Allocator.TempJob);
Job.WithCode(() =>
{
for (int i = 0; i < randomNumbers.Length; i++)
randomNumbers[i] = i;
}).Schedule();
randomNumbers.Dispose();
}
}
本質上就是把編寫一個Job并執行個體化的過程封裝在了 Job.WithCode 中了。
IJobChunk Job(推薦使用)
IJobChunk其實是和JobSystem中IJob,IJobFor等一樣的,ECS拓展的一個用于實作Job的接口。和其他的Job接口一樣,需要實作一個Execute方法,IJobChunk的Execute方法如下:
void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex);
來看下這三個參數:
ArchetypeChunk chunk:在我們執行實作了IJobChunk的Job的時候,需要傳入一個EntityQuery參數。而Entity根據其Archetype存放在不同的Chunk中,這個參數代表的就是這些Chunk。例如我們傳入的EntityQuery中含有M個Entity,這些Entity分部在N個Chunk中,那個這個Execute方法會在每次排程Job的時候調用N次,這個參數即跟這些Chunk資訊一一對應。
int chunkIndex:該Chunk的下标
int firstEntityIndex:該Chunk中第一個Entity的下标
實作IJobChunk的Job
在Execute方法中,我們可以利用 chunk.GetNativeArray(ArchetypeChunkComponentType type) 來擷取到該Chunk下符合條件的所有Entity,傳回的是一個NativeArray數組。Entity數量除了使用NativeArray.Length外還可使用chunk.count來擷取。
在Execute方法外,我們除了定義一些需要的參數以為,還需要定義ArchetypeChunkComponentType<T>類型的參數用于擷取資料,T即為Component類型。
例如我們要實作一個每幀移動帶有Translation元件的Entity的功能,那麼Job可以這麼寫
public struct CubeMoveJob : IJobChunk
{
public float deltaTime;
public ArchetypeChunkComponentType<Translation> translationType;
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
var translationArray = chunk.GetNativeArray(translationType);
for (var i = 0; i < chunk.Count; i++)
{
var translation = translationArray[i];
translationArray[i] = new Translation(){Value = new float3(translation.Value.x, translation.Value.y + deltaTime, translation.Value.z)};
}
}
}
我們可以定義多個ArchetypeChunkComponentType,若有些Component隻需要讀取其資料不需要修改的話,記得在該ArchetypeChunkComponentType前面添加[ReadOnly]标簽來提升性能。
如果我們在擷取EntityQuery的時候用到了Any屬性,例如
var desc = new EntityQueryDesc
{
Any = new ComponentType[] {typeof(Translation), typeof(NonUniformScale)},
};
query = GetEntityQuery(desc);
那麼在我們的Execute方法中,Chunk内的Entity可能隻擁有其中一個Component,我們可以利用 chunk.Has(ArchetypeChunkComponentType) 方法來判斷是否含有對應Component,例如對上面的Job的Execute方法體進行修改:
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
if(!chunk.Has(translationType)) return;//新增
var translationArray = chunk.GetNativeArray(translationType);
......
}
使用Job
和普通的Job一樣,我們需要執行個體化Job,然後設定對應的參數,最後排程即可。
我們可以利用SystemBase.GetArchetypeChunkComponentType<T>(bool isReadOnly)方法來擷取到對應Component(T)的 ArchetypeChunkComponentType屬性,其參數用于标記是否是隻讀的。
在排程Job的時候需要傳入EntityQuery和SystemBase提供的Dependency
System的代碼如下,即可實作利用IJobChunk來處理資料
public class CubeMoveSystem : SystemBase
{
EntityQuery query;
protected override void OnStartRunning()
{
base.OnStartRunning();
query = GetEntityQuery(typeof(Translation));
}
protected override void OnUpdate()
{
float time = Time.DeltaTime;
var job = new CubeMoveJob() {
translationType = GetArchetypeChunkComponentType<Translation>(),
deltaTime = time
};
Dependency = job.ScheduleParallel(query, Dependency);
}
}
實作類似ChangeFilter的功能
在某些情況下我們可能隻需要處理修改過的Component,而不需要處理所有的。根據前面的知識,我們可以通過 EntityQuery.SetChangedVersionFilter() 進行過濾。但是此方法最多隻支援過濾兩個Component,若有時候我們需要過濾更多未改動的Component時,需要怎麼辦呢?
我們可以利用 ArchetypeChunk.DidChange(ArchetypeChunkComponentType type, uint lastSystemVersion) 方法來判斷某個Component是否發生了修改。SystemBase為我們提供了LastSystemVersion屬性,我們需要将這個值傳遞給Job,才能正确的做出判斷。
簡單的實作如下:
public struct CubeMoveJob : IJobChunk
{
......
public uint lastSystemVersion;//新增
public void Execute(ArchetypeChunk chunk, int chunkIndex, int firstEntityIndex)
{
//新增
bool isChange = chunk.DidChange(translationType, lastSystemVersion);
if(!isChange) return;
//原有邏輯
......
}
}
public class CubeMoveSystem : SystemBase
{
......
protected override void OnUpdate()
{
......
var job = new CubeMoveJob() {
translationType = GetArchetypeChunkComponentType<Translation>(),
deltaTime = time,
lastSystemVersion = LastSystemVersion//新增
};
......
}
}
這樣若我們需要判斷多個Component的時候,隻需要調用多次DidChange方法并對比結果即可。
C# Job System
除了使用IJobChunk外,我們也可以直接使用普通的Job來實作。原理差不多就不做過多介紹了,直接看代碼
public class CubeMoveSystem : SystemBase
{
[BurstCompile]
struct RotationSpeedJob : IJobParallelFor
{
[DeallocateOnJobCompletion] public NativeArray<ArchetypeChunk> chunks;
public ArchetypeChunkComponentType<Translation> translationType;
public float deltaTime;
public void Execute(int chunkIndex)
{
var chunk = chunks[chunkIndex];
var translationArray = chunk.GetNativeArray(translationType);
for (int i = 0; i < chunk.Count; i++)
{
var translation = translationArray[i];
translationArray[i] = new Translation(){Value = new float3(translation.Value.x, translation.Value.y + deltaTime, translation.Value.z)};
}
}
}
EntityQuery query;
protected override void OnStartRunning()
{
query = GetEntityQuery(typeof(Translation));
}
protected override void OnUpdate()
{
var chunks = query.CreateArchetypeChunkArray(Allocator.TempJob);
var rotationsSpeedJob = new RotationSpeedJob
{
chunks = chunks,
translationType = GetArchetypeChunkComponentType<Translation>(),
deltaTime = Time.DeltaTime,
};
Dependency = rotationsSpeedJob.Schedule(chunks.Length, 32, Dependency);
}
}
System的層次結構以及執行順序(ComponentSystemGroup)
在前面的例子中,我們隻需要編寫一個System加好相關的邏輯後,運作的時候該System就會被執行。原來ECS會在運作時自動找到我們項目中的System,執行個體化它們并将其添加到預設的System Group中。這些System Group其實就是繼承于我們的ComponentSystemGroup。
ComponentSystemGroup
ComponentSystemGroup類和SystemBase類一樣都是ComponentSystemBase的子類,是以它也和SystemBase類似,有着OnUpdate等方法。ComponentSystemGroup中有一個List用于存放相關ComponentSystemBase類,然後在OnUpdate方法中周遊這個List調用它們的Update方法。是以我們System的Update方法就是在這些ComponentSystemGroup中被調用的。
由于ComponentSystemGroup也屬于ComponentSystemBase類,是以可以形成嵌套的關系,例如ComponentSystemGroup A在ComponentSystemGroup B的List中,當A在B中被調用了Update之後,A中List中的資料也會跟着被調用Update。(大家可以看下源碼,可以更好的了解)
在實際開發中,我們往往需要一個System在另一個System之後執行,來保證邏輯的正确,說白了就是ComponentSystemGroup中List的排序問題。ECS為我們提供 [UpdateBefore] and [UpdateAfter] 兩個Attribute,可以讓我們很輕松的解決System的排序問題。
一些常用的Attribute
[UpdateBefore] 和 [UpdateAfter]
字面意思,使一個System的Update方法在另一個System的前或者後執行(當然兩個System需要在同一個Group中),在聲明System Class的時候使用。例如:
public class aa : SystemBase
{
protected override void OnUpdate()
{
}
}
[UpdateBefore(typeof(aa))]
public class bb : SystemBase
{
protected override void OnUpdate()
{
}
}
這樣就可以保證bb的OnUpdate在aa之前執行。
需要注意的是除了OnUpdate外,也會影響OnStartRunning和OnStopRunning的執行順序(可以看下SystemBase的Update方法)但是不會影響OnCreate的執行順序。
同時這兩個Attribute還可以作用于ComponentSystemGroup,設定整個Group中所有System和另個Group中所有System的Update先後執行順序。
[UpdateInGroup]
設定System歸屬的ComponentSystemGroup,如果不設定,ECS會将該System加到預設World的SimulationSystemGroup中。
[DisableAutoCreation]
前面說到我們的ECS會找到所有的System,執行個體化它們并将其添加到預設的Group中,設定此标簽就可以阻止這一步操作。
預設的System Group
ECS在預設的World中已經為我們建立了一些ComponentSystemGroup執行個體,我們可以利用Entity Debugger看見這些Group,如下
Group和System的層次結構可以清晰明了,我們的自定義的System(如圖中的CubeMoveSystem)也被添加在了SimulationSystemGroup當中
補充
System.Enabled
我們可以通過World擷取到System,然後通過設定其Enable屬性來控制System是否啟用,如下
CustomSystem system = World.DefaultGameObjectInjectionWorld.GetOrCreateSystem<CustomSystem>();
system.Enabled = false;
Profiler
我們可以通過Window->Analysis->Profiler打開Profiler視窗,在CPU Usage欄中可以檢視到我們CPU的使用情況。