Pesquisar em Unity

A evolução das melhores práticas Unity

Last updated: December 2018

What you will get from this talk: updated scripting performance and optimization tips from Ian Dundore that reflect the evolution of Unity’s architecture to support data-oriented design.

Unity’s evolving, and the old tricks might no longer be the best ways to squeeze performance out of the engine. In this article, you’ll get a rundown of a few Unity changes (from Unity 5.x to 2018.x) and how you can take advantage of them. All of these practices come from Ian Dundore's talk, so, if you prefer, you can go straight to the source.

Desempenho de scripts

One of the most difficult tasks during optimization is choosing how to optimize code once a hot-spot has been discovered. There are many different factors involved: platform-specific details of the operating system, CPU, and GPU; threading; memory access; and several others. It is difficult to know in advance which optimization will produce the biggest real-world benefit.

Generally, it’s best to prototype optimizations in small test projects – you can iterate much faster. However, isolating code into a test project poses its own challenge: simply isolating a piece of code changes the environment in which it runs. Thread timings may differ; the managed heap may be smaller or less fragmented. Therefore, it’s important to be careful in designing your tests.

Start by considering the inputs to your code and how the code reacts when you change its inputs.

  • Como reage a dados altamente coerentes localizados em série na memória?
  • Como lida com dados incoerentes em cache?
  • Quanto código você removeu do loop em que seu código é executado? Você alterou o uso do cache de instruções do processador?
  • Em que hardware está sendo executado? Até que ponto esse hardware implementa a previsão de ramificações? Até que ponto executa micro-operações fora de serviço? Tem suporte a SIMD?
  • Se você tem um sistema altamente multi-segmentado e está executando em um sistema com mais núcleos em vez de menos núcleos, como reage o sistema?
  • Quais são os parâmetros de escala do seu código? Escala linearmente à medida que aumenta seu conjunto de entradas, ou mais que linearmente?

Efetivamente, você deve pensar exatamente o que você está medindo com o seu equipamento de teste.

Como exemplo, considere o seguinte teste de uma operação simples: compare duas strings.

Quando as APIs de C# comparam duas strings, elas realizam conversões locais para garantir que caracteres diferentes podem corresponder a caracteres diferentes quando são provenientes de culturas diferentes e notará que isso é muito lento.

A maioria das APIs de string em C# é culturalmente sensível, mas uma não é: String.Equals .

Se você abrir String.CS do GitHub e olhar para String.Equals , você verá: uma função muito simples que executa algumas verificações antes de assumir o controle de uma função chamada EqualsHelper , uma função privada que você não pode recuperar diretamente sem reflexão.

lp MonoString CS1

EqualsHelper is a simple method. It walks through the strings 4 bytes at a time, comparing the raw bytes of the input strings. If it finds a mismatch, it stops and returns "false".

But there are other ways to check for string equality. The most innocuous-looking is an overload of String.Equals which accepts two parameters: the string to compare against, and an Enum called StringComparison.

Já determinamos que a sobrecarga de parâmetro único de String.Equals faz pouco antes de passar o controle para EqualsHelper . O que a sobrecarga de dois parâmetros faz?

Se você observar o código de sobrecarga de dois parâmetros, executa algumas verificações adicionais antes de inserir uma grande instrução switch . Esta instrução testa o valor da enumeração StringComparison . Como procuramos paridade com a sobrecarga de parâmetro único, queremos uma comparação ordinal - uma comparação byte a byte. Nesse caso, o controle passa por 4 verificações antes de chegar ao caso StringComparison.Ordinal , onde o código é muito semelhante à sobrecarga String.Equals de parâmetro único. Isso significa que, se você usar a sobrecarga de dois parâmetros de String.Equals em vez da sobrecarga de parâmetro único, o processador executa algumas operações de comparação adicionais. Pode ser mais lento, mas vale a pena tentar.

Você não quer deter-se a testar apenas String.Equals quando estiver interessado em todas as maneiras de comparar strings por igualdade. Há uma sobrecarga de String.Compare que pode realizar comparações ordinais, bem como um método chamado String.CompareOrdinal que possui duas sobrecargas diferentes.

Como uma implementação de referência, aqui está um exemplo simples codificado à mão. É apenas uma pequena função com uma verificação de comprimento que percorre cada caractere nas duas strings de entrada e as verifica.

Depois de verificar o código, existem quatro casos de teste diferentes que parecem úteis imediatamente:

  • Duas strings idênticas, para testar o desempenho do pior caso.
  • Duas strings com caracteres aleatórios, mas de comprimento igual, para evitar verificações de comprimento.
  • Duas strings de caracteres aleatórios e comprimento igual com o mesmo primeiro caractere para evitar uma otimização interessante encontrada apenas em String.CompareOrdinal .
  • Duas strings com caracteres aleatórios e comprimentos diferentes para testar o melhor desempenho.

