ECS丨回到主线程搞事情 + ECS生命周期创建实体

提示:选中右侧目录,可快速找到所需内容

本系列博客地址:传送门

本节我们要讲在主线程操作实体。如何在主线程:

  • 创建实体(Create Entities)
  • 销毁实体(Destroy Entities)
  • 给实体添加组件(Add Components)
  • 删除实体的组件(Remove Components)

一、在主线程操作的原因

至于为什么要在主线程做这件事,让我们思考下面问题:

我们已经知道,JobComponentSystem配合各种JobIJobForEach、IJobChunk等),可以方便地实现并行(多线程、多核)执行逻辑。既然涉及到多线程,就会有一个麻烦的事情——某个线程做了破坏结构的操作,其他线程会受到影响。

注:System的OnUpdate函数都是在主线程调用,Job才是在多线程中并行调用的。

比如,某个Job给实体删除了一个组件,会发生什么事情?

我们的实体都是按块(Chunk)存储的,一旦某个实体的组件数量或类型改变了,它就不属于当前的块,它会被移到其他块里。所以,如果某Job给实体删除了一个组件,那么,这个实体就会被移到另一个块里。

那么,另外一个并行Job呢?这个并行的Job还不知道实体被移到另一个块了,也不知道这个实体被删除了某个组件,所以这个并行的Job会做出一些不太正确的操作。(操作了即将不存在的组件、或操作了错误的块里的实体)

为了解决这种冲突,ECS规定,以下行为都不能在Job中处理:

  • 创建实体(Create Entities)
  • 销毁实体(Destroy Entities)
  • 给实体添加组件(Add Components)
  • 删除实体的组件(Remove Components)

二、怎样在主线程搞事

上面的四种行为都不能在Job中处理,但是,很多情况下,只有在Job中才能决定要不要创建实体、添加组件等,这种时候应该怎么办?

于是,就有了EntityCommandBufferSystem。它可以让我们在Job(IJobForEach、IJobChunk等)里添加一些任务队列,然后在主线程中执行这些任务。

ECS默认的三个系统分组,分别都有一个 Begin 和 End 的 EntityCommandBufferSystem。为的是让我们可以在分组的开始或结束时作一些特定的操作。比如,创建/销毁实体,大部分情况下就是在第一个分组的BeginInitializationEntityCommandBufferSystem里进行。

BeginInitializationEntityCommandBufferSystem是在初始化阶段的第一个System,它是最先执行的。

我们只要把创建/销毁实体等操作放到BeginInitializationEntityCommandBufferSystem里面执行,就不怕后续逻辑出现冲突问题了。

System:

所以,如何把创建实体的操作放到BeginInitializationEntityCommandBufferSystem执行?

代码如下:

using Unity.Collections;
using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;

public class SpawnerSystem_FromEntity : JobComponentSystem
{
    BeginInitializationEntityCommandBufferSystem beginIni;

    protected override void OnCreate()
    {
        // 获取或创建 BeginInitializationEntityCommandBufferSystem
        beginIni = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        // 获取了队列
        var commandBuffer = beginIni.CreateCommandBuffer().ToConcurrent();

        var jobHandle = Entities    // entity,通过筛选 Spawner_FromEntity 得到的实体
            .ForEach((Entity entity, int entityInQueryIndex, in Spawner_FromEntity spawnerFromEntity) =>
            {
                // 在Buffer中创建实体
                var instance = commandBuffer.Instantiate(entityInQueryIndex, spawnerFromEntity.prefab_SFE);

                // 删除筛选出来的实体对象,这个很重要,后面会解释
                commandBuffer.DestroyEntity(entityInQueryIndex, entity);
            }).Schedule(inputDeps);

        // 把Job添加到 EntityCommandBufferSystem
        beginIni.AddJobHandleForProducer(jobHandle);

        return jobHandle;
    }
}

问题解释:

1、Job在Update里,beginIni.AddJobHandleForProducer(jobHandle)会无限添加Job吗?

答:在JobComponentSystem中,如果Job没有筛选出实体数据,那么,OnUpdate是不会被调用的。

ForEach里是筛选了Spawner_FromEntity组件的,而我们这个程序里只有一个实体拥有这个组件,而后面又通过DestroyEntity将筛选出来的实体删除了。

于是,在下一轮的循环中,已经筛选不出任何实体了,于是,OnUpdate函数也不会被调用。

Component

原先我们直接将转化实体的代码放在C里,现在我们分开了,C只放数据,转化实体代码在E下面。

之前的Component加了转化实体特性 [GenerateAuthoringComponent],使得C可以挂载到实体上。但下方代码没加,继承的不是Mono,所以不能挂在物体上。这个记录了数据的C只能通过下方的转化实体代码动态挂载到物体上。

Spawner_FromEntity 组件是Component,内容如下:

using Unity.Entities;

public struct Spawner_FromEntity : IComponentData
{
    public Entity prefab_SFE;
}

Entity

之前我们学过了:

下面代码作用是将本物体转化成实体,且将本脚本上的预制体转化成实体赋值给C。

本节操作方法如下:

将如下代码挂在在空物体上,且挂上 ConvertToEntity 。

该代码挂载上预制体。这个预制体就是我们后面要生成的预制体。

那这个C上的预制体什么时候在场景中生成呢?看S代码。

using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;

[RequiresEntityConversion]
public class SpawnerAuthoring_FromEntity : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
    public GameObject prefab_SAFE;

    // 被 IDeclareReferencedPrefabs 接口调用
    // DeclareReferencedPrefabs:让GameObjectConversionSystem对象知道我们预制体的存在,以便通过预制体创建实体。
    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(prefab_SAFE);
    }

    // 现在拥有两个实体:
    // 1、Convert 被 IConvertGameObjectToEntity 接口调用,将当前物体转化为实体
    // 2、Convert 内,给当前实体添加了 Spawner_FromEntity 组件,且给这个组件赋值了转化后的实体。
    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        var spawnerData = new Spawner_FromEntity
        {
            // GetPrimaryEntity:将我们的GameObject对象转化成Entity对象。
            prefab_SFE = conversionSystem.GetPrimaryEntity(prefab_SAFE),
        };
        dstManager.AddComponentData(entity, spawnerData);
    }
}

待我整理整理,看看这几种创建实体的方法到底有什么优缺点,头大

更新中

原创文章 347 获赞 98 访问量 23万+

猜你喜欢

转载自blog.csdn.net/weixin_38239050/article/details/105535213