ECS的简单入门(五):Entity Command Buffer

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);
    }
}

猜你喜欢

转载自blog.csdn.net/wangjiangrong/article/details/107363283