Depois de alguns testes, String.Equals é o vencedor claro. Isso se aplica independentemente da plataforma, da versão de tempo de execução do script e do uso de Mono ou IL2CPP.

Vale a pena notar que String.Equals é o método usado pelo operador de igualdade de string, ==, então não deixe escapar e mude a == b para a.Equals (b) todo o seu código!

Ao comparar os resultados, parece estranho constatar até que ponto a implementação codificada manualmente é pior. Ao revisar o código IL2CPP, vemos que Unity injeta verificações de limites de array e verificações null no caso de compilação cruzada do código.

Essas verificações podem ser desativadas. Na sua pasta de instalação Unity , localize a subpasta IL2CPP . Esta subpasta contém o arquivo IL2CPPSetOptionAttributes.cs . Arraste-o para o seu projeto e você terá acesso ao Il2CppSetOptionAttribute ..

Você pode decorar tipos e métodos com este atributo. Você pode configurá-lo para desabilitar verificações automáticas de null, verificações automáticas de limites de array ou ambos. Isso pode acelerar o código - às vezes substancialmente. Neste caso de teste específico, o método de comparação de string codificado manualmente foi 20% mais rápido!

lp null check tip

Transforms

Você não saberia apenas observando a Hierarquia no Unity Editor, mas o componente Transform mudou muito entre o Unity 5 e o Unity 2018. Essas alterações também apresentam algumas novas e interessantes possibilidades de melhoria de desempenho.

De volta ao Unity 4 e ao Unity 5.0, um objeto seria atribuído em algum lugar na pilha de memória Unity nativa ao criar um Transform. Esse objeto poderia estar em qualquer lugar na pilha de memória nativa - não havia garantia de que dois Transforms sequencialmente atribuídos seriam atribuídos lado a lado, ou que um Transform filho seria atribuído perto de seu pai.

Como resultado, uma iteração linear através de uma Hierarquia Transform não iterou linearmente através de um repositório de memória contíguo. O processador parou repetidamente porque tinha que esperar por dados Transform recuperados do cache L2 ou da memória principal.

No back-end de Unity, toda vez que a posição, a rotação ou a escala de um Transform era alterada, esse Transform enviava uma mensagem OnTransformChanged . Essa mensagem tinha que ser recebida por todos os Transforms filhos, para que pudessem atualizar seus próprios dados e notificar qualquer outro componente interessado nessas alterações. Por exemplo, um Transform filho com um Collider deve atualizar o sistema de Física sempre que o Transform filho ou o Transform pai for alterado.

Essa mensagem inevitável causou muitos problemas de desempenho, especialmente porque não havia um modo integrado de evitar mensagens falsas. Se você fosse alterar um Transform, sabendo que também alteraria seus filhos, não havia maneira de evitar que Unity enviasse uma mensagem OnTransformChanged após cada alteração. Isso desperdiçou muito tempo de CPU.

Devido a esse detalhe, um dos conselhos mais comuns em versões mais antigas do Unity é agrupar as alterações em Transforms. Ou seja, capturar uma posição e rotação de Transform uma vez, no início de um frame, e usar e atualizar esses valores em cache ao longo do frame. Aplicar as alterações na posição e rotação apenas uma vez, no final do frame. Este é um bom conselho, até ao Unity 2017.2.

Felizmente, a partir do Unity 2017.4 e 2018.1, o OnTransformChanged está inativo. Um novo sistema, TransformChangeDispatch , substituiu-o.

TransformChangeDispatch foi apresentado pela primeira vez no Unity 5.4. Nesta versão, os Transforms não eram mais objetos solitários que poderiam estar localizados em qualquer parte da pilha de memória nativa de Unity. Em vez disso, cada Transform raiz em uma cena seria representado por um buffer de dados contíguo. Esse buffer, chamado de estrutura TransformHierarchy , contém todos os dados de todas os Transforms abaixo de uma Transform raiz.

Além disso, um TransformHierarchy também armazena metadados sobre cada Transform que contém. Esses metadados incluem uma máscara de bits que indica se um determinado Transform está "sujo" - se sua posição, rotação ou escala foi alterada desde a última vez em que o Transform foi identificado como "limpo". Também inclui uma máscara de bits para rastrear qual dos outros sistemas Unity está interessado em alterações em um Transform específico.

