Esta postagem faz parte de uma série sobre ECS. Se você não acompanhou as postagens anteriores, poderá começar a partir daqui.

Utilização de MultiThreading em sistemas de spawn

Como vimos no post anterior, criar entidades a partir de MonoBehaviours é decente e funciona. Mas pense em um cenário em que temos um grande número de entidades sendo instanciadas ao mesmo tempo.

Se assim como eu, você já teve que enfrentar este tipo de cenário, deve ter percebido um gargalo no desempenho e provavelmente fez uso de co-rotinas para amenizar o problema.

Instanciar as entidades utilizando um sistema MultiThreading faz todo sentido neste tipo de cenário e, particularmente eu acredito que na maioria dos outros cenários também. Por que não?

Antes de começarmos a espalhar nossas entidades pelo mundo, há algumas coisas que precisamos entender.

EntityCommandBuffer, lembre-se dele!

Quando alteramos dados estruturais no mundo ECS como por exemplo adicionar ou remover componentes, criar ou destruir entidades, corremos sérios riscos de criar uma condição de corrida se estivermos trabalhando em um sistema MultiThreading, pois outros threads ou até mesmo o thread principal podem estar lendo/gravando estes dados. Além disso, em um job não temos acesso ao nosso querido EntityManager.

Basicamente um EntityCommandBuffer (ou Buffer de comando de Entidade) como o nome sugere, é um buffer de trabalhos. Os trabalhos neste buffer são enfileirados e executados somente após todos os jobs anteriores estarem completos no quadro atual, resolvendo as questões de condições de corrida. No entanto, os mundos no sistema ECS possuem uma hierarquia de atualização que devemos respeitar e é preciso definir em que ordem os nossos ECB’s serão executados. Isso será mostrado abaixo.

Por favor, não ignore a leitura abaixo!

Talvez pareça um exagero, mas entender a ordem de atualização dos sistemas, ajuda a resolver grande parte dos problemas encontrados no desenvolvimento com DOTS. E saber a ordem em que as coisas funcionam, realmente faz diferença!

Ordem de atualização de sistemas

Cada SystemComponentGroup ( ou Grupo de Sistema de componentes ) especifica a ordem de atualização dos sistemas no mundo ECS. Nós podemos colocar um sistema em um grupo utilizando o atributo [UpdateInGroup] na classe do sistema e também utilizar [UpdateBefore] e [UpdateAfter] para especificar em que ordem de atualização dentro do grupo nosso sistema será executado.

Atributos utilizados com SystemComponentGroup

Na lista abaixo veremos alguns dos atributos utilizados na declaração de um sistema:

  • [UpdateInGroup] – Especifica um ComponentSystemGroup  ao qual este sistema deverá ser membro. Se esse atributo for omitido, o sistema será automaticamente adicionado ao grupo SimulationSystem padrão do mundo (veja abaixo).
  • [UpdateBefore] e [UpdateAfter] – Utilizados para ordenar a ordem de atualização dos sistemas. Os sistemas especificados serão ordenados dentro do grupo ao qual pertencem.

Grupos de sistema padrão

A lista abaixo mostra os grupos de sistema presentes no mundo padrão e uma breve descrição de cada um:

  • InitializationSystemGroup – Atualizado no final da inicialização do Player Loop.
  • SimulationSystemGroup – Atualizado no final de cada update.
  • PresentationSystemGroup – Atualizado no final de cada PreLateUpdate.

Você pode encontrar a árvore completa dos grupos de sistema AQUI.

Enfim, instanciando nossas entidades!

Então, depois de tudo esclarecido (eu espero que sim!), chegou a hora de vermos como instanciar nossas entidades utilizando EntityCommandBuffers.

Iremos criar uma entidade com um componente contendo os dados necessários para a criação das entidades que serão spawnadas e um sistema que irá processar estes dados e colocá-los em um trabalho com a utilização de um entityCommandBuffer que será responsável pela criação das entidades referidas.

SpawnnerComponent

Crie um script chamado SpawnnerComponent.cs com o conteúdo abaixo:

using Unity.Entities;
using Unity.Mathematics;

public struct SpawnnerComponent : IComponentData
{
    public Entity Prefab;
    public int SpawnNumber;
    public int divisions;
}

Nada de novo aqui, apenas a struct que compõe nosso componente.

SpawnnerAuthoring

Abaixo, o conteúdo do script SpawnnerAuthoring.cs:

using System.Collections.Generic;
using Unity.Entities;
using UnityEngine;
using Unity.Mathematics;
/// <summary>
/// Este script de conversão é semelhante aos vistos anteriormente.
/// A novidade aqui é o uso da interface IDeclareReferencedPrefabs.
/// </summary>

[RequiresEntityConversion]
public class SpawnnerAuthoring : MonoBehaviour, IDeclareReferencedPrefabs, IConvertGameObjectToEntity
{
    public GameObject Prefab;
    public int spawnNumber;
    public int divisions;

    //Os prefabs devem ser referenciados para que o sistema de conversão os conheça com antecedência.
    public void DeclareReferencedPrefabs(List<GameObject> referencedPrefabs)
    {
        referencedPrefabs.Add(Prefab);
    }

