在前面的例子中,我们都是通过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
在程序执行的时候,有些操作可能需要等待所有当前正在被调度的Job全部执行完成,那么这个时间点我们可以称之为一个同步点(sync point)。
像前面的我们在一个System中要销毁一个Entity,但是同一帧中,可能有另一个System会对这个Entity的别的Component的值进行修改,那么就肯定会出错。因此我们的销毁操作应该等其他System中的操作都做完再去执行。
如果同步点越多那么我们的Job的执行效率就会越差,需要很多的等待,因此我们要尽量的避免同步点。
结构变化(Structural change)是造成同步点的主要原因,下面的操作都会造成结构变化
可以看出,所谓的结构变化,就是我们的操作导致存储Entity的Chunk发生了变化。而这些操作只能在主线程中去执行。这也就是我们我们前面不能在Job中(子线程)去销毁Entity的原因了。
这个时候就要由我们的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是前面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,他们也不例外,如下:
我们应该尽量使用这些已存在的EntityCommandBufferSystem,会比我们自己创建EntityCommandBufferSystem产生更少的Sync point。我们可以使用下面方法来获取到这些EntityCommandBufferSystem:
EndSimulationEntityCommandBufferSystem endSimulationEcbSystem = World.GetOrCreateSystem();
接着可以利用CreateCommandBuffer方法来创建一个ECB:
EntityCommandBuffer ecb = endSimulationEcbSystem.CreateCommandBuffer();
接着我们就可以使用ECB来执行Structural change操作:
ecb.CreateEntity();
ecb.DestroyEntity(entity);
ecb.AddComponent(entity);
ecb.RemoveComponent(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(entityInQueryIndex, entity);
ecbc.RemoveComponent(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();
}
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);
}
}