Com esses dados, Unity agora pode criar uma lista de Transforms sujos para qualquer outro sistema interno. Por exemplo, o sistema de Física pode consultar o TransformChangeDispatch para obter uma lista de Transforms cujos dados foram alterados desde a última FixedUpdate do sistema de Física.

No entanto, para compilar essa lista de Transforms alterados, o sistema TransformChangeDispatch não deve iterar em todos os Transforms em uma cena. Isso se tornaria muito lento se uma cena incluísse muitos Transforms - especialmente porque, na maioria dos casos, muito poucos Transforms teriam mudado.

Para corrigir isso, o TransformChangeDispatch rastreia uma lista de estruturas TransformHierarchy sujas. Sempre que um Transform é alterado, marca-se a si próprio como sujo, marca seus filhos como sujos e, em seguida, registra a TransformHierarchy na qual é armazenado com o sistema TransformChangeDispatch. Quando outro sistema no Unity solicita uma lista de Transforms alterados, o TransformChangeDispatch itera sobre qualquer Transform armazenado nas respetivas estruturas TransformHierarchy sujas. Os Transforms com o conjunto adequado de bits sujos e de interesse são adicionadas a uma lista, que é então enviada para o sistema que fez a solicitação.

Devido a essa arquitetura, quanto mais você dividir sua hierarquia, melhor será a capacidade de controlar as alterações em um nível granular. Quanto mais Transforms existirem na raiz de uma cena, menos Transforms teremos que examinar ao procurar alterações.

No entanto, há outra implicação. O TransformChangeDispatch usa o sistema multithreading interno de Unity para dividir os processos de trabalho para examinar as estruturas de TransformHierarchy. Essa divisão e mesclagem de resultados resulta em uma pequena sobrecarga toda vez que um sistema precisa consultar uma lista de alterações de TransformChangeDispatch.

A maioria dos sistemas internos do Unity solicita atualizações uma vez por frame, imediatamente antes de serem executadas. Por exemplo, o sistema Animation solicita atualizações imediatamente antes de avaliar todos os animadores ativos em sua cena. Da mesma forma, o sistema de renderização solicita atualizações para todos os Renderers ativos em sua cena antes de começar a selecionar a lista de objetos visíveis.

Um sistema se comporta de maneira um pouco diferente: Física.

No Unity 2017.1 (e versões mais antigas), as atualizações de Física estavam em sincronia. Se uma transformação era movida ou girada com um Collider conectado, a cena física era imediatamente atualizada. Isso garantia que a mudança de posição ou rotação do Collider fosse representada no mundo da Física e que Raycasts e outras consultas de Física fossem precisas.

No Unity 2017.2, quando migramos o sistema de Física para usar o TransformChangeDispatch, essa foi uma alteração necessária, mas também causou problemas. Cada vez que um Raycast era executado, o TransformChangeDispatch precisava ser consultado para obter uma lista de Transforms alterados que teriam que ser aplicados ao mundo da Física. Isso pode consumir muito tempo, dependendo do tamanho das Hierarquias de Transform e da maneira como o código chama as APIs de Física.

Esse comportamento é regido por uma nova configuração, Physics.autoSyncTransforms . Do Unity 2017.2 até ao Unity 2018.2, essa configuração é padronizada como "true", e Unity sincroniza automaticamente o mundo da Física para atualizações Transform sempre que você usar uma API de consulta de Física como Raycast ou Spherecast .

Essa configuração pode ser alterada, seja em suas configurações de Física no editor Unity ou no tempo de execução, definindo a propriedade Physics.autoSyncTransforms . Se você definir "false" e desabilitar a sincronização automática de Física, o sistema de Física consultará o sistema TransformChangeDispatch apenas para alterações em um horário específico: imediatamente antes de executar FixedUpdate .

Se houver problemas de desempenho ao usar APIs de consulta Física, há duas outras maneiras de lidar com elas.

Primeiro, você pode definir Physics.autoSyncTransforms como "false". Isso eliminará os picos gerados pelas atualizações das cenas de Física e TransformChangeDispatch das consultas de Física.

No entanto, se você fizer isso, as alterações nos Colliders não serão sincronizadas com a cena Física até a próxima FixedUpdate . Isso significa que, se você desabilitar autoSyncTransforms, mover um Collider e chamar Raycast com um Ray direcionado à nova posição do Collider, o Raycast pode não acertar o Collider - isso ocorre porque o Raycast está trabalhando com a última versão atualizada da cena de Física, a qual ainda não foi atualizada com a nova posição do Collider.

Isso pode resultar em bugs estranhos, e você deve testar cuidadosamente o seu jogo para ter certeza que a desativação da sincronização automática do Transform não cause problemas. Se você precisar forçar uma atualização física com alterações do Transform, você pode usar Physics.SyncTransforms . Esta API é lenta, então é melhor não usar várias vezes por frame!

Observe que, a partir do Unity 2018.3, Physics.autoSyncTransforms será padronizado como "false".

A segunda maneira de otimizar o tempo gasto na consulta do TransformChangeDispatch é reorganizar a ordem em que você está consultando e atualizando a cena Física para torná-la mais amigável ao novo sistema.

Lembre-se: com o Physics.autoSyncTransforms definido como "true", todas as consultas de Física verificarão as alterações com TransformChangeDispatch. No entanto, se TransformChangeDispatch não tiver nenhuma estrutura TransformHierarchy suja para verificar e o sistema de Física não tiver Transforms atualizados para aplicar à cena Física, não haverá quase nenhuma sobrecarga adicionada à consulta Física.

Você poderia realizar todas as suas consultas de Física em um lote e, em seguida, aplicar todas as alterações aos Transforms em um lote também. Não misture alterações de Transform e APIs de consultas de Física.

Este exemplo ilustra a diferença:

lp example transforms

A diferença de desempenho entre esses dois exemplos é impressionante e se torna ainda mais dramática quando uma cena contém apenas pequenas hierarquias Transform.

lp example transforms results

O sistema de áudio

Internamente, Unity usa um sistema chamado FMOD para reproduzir AudioClips. O FMOD é executado em seus próprios threads, e eles são responsáveis pela descodificação e mixagem de áudio. No entanto, a reprodução de áudio não é totalmente independente. Há algum trabalho realizado no thread principal para cada fonte de áudio ativa em uma cena. Além disso, em plataformas com menos núcleos (como telefones celulares mais antigos), os threads de áudio do FMOD podem competir por núcleos de processador com os threads principais e de renderização Unity.

Em cada frame, o Unity faz um loop em todas as fontes de áudio ativas. Para cada uma delas, Unity calcula a distância entre a fonte de áudio e o ouvinte de áudio ativo e alguns outros parâmetros. Esses dados permitem calcular a atenuação de volume, o desvio doppler e outros efeitos que podem afetar as fontes de áudio individuais.

Um problema comum surge da caixa "Mute" em uma fonte de áudio. Você pode pensar que configurar "Mute" com "true" elimina qualquer cálculo relacionado à respetiva Fonte de Áudio - mas não é assim!

lp audio checkbox

Em vez disso, a configuração “Mute” simplesmente fixa o parâmetro Volume a zero, depois que todos os outros cálculos relacionados ao volume são realizados, incluindo a verificação da distância. Unity também enviará a Fonte de Áudio desativada para o FMOD, que a ignorará. O cálculo dos parâmetros da Fonte de Áudio e o envio de Fontes de Áudio para o FMOD aparecerão como AudiosSystem.Update no Unity Profiler.

Se você notar muito tempo alocado para esse marcador de Profiler, verifique se você tem muitas fontes de áudio ativas silenciadas. Se estiverem, considere desabilitar os componentes silenciados da Fonte de Áudio em vez de silenciá-los ou desabilitar o GameObject. Você também pode usar AudioSource.Stop , que interromperá a reprodução.

Outra coisa que você pode fazer é fixar a contagem de voz nas configurações de áudio do Unity. Para fazer isso, você pode usar AudioSettings.GetConfiguration , que retorna uma estrutura contendo dois valores de interesse: a contagem de voz virtual e a contagem de voz real.

Reduzir o número de Vozes Virtuais reduzirá o número de Fontes de Áudio que o FMOD examinará ao determinar quais serão realmente reproduzidas. Reduzir a contagem de Vozes Reais reduzirá o número de fontes de áudio que o FMOD mistura para produzir o áudio do seu jogo.

Para alterar o número de Vozes Virtuais ou Reais que usa FMOD, você deve alterar os valores apropriados na estrutura AudioConfiguration retornada por AudioSettings.GetConfiguration e redefinir o sistema de Áudio com o nova configuração passando a estrutura AudioConfiguration como parâmetro para AudioSettings.Reset . A reprodução de áudio será interrompida, por isso é recomendável fazer isso quando os jogadores não percebam a alteração, como durante o carregamento de uma tela ou no momento da inicialização.

Animações

Existem dois sistemas diferentes que podem ser usados para reproduzir animações no Unity: o sistema Animator e o sistema de Animação.

Por "Sistema Animator" entende-se o sistema que envolve o componente Animator, que é anexado ao GameObjects para animar os mesmos, e o recurso AnimatorController, que é referenciado por um ou mais animadores. Este sistema foi historicamente chamado Mecanim, e é muito rico em recursos.

Em um Controlador de Animador, você define estados. Esses estados podem ser um clipe de animação ou Blend Tree. Os estados podem ser organizados em camadas. Cada frame avalia o estado ativo em cada camada e os resultados de cada camada são misturados e aplicados ao modelo animado. Ao fazer a transição entre dois estados, ambos os estados são avaliados.

O outro sistema é o "sistema de animação". É representado pelo componente Animação e é muito simples. Em cada frame, cada componente de Animação ativo percorre linearmente todas as curvas de seu clipe de animação associado, avalia essas curvas e aplica os resultados.

A diferença entre esses dois sistemas não é apenas funções, mas também os detalhes subjacentes da implementação.

O sistema Animator é altamente multithreaded. Seu desempenho muda drasticamente em diferentes CPUs com diferentes números de núcleos. Em geral, ele é dimensionado de maneira menos linear que o número de curvas em seus Clipes de Animação. Portanto, ele funciona muito bem ao avaliar animações complexas com um grande número de curvas. No entanto, tem um custo indireto bastante alto.

O sistema de Animação, sendo simples, quase não tem sobrecarga. Seu desempenho é dimensionado linearmente com o número de curvas nos Clipes de Animação que estão sendo reproduzidos.

Essa diferença é mais impressionante quando os dois sistemas são comparados ao reproduzir Clipes de Animação idênticos.

lp animation clips test

Ao reproduzir os Clipes de Animação, tente escolher o sistema que melhor se adapte à complexidade do seu conteúdo e ao hardware em que o jogo será executado.

Outro problema comum é o uso excessivo de camadas em Controladores de Animação. Quando um Animator é executado, ele avalia cada frame em todas as camadas do seu Controlador de Animação. Isso inclui camadas cujo peso é definido como zero, o que significa que não contribui de forma visível para os resultados finais da animação.

Cada Camada adicional adicionará computação adicional a cada Controlador de Animação para cada frame. Portanto, tente usar camadas com moderação. Se você precisar depurar camadas de demonstração ou cinemáticas em um Controlador de Animação, tente refatorá-las. Mesclar-las em camadas existentes, ou eliminá-las antes de publicar seu jogo.

Plataforma humanóide vs genérica

Por padrão, Unity importa modelos animados com a plataforma Generica, mas é comum que os desenvolvedores mudem para a plataforma Humanóide quando estão animando um personagem. No entanto, isso não é completamente fácil.

A plataforma Humanoid adiciona duas funções adicionais ao Sistema Animator: cinemática inversa e retargeting de animação. O retargeting de animação é ótimo, permitindo que você reutilize animações em diferentes avatares.

No entanto, mesmo que você não esteja usando o IK ou o Animation Retargeting, o Animator de um personagem manipulado por um humanóide ainda computará dados de IK e Retargeting em cada frame. Isso consome cerca de 30 a 50% mais tempo de CPU do que a plataforma genérica, que não faz esses cálculos.

Se você não estiver usando as funções da plataforma Humanóide, você deve usar a plataforma Genérica.

Animator Pooling

Object Pooling é uma estratégia chave para evitar picos de desempenho durante o jogo. No entanto, os animadores têm sido historicamente difíceis de usar com o Object Pooling. Sempre que um GameObject do Animator é ativado, ele deve recriar buffers de dados intermediários para serem usados ao avaliar o Animator Controller do Animator. Isso é chamado de Animator Rebind e aparece no Perfil Unity como Animator.Rebind .

Antes do Unity 2018, a única solução alternativa era desativar o Componente Animator, não o GameObject. Isso teve efeitos colaterais: se você tivesse algum MonoBehaviors, Mesh Colliders ou Mesh Renderers em seu personagem, também iria desativá-los. Dessa forma, você poderia economizar todo o tempo de CPU que estava sendo usado pelo seu personagem. Mas isso adiciona complexidade ao seu código e é suscetível de erros.

Com o Unity 2018.1, introduzimos a API Animator.KeepControllerStateOnEnable . Essa propriedade é padronizada como "false", significando que o Animator se comportará como sempre - deslocalizando seus buffers de dados intermediários quando está desabilitado e relocalizando-os quando habilitado.

No entanto, se você definir essa propriedade como "true", os Animators manterão seus buffers enquanto estiverem desativados. Isso significa que não haverá Animator.Rebind quando esse Animator for reativado. Animators podem finalmente agrupar-se!

Mais recursos

Queremos saber! Você gostou deste conteúdo?

Sim, continue. Bem. Poderia ser melhor
Eu entendi

Usamos cookies para garantir a melhor experiência no nosso site. Clique aqui para obter mais informações.