    public void Convert(Entity entity, EntityManager dstManager, GameObjectConversionSystem conversionSystem)
    {
        //Adicionamos SpawnnerComponent e setamos suas variáveis de forma apropriada.
        SpawnnerComponent m_spawnnerComponent = new SpawnnerComponent
        {
            Prefab = conversionSystem.GetPrimaryEntity(Prefab),
            SpawnNumber = spawnNumber,
            divisions = divisions
        };

        dstManager.AddComponentData(entity, m_spawnnerComponent);
    }
}

É semelhante aos scripts de conversão vistos anteriormente. A novidade é o uso da interface IDeclareReferencedPrefabs que está comentada no script.

SpawnnerSystem

Agora, vamos criar o script que contém o nosso sistema de spawn:

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

//Colocamos o nosso sistema dentro do grupo SimulationSystemGroup (Que é atualizado ao final do ciclo Update).
[UpdateInGroup(typeof(SimulationSystemGroup))]
public class SpawnnerSystem : JobComponentSystem
{
    //Utilizamos BeginSimulationEntityCommandBufferSystem, pois ele é atualizado antes de TransformSystemGroup.
    BeginInitializationEntityCommandBufferSystem m_EntityCommandBufferSystem;

    protected override void OnCreate()
    {
        // Setamos BeginInitializationEntityCommandBufferSystem para não precisar referenciá-la em cada quadro.
        m_EntityCommandBufferSystem = World.GetOrCreateSystem<BeginInitializationEntityCommandBufferSystem>();
    }

    /// <summary>
    /// IJobForEachWithEntity é semelhante a IJobForEach, mas dá acesso ao índice da entidade processada.
    /// Neste trabalho, varremos todas as entidades que contém o componente SpawnnerComponent e de acordo com
    /// os seus dados, "spawnamos" as entidades correspondentes.
    /// </summary>
    [BurstCompile]
    public struct SpawnnerJob : IJobForEachWithEntity<SpawnnerComponent>
    {
        //Referência ao nosso commandBuffer.
        public EntityCommandBuffer.Concurrent commandBuffer;

        public void Execute(Entity entity, int entityIndex, ref SpawnnerComponent m_SpawnnerComonent)
        {
            /* Instanciamos nossas entidades com algum espaço entre elas.
             * Repare que utilizamos o nosso commandBuffer ao invés de EntityManager.Instantiate().
             * Após criar a entidade, setamos o valor do componente Translation para posicionar no local desejado.
             */
            float3 spawnPosition = float3.zero;
            int divisions = 0;

            for (int i = 0; i < m_SpawnnerComonent.SpawnNumber; i++)
            {
                Entity instance = commandBuffer.Instantiate(entityIndex, m_SpawnnerComonent.Prefab);
                commandBuffer.SetComponent(entityIndex, instance, new Translation {Value = spawnPosition});
                spawnPosition.x += 1.25f;
                divisions++;

                if (divisions == m_SpawnnerComonent.divisions)
                {
                    spawnPosition.y += 1.25f;
                    divisions = 0;
                    spawnPosition.x = 0;
                }
            }

            //Destruímos a entidade que contém os dados.
            commandBuffer.DestroyEntity(entityIndex, entity);
        }
    }

    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        JobHandle handle = new SpawnnerJob{
            //Criamos e setamos nosso commandBuffer e agendamos nosso trabalho.
            commandBuffer = m_EntityCommandBufferSystem.CreateCommandBuffer().ToConcurrent()
        }.Schedule(this, inputDeps);

        // Precisamos dizer ao sistema qual trabalho ele precisa concluir antes que ele possa reproduzir os comandos.
        m_EntityCommandBufferSystem.AddJobHandleForProducer(handle);

        return handle;


    }
}

Iteramos todas as entidades com o componente SpawnnerComponent utilizando IJobForEachWithEntity que nos dá acesso ao índice da entidade (necessário para que tenhamos uma referência na hora de destruí-la).

Ao processar estes dados, criamos e posicionamos as entidades resultantes utilizando um EntityCommandBuffer. Estes dados são processados em um trabalho.

Conclusão

Para testar nosso sistema, crie um um objeto vazio na cena e adicione os scripts ConvertToEntity e SpawnnerAuthoring. Não esqueça de marcar a opção Convert and Destroy no script ConvertToEntity.

Crie um prefab e defina ele na variável Prefab de SpawnnerAuthoring. Defina também as variáveis Division e SpawnNumber.

No meu caso, utilizei o prefab de uma esfera, defini o número de objetos a serem instanciados como 10.000 com 100 divisões. O tempo para a criação das entidades foi praticamente instantâneo:

Dica:

Para um melhor desempenho, crie um material para o prefab a ser instanciado e marque a opção “Enable GPU Instancing”.

Apesar de ser um pouco mais complexo, um sistema de spawn utilizando Jobs faz sentido em cenários em que é preciso criar grandes quantidades de entidades.

artplayer

Author artplayer

More posts by artplayer

Deixe uma resposta

Receber notícias

Digite seu endereço de e-mail para receber notificações de novas publicações por e-mail.
%d blogueiros gostam disto: