天天看點

ECS的簡單入門(五):Entity Command BufferEntity Command Buffer (ECB)

Entity Command Buffer (ECB)

在前面的例子中,我們都是通過EntityManager來建立或銷毀Entity,或者給Entity添加删除Component。但是在實際的情況中,我們可能需要在System中來做這些操作,比如一個Entity有一個存活時長的屬性,我們會用一個System來計算已存活的時間,當時間到了後銷毀該Entity。

根據前面的知識,我們需要一個Component來存儲這個存活時長的屬性,例如:

struct Lifetime : IComponentData
{
    public float Value;
}
           

然後需要一個System對該Component進行處理,例如:

class LifetimeSystem : SystemBase
{
    protected override void OnUpdate()
    {
        float time = Time.DeltaTime;
        Entities.ForEach((Entity entity, ref Lifetime lifetime) =>
        {
            if (lifetime.Value <= 0)
                World.DefaultGameObjectInjectionWorld.EntityManager.DestroyEntity(entity);
            else
                lifetime.Value -= time;
        }).ScheduleParallel();
    }
}
           

代碼看着好像沒啥問題,每幀減去已存活的時間,當時間為0後利用我們的EntityManager來銷毀這個Entity。

但是切回Unity,編譯後會發現,編譯報錯了:

error DC0027: Entities.ForEach Lambda expression makes a structural change. Use an EntityCommandBuffer to make structural changes or add a .WithStructuralChanges invocation to the Entities.ForEach to allow for structural changes.  Note: LambdaJobDescriptionConstruction is only allowed with .WithoutBurst() and .Run().

從這個報錯中,引出一個問題,何為structural change?在解釋structural change之前,我們先來了解下Sync points

Sync points (synchronization point)

在程式執行的時候,有些操作可能需要等待所有目前正在被排程的Job全部執行完成,那麼這個時間點我們可以稱之為一個同步點(sync point)。

像前面的我們在一個System中要銷毀一個Entity,但是同一幀中,可能有另一個System會對這個Entity的别的Component的值進行修改,那麼就肯定會出錯。是以我們的銷毀操作應該等其他System中的操作都做完再去執行。

如果同步點越多那麼我們的Job的執行效率就會越差,需要很多的等待,是以我們要盡量的避免同步點。

Structural changes

結構變化(Structural change)是造成同步點的主要原因,下面的操作都會造成結構變化

  • 建立Entity
  • 銷毀Entity
  • 給Entity添加Component
  • 删除Entity中的Component
  • 修改Entity中Share Component的值

可以看出,所謂的結構變化,就是我們的操作導緻存儲Entity的Chunk發生了變化。而這些操作隻能在主線程中去執行。這也就是我們我們前面不能在Job中(子線程)去銷毀Entity的原因了。

Avoiding sync points(避免同步點)

這個時候就要由我們的Entity Command Buffer(ECB)出場了,ECB可以讓我們會導緻結構變化的操作排隊等候,而不是立即執行。存儲在ECD中的指令可以在一幀的最後時刻再去執行,這樣原本一幀内可能有多個同步點,可以集中到一起,變成一個同步點。

在前面介紹ComponentSystemGroup的時候,我們知道它有一個List(m_systemsToUpdate)用于存放該Group下所有的System。在一個标準的ComponentSystemGroup執行個體中,這個List的前後會分别有一個EntityCommandBufferSystem的執行個體,這也是一種繼承于ComponentSystemBase的System類型,我們可以從中擷取到ECB對象。一幀内,在Group中的所有System的Structural change都會在同一時刻被執行。同時使用ECB我們可以将結構變化的操作放在Job中多線程處理,否則隻能在主線程中去執行。

如果有些任務不能使用EntityCommandBufferSystem,那麼我們可以嘗試将組内帶有Structural change的System排列在一起,因為如果兩個帶有Structural change的System的Update方法是接連着的,那麼隻會産生一個Sync point。

EntityCommandBufferSystem

EntityCommandBufferSystem是前面System篇沒有提到的,它也是ECS提供的System中的一種。我們可以從一個EntityCommandBufferSystem中擷取到多個ECB對象,然後根據他們被建立的順序,在Update的時候執行它們。這樣在Update的時候隻會造成一個Sync point,而不再是一個ECB産生一個Sync point。

前面我們提到ECS預設的World給我們提供了三個System Group,分别為initialization, simulation, and presentation。前面提到每個System Group中存放System的List前後都應該有一個EntityCommandBufferSystem,他們也不例外,如下:

  • BeginInitializationEntityCommandBufferSystem
  • EndInitializationEntityCommandBufferSystem
  • BeginSimulationEntityCommandBufferSystem
  • EndSimulationEntityCommandBufferSystem
  • BeginPresentationEntityCommandBufferSystem

我們應該盡量使用這些已存在的EntityCommandBufferSystem,會比我們自己建立EntityCommandBufferSystem産生更少的Sync point。我們可以使用下面方法來擷取到這些EntityCommandBufferSystem:

EndSimulationEntityCommandBufferSystem endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
           

接着可以利用CreateCommandBuffer方法來建立一個ECB:

EntityCommandBuffer ecb = endSimulationEcbSystem.CreateCommandBuffer();
           

接着我們就可以使用ECB來執行Structural change操作:

ecb.CreateEntity();
ecb.DestroyEntity(entity);
ecb.AddComponent<Translation>(entity);
ecb.RemoveComponent<Translation>(entity);
ecb.SetSharedComponent(entity, new RenderMesh());
           

如果我們的ECB對象要在一個被排程的多線程Job中使用(Job.ScheduleParallel()),我們需要調用ToConcurrent方法,來将其轉換成一個并發的ECB對象。

EntityCommandBuffer.Concurrent ecbc = ecb.ToConcurrent();
           

這樣在ECB中的指令序列不在依賴于代碼的執行順序,是以我們必須将Entity在EntityQuery中的下标作為參數傳遞進去:

ecbc.CreateEntity(entityInQueryIndex);
ecbc.DestroyEntity(entityInQueryIndex, entity);
ecbc.AddComponent<Translation>(entityInQueryIndex, entity);
ecbc.RemoveComponent<Translation>(entityInQueryIndex, entity);
ecbc.SetSharedComponent(entityInQueryIndex, entity, new RenderMesh());
           

最後我們要利用AddJobHandleForProducer方法将我們的System的JobHandle加到EntityCommandBufferSystem中

endSimulationEcbSystem.AddJobHandleForProducer(Dependency);
           

執行個體

前面講了一堆有的沒有,程式員還是看代碼最實際。我們如何使用ECB解決前面的報錯問題呢?代碼如下

class LifetimeSystem : SystemBase
{
    EndSimulationEntityCommandBufferSystem endSimulationEcbSystem;
    
    protected override void OnCreate()
    {
        base.OnCreate();
        endSimulationEcbSystem = World.GetOrCreateSystem<EndSimulationEntityCommandBufferSystem>();
    }

    protected override void OnUpdate()
    {
        float time = Time.DeltaTime;
        EntityCommandBuffer ecb = endSimulationEcbSystem.CreateCommandBuffer();
        EntityCommandBuffer.Concurrent ecbc = ecb.ToConcurrent();
        Entities.ForEach((Entity entity, int entityInQueryIndex, ref Lifetime lifetime) =>
        {
            if (lifetime.Value <= 0)
                ecbc.DestroyEntity(entityInQueryIndex, entity);
            else
                lifetime.Value -= time;
        }).ScheduleParallel();
        endSimulationEcbSystem.AddJobHandleForProducer(this.Dependency);
    }
}