天天看點

ECS的簡單入門(四):System概念建立一個System(SystemBase)System的層次結構以及執行順序(ComponentSystemGroup)補充Profiler

概念

在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十分的相像,它也有相應的生命周期,執行順序如下圖

ECS的簡單入門(四):System概念建立一個System(SystemBase)System的層次結構以及執行順序(ComponentSystemGroup)補充Profiler
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資訊等

ECS的簡單入門(四):System概念建立一個System(SystemBase)System的層次結構以及執行順序(ComponentSystemGroup)補充Profiler

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方法的參數可以作為我們的查詢條件,那麼這些參數具體有哪些規則呢?

我們最多可以設定八個參數,規則如下:

  1. 最前面的為按值傳遞的參數(一些ECS定義好的特殊參數)
  2. 其次是可寫的參數(ref Component)
  3. 最後為隻讀的參數(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,如下

ECS的簡單入門(四):System概念建立一個System(SystemBase)System的層次結構以及執行順序(ComponentSystemGroup)補充Profiler

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的使用情況。

ECS的簡單入門(四):System概念建立一個System(SystemBase)System的層次結構以及執行順序(ComponentSystemGroup)補充Profiler

繼續閱讀