Unidade, ECS, Atores: como aumentar o FPS em seu jogo dez vezes, quando não há nada para otimizar [com edições]

O que é ECS
O que são atores

Eu sempre ouvi o quão bom é o modelo do ECS e que Jobs e Burst da biblioteca Unity são a solução para todos os problemas de desempenho. Para não adicionar a palavra “provavelmente” e “talvez” todas as vezes, discutindo a velocidade do código, decidi verificar tudo pessoalmente.

Meu objetivo era ter uma mente aberta sobre a rapidez com que essa ferramenta de desenvolvimento é e se usar paralelização para cálculos. E se for, é melhor usar o Unity.Jobs ou System.Threading ? Ao mesmo tempo, descobri qual é o uso do ECS em tarefas reais.


Condições de teste (próximas às tarefas reais do jogo):

  • O processador i5 2500 (4 núcleos sem hiper negociação) e o Unity2019.3.0f1
  • Every GameObject cada quadro ...

    A) se move ao longo de uma curva quadrática de Bezier por 10 minutos, do ponto inicial até o final.

    B) calcula seu colisor quadrado (caixa 10f10f), que usa math.sincos, math.asin, math.sqrt (o mesmo, cálculos bastante complicados para todos os testes).
  • Os objetos antes das medições do FPS são definidos em posições aleatórias na zona 720fx1280f e movem-se para um ponto aleatório nessa zona.
  • Tudo é testado na versão IL2CPP no PC
  • Os testes são gravados alguns segundos após o lançamento, para que todos os cálculos preliminares e a inclusão dos sistemas Unity não afetem o FPS. Pelas mesmas razões, apenas o código de atualização de cada quadro é mostrado.
  • Os objetos não têm uma exibição visual na versão para que a renderização não afete o FPS.

Posições de teste e código de atualização


  1. MonoBehaviour sequential (marcação condicional).
    O script MonoBehaviour é "travado" no objeto, na atualização da qual a posição, o colisor é calculado e o próprio é movido.

    Atualizar código
    void Update() { //    var velocityToOneFrame = velocityToOneSecond * Time.deltaTime; observedDistance += velocityToOneFrame; var t = observedDistance / distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(posToMove.c0, posToMove.c2,posToMove.c1); //   obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); //     tr.position = new Vector3(newPos.x, newPos.y); #if UNITY_EDITOR DebugDrowBox(obj.collBox, Color.blue, Time.deltaTime); #endif } 

  2. Atores sequenciais em classes de componentes sem paralelização.

    Atualizar código
     public void Tick(float delta) { foreach (ent entity in groupMoveBezier) { var cMoveBezier = entity.ComponentMoveBezier_noJob(); var cObject = entity.ComponentObject(); ref var obj = ref cObject.obj; //    var velocityToOneFrame = cMoveBezier.velocityToOneSecond * delta; cMoveBezier.observedDistance += velocityToOneFrame; var t = cMoveBezier.observedDistance / cMoveBezier.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(cMoveBezier.posToMove.c0, cMoveBezier.posToMove.c2,cMoveBezier.posToMove.c1); //   obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); //     cObject.tr.position = new Vector3(newPos.x, newPos.y, 0); #if UNITY_EDITOR DebugDrowBox(obj.collBox, Color.blue, Time.deltaTime); #endif } } 

  3. Atores + Trabalhos + Explosão

    Cálculo e movimento em Trabalhos das bibliotecas Unity.Jobs 0.1.1, Unity.Burst 1.1.2.
    Verificações de segurança - desativadas
    Anexar editor - desativado
    EmpregosDebbuger - desativado
    Para a operação normal de IJobParallelForTransform, todos os objetos em movimento possuem um "objeto pai" (até 255 partes de objetos em cada "pai", de acordo com a recomendação para desempenho máximo).
    Atualizar código
      public void Tick(float delta) { if (index <= 0) return; handlePositionUpdate.Complete(); #if UNITY_EDITOR for (var i = 0; i < index; i++) { var obj = nObj[i]; DebugDrowBox(obj.collBox, Color.blue, Time.deltaTime); } #endif jobPositionUpdate.nSetMove = nSetMove; jobPositionUpdate.nObj = nObj; jobPositionUpdate.deltaTime = delta; handlePositionUpdate = jobPositionUpdate.Schedule(transformsAccessArray); } } [BurstCompile] struct JobPositionUpdate : IJobParallelForTransform { public NativeArray<SetMove> nSetMove; public NativeArray<Obj> nObj; [Unity.Collections.ReadOnly] public float deltaTime; public void Execute(int index, TransformAccess transform) { var setMove = nSetMove[index]; var velocityToOneFrame = nSetMove[index].velocityToOneSecond * deltaTime; //    setMove.observedDistance += velocityToOneFrame; var t = setMove.observedDistance / setMove.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(setMove.posToMove.c0, setMove.posToMove.c2,setMove.posToMove.c1); nSetMove[index] = setMove; //   var obj = nObj[index]; obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); nObj[index] = obj; //     transform.position = (Vector2) newPos; } } public struct SetMove { public float2x3 posToMove; public float distanceFull; public float velocityToOneSecond; public float observedDistance; } 
  4. Atores + Parallel.For

    Em vez do loop For usual através de um grupo de entidades em movimento, Parallel.For é usado na biblioteca System.Threading.Tasks. Ele calcula a nova posição e o colisor em fluxos paralelos. Mover um objeto é realizado em um grupo vizinho.

    Atualizar código
      public void Tick(float delta) { Parallel.For(0, groupMoveBezier.length, i => { ref var entity = ref groupMoveBezier[i]; var cMoveBezier = entity.ComponentMoveBezier_actorsParallel(); ref var obj = ref entity.ComponentObject().obj; //    var velocityToOneFrame = cMoveBezier.velocityToOneSecond * delta; cMoveBezier.observedDistance += velocityToOneFrame; var t = cMoveBezier.observedDistance / cMoveBezier.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(cMoveBezier.posToMove.c0, cMoveBezier.posToMove.c2,cMoveBezier.posToMove.c1); //   obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox1.posAndSize.c1 }; obj.collBox1 = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); }); //     foreach (ent entity1 in groupMoveBezier) { var cObject = entity1.ComponentObject(); cObject.tr.position = new Vector3(cObject.obj.properties.c0.x, cObject.obj.properties.c0.y, 0); #if UNITY_EDITOR DebugDrowBox(cObject.obj.collBox1, Color.blue, Time.deltaTime); #endif } } 

Teste com movimento [1]:


500 objetos



(uma foto do editor próxima ao texto com FPS para mostrar o que está acontecendo visualmente)

  1. MonoBehaviour sequential:

  2. Atores sequenciais:

  3. Atores + Trabalhos + Explosão:

  4. Atores + Paralelo.


5000 objetos




  1. MonoBehaviour sequential:

  2. Atores sequenciais:

  3. Atores + Trabalhos + Explosão:

  4. Atores + Paralelo.



50.000 objetos



  1. MonoBehaviour sequential:

  2. Atores sequenciais:

  3. Atores + Trabalhos + Explosão:

  4. Atores + Paralelo.


Atores + Encadeados (construído na paralelização de Atores em System.Threading)


Os atores têm a capacidade de armazenar todos os componentes do jogo em estruturas, em vez de classes. Isso representa mais hemorróidas em termos de escrita de código, mas nessas condições o programa trabalha mais com a pilha e não com a pilha gerenciada, o que afeta significativamente sua velocidade.

Atualizar código
  public void Tick(float delta) { groupMoveBezier.Execute(delta); for (int i = 0; i < groupMoveBezier.length; i++) { ref var cObject = ref groupMoveBezier.entities[i].ComponentObject(); cObject.tr.position = new Vector3(cObject.obj.properties.c0.x, cObject.obj.properties.c0.y, 0); #if UNITY_EDITOR DebugDrowBox(cObject.obj.collBox, Color.blue, Time.deltaTime); #endif } } static void HandleCalculation(SegmentGroup segment) { for (int i = segment.indexFrom; i < segment.indexTo; i++) { ref var entity = ref segment.source.entities[i]; ref var cMoveBezier = ref entity.ComponentMoveBezier(); ref var cObject = ref entity.ComponentObject(); ref var obj = ref cObject.obj; //    var velocityToOneFrame = cMoveBezier.velocityToOneSecond * segment.delta; cMoveBezier.observedDistance += velocityToOneFrame; var t = cMoveBezier.observedDistance / cMoveBezier.distanceFull; if (t > 1f) t = 1f; var newPos = t.CalculateBesierPos(cMoveBezier.posToMove.c0, cMoveBezier.posToMove.c2, cMoveBezier.posToMove.c1); //   obj.properties.c0 = newPos; var posAndSize = new float2x2 { c0 = newPos, c1 = obj.collBox.posAndSize.c1 }; obj.collBox = obj.entity.NewCollBox(posAndSize, new float2(10f, 10f), obj.rotation.ToEulerAnglesZ()); } } 


nos componentes da classe

nos componentes da estrutura

Nesse caso, obtemos + 10% para o FPS, mas no exemplo existem apenas duas estruturas de componentes, e não dezenas, como deveria ser no produto final. O crescimento não linear do FPS é possível aqui, pois os componentes do programa de tipos de referência são substituídos por tipos de valor .

Conclusão


  • Em todos os casos, o FPS em Atores sem Paralelo. Para aumenta em cerca de duas vezes e com ele - em três vezes em comparação com o MonoBehaviour sequencial. Com o aumento dos cálculos matemáticos, essas proporções permanecem.
  • Para mim, uma vantagem adicional dos Atores ECS sobre o sequencial MonoBehaviour é que a paralelização dos cálculos, aumentando a velocidade, é adicionada elementarmente.
  • O uso de Atores + Trabalhos + Explosão aumenta o FPS em cerca de dez vezes em comparação com o sequencial MonoBehaviour
  • É certo que esse aumento no FPS se deve em grande parte ao Burst. Obviamente, para sua operação normal, você precisa usar tipos de dados do Unity.Mathematics (por exemplo, substitua Vector3 por float3)
    E é muito importante: no meu processador com 50.000 objetos na tela para aumentar o FPS com antes !
    Os seguintes pontos devem ser observados:
    1) Se nos cálculos você puder passar sem uma biblioteca, é melhor não usá-la (marcador vermelho - ruim, verde - bom)

    2) Você não pode usar a biblioteca Mathf - apenas matemática, caso contrário, o burst não poderá vetorizar e processar os dados.

  • A julgar por vários testes de terceiros, o MonoBehavior sequencial com 50.000 objetos mostra os mesmos ~ 50fps em todos os lugares. Mas o trabalho em Atores + Trabalhos ou Threaded é muito diferente.
    Além disso, quanto mais moderno o processador, mais útil é dividir o trabalho em vários trabalhos "enfileirados": cálculo de posição, colisor, movendo-se para uma posição.
    Você pode baixar um programa de teste e comparar o trabalho de Atores + Trabalhos + Disparo [um trabalho] com Atores + Trabalhos + Disparo [quatro trabalhos]. (No meu processador de quatro núcleos sem hiper negociação, o primeiro teste é -0.2ms mais rápido com 50.000 objetos)
  • A eficácia do ECS depende do número de elementos adicionais (renderização, física da unidade etc.).

[1] Não sei qual é o desempenho em outras estruturas do ECS, nos sistemas ECS-Unity / DOTS.

Fonte de teste

Agradecemos a Oleg Morozov (BenjaminMoore) por editar trabalhos, adicionar SceneSelector e um novo contador de fps.
Obrigado a iurii zakipnyi pelas instruções, revisões e testes adicionais Atores + Trabalhos + Explosão [quatro trabalhos]

Source: https://habr.com/ru/post/pt478824/


All Articles