Veja como otimizar o desempenho da interface do usuário no Unity usando este guia detalhado com vários experimentos, conselhos práticos e testes de desempenho para comprovar!
Olá! Sou Sergey Begichev, Desenvolvedor de Clientes na Pixonic (MY.GAMES). Nesta publicação, discutirei a otimização de UI no Unity3D. Embora renderizar um conjunto de texturas possa parecer simples, isso pode levar a problemas significativos de desempenho. Por exemplo, em nosso projeto War Robots, versões de UI não otimizadas foram responsáveis por até 30% da carga total da CPU — um número surpreendente!
Normalmente, esse problema surge sob duas condições: uma, quando há vários objetos dinâmicos, e duas, quando os designers criam layouts que priorizam o dimensionamento confiável em diferentes resoluções. Até mesmo uma pequena UI pode gerar uma carga perceptível sob essas circunstâncias. Vamos explorar como isso funciona, identificar as causas da carga e discutir possíveis soluções.
Primeiro, vamos rever
Embora os pontos 2 e 3 sejam intuitivamente claros, o restante das recomendações pode ser problemático de imaginar na prática. Por exemplo, o conselho de “dividir suas telas em subtelas” é certamente valioso, mas a Unity não fornece diretrizes claras sobre os princípios por trás dessa divisão. Falando por mim, em termos práticos, quero saber onde faz mais sentido implementar subtelas.
Considere o conselho para “evitar grupos de layout”. Embora possam contribuir para uma alta carga de UI, muitas UIs grandes vêm com vários grupos de layout, e retrabalhar tudo pode ser demorado. Além disso, designers de layout que evitam grupos de layout podem acabar gastando significativamente mais tempo em suas tarefas. Portanto, seria útil entender quando tais grupos devem ser evitados, quando podem ser benéficos e quais ações tomar se não pudermos eliminá-los.
Essa ambiguidade nas recomendações da Unity é uma questão central — muitas vezes não fica claro quais princípios devemos aplicar a essas sugestões.
Para otimizar o desempenho da UI, é essencial entender como o Unity constrói a UI. Entender esses estágios é crucial para uma otimização efetiva da UI no Unity. Podemos identificar amplamente três estágios principais nesse processo:
Layout . Inicialmente, o Unity organiza todos os elementos da UI com base em seus tamanhos e posições designadas. Essas posições são calculadas em relação às bordas da tela e outros elementos, formando uma cadeia de dependências.
Loteamento . Em seguida, o Unity agrupa elementos individuais em lotes para uma renderização mais eficiente. Desenhar um elemento grande é sempre mais eficiente do que renderizar vários menores. (Para um mergulho mais profundo no loteamento, consulte
Renderização . Finalmente, o Unity desenha os lotes coletados. Quanto menos lotes houver, mais rápido será o processo de renderização.
Embora existam outros elementos envolvidos no processo, essas três etapas respondem pela maioria dos problemas, então, por enquanto, vamos nos concentrar nelas.
O ideal é que, quando nossa interface de usuário permanece estática — ou seja, nada se move ou muda —, podemos criar o layout uma vez, criar um único lote grande e renderizá-lo com eficiência.
No entanto, se modificarmos a posição de um único elemento, precisamos recalcular sua posição e reconstruir o lote afetado. Se outros elementos dependerem dessa posição, precisaremos recalcular suas posições também, causando um efeito cascata por toda a hierarquia. E quanto mais elementos precisarem de ajuste, maior será a carga de loteamento.
Então, mudanças em um layout podem desencadear um efeito cascata por toda a UI, e nosso objetivo é minimizar o número de mudanças. (Alternativamente, podemos tentar isolar as mudanças para evitar uma reação em cadeia.)
Como um exemplo prático, esse problema é particularmente pronunciado ao usar grupos de layout. Cada vez que um layout é reconstruído, cada LayoutElement executa uma operação GetComponent, que pode ser bastante intensiva em recursos.
Vamos examinar uma série de exemplos para comparar os resultados de desempenho. (Todos os testes foram conduzidos usando o Unity versão 2022.3.24f1 em um dispositivo Google Pixel 1.)
Neste teste, criaremos um grupo de layout com um único elemento e analisaremos dois cenários: um em que alteramos o tamanho do elemento e outro em que utilizamos a propriedade FillAmount.
Alterações em RectTransform:
Alterações em FlllAmount:
No segundo exemplo, tentaremos fazer a mesma coisa, mas em um grupo de layout com 8 elementos. Neste caso, ainda mudaremos apenas um elemento.
Alterações em RectTransform:
Alterações em FlllAmount:
Se, no exemplo anterior, as alterações no RectTransform resultaram em uma carga de 0,2 ms no layout, desta vez a carga aumenta para 0,7 ms. Da mesma forma, a carga de atualizações em lote aumenta de 0,65 ms para 1,10 ms.
Embora ainda estejamos modificando apenas um elemento, o aumento do tamanho do layout impacta significativamente a carga durante a reconstrução.
Em contraste, quando ajustamos o FillAmount de um elemento, não observamos aumento na carga, mesmo com um número maior de elementos. Isso ocorre porque modificar o FillAmount não aciona uma reconstrução de layout, resultando em apenas um pequeno aumento na carga de atualização em lote.
Claramente, usar FillAmount é a escolha mais eficiente neste cenário. No entanto, a situação se torna mais complexa quando alteramos a escala ou a posição de um elemento. Nesses casos, é desafiador substituir os mecanismos internos do Unity que não acionam a reconstrução do layout.
É aqui que SubCanvases entram em cena. Vamos examinar os resultados quando encapsulamos um elemento mutável dentro de um SubCanvas.
Criaremos um grupo de layout com 8 elementos, um dos quais será alojado em um SubCanvas, e então modificaremos sua transformação.
Alterações de RectTransform em SubCanvas:
Como os resultados indicam, encapsular um único elemento dentro de um SubCanvas quase elimina a carga no layout; isso ocorre porque o SubCanvas isola todas as alterações, impedindo uma reconstrução nos níveis mais altos da hierarquia.
No entanto, é importante notar que mudanças dentro da tela não influenciarão o posicionamento de elementos fora dela. Portanto, se expandirmos muito os elementos, existe o risco de que eles se sobreponham aos elementos vizinhos.
Vamos prosseguir envolvendo 8 elementos de layout em um SubCanvas:
O exemplo anterior demonstra que, enquanto a carga no layout permanece baixa, a atualização em lote dobrou. Isso significa que, embora dividir elementos em vários SubCanvases ajude a reduzir a carga na construção do layout, isso aumenta a carga na montagem em lote. Consequentemente, isso pode nos levar a um efeito líquido negativo geral.
Agora, vamos conduzir outro experimento. Primeiro, criaremos um grupo de layout com 8 elementos e então modificaremos um dos elementos de layout usando o animador.
O animador ajustará o RectTransform para um novo valor:
Aqui, vemos o mesmo resultado do segundo exemplo, onde mudamos tudo manualmente. Isso é lógico porque não faz diferença o que usamos para mudar RectTransform.
O animador altera RectTransform para um valor semelhante:
Os animadores enfrentavam anteriormente um problema em que eles sobrescreviam continuamente o mesmo valor a cada quadro, mesmo que esse valor permanecesse inalterado. Isso inadvertidamente acionava uma reconstrução de layout. Felizmente, versões mais recentes do Unity resolveram esse problema, eliminando a necessidade de alternar para alternativas
Agora, vamos examinar como a alteração do valor do texto se comporta dentro de um grupo de layout com 8 elementos e se isso aciona uma reconstrução de layout:
Vemos que a reconstrução também é acionada.
Agora, vamos alterar o valor de TextMechPro no grupo de layout de 8 elementos:
O TextMechPro também aciona uma reconstrução de layout e parece até colocar mais carga no processamento em lote e na renderização do que o Text normal.
Alterando o valor TextMechPro no SubCanvas em um grupo de layout de 8 elementos:
O SubCanvas isolou efetivamente as alterações, impedindo a reconstrução do layout. No entanto, embora a carga nas atualizações em lote tenha diminuído, ela continua relativamente alta. Isso se torna uma preocupação ao trabalhar com texto, pois cada letra é tratada como uma textura separada. Modificar o texto consequentemente afeta várias texturas.
Agora, vamos avaliar a carga incorrida ao ativar e desativar um GameObject (GO) dentro do grupo de layout.
Ativando e desativando um GameObject dentro de um grupo de layout de 8 elementos:
Como podemos ver, ativar ou desativar um GO também aciona uma reconstrução de layout.
Ativando um GO dentro de um SubCanvas com um grupo de layout de 8 elementos:
Nesse caso, o SubCanvas também ajuda a aliviar a carga.
Agora, vamos verificar qual é a carga se ligarmos ou desligarmos todo o GO com um grupo de layout:
Como os resultados mostram, a carga atingiu seu nível mais alto até agora. Habilitar o elemento raiz aciona uma reconstrução de layout para os elementos filhos, o que, por sua vez, resulta em uma carga significativa tanto no lote quanto na renderização.
Então, o que podemos fazer se precisarmos habilitar ou desabilitar elementos inteiros da IU sem criar carga excessiva? Em vez de habilitar e desabilitar o GO em si, você pode simplesmente desabilitar o componente Canvas ou Canvas Group. Além disso, definir o canal alfa do Canvas Group como 0 pode atingir o mesmo efeito, evitando problemas de desempenho.
Aqui está o que acontece com a carga quando desabilitamos o componente Canvas Group. Como o GO permanece habilitado enquanto a tela é desabilitada, o layout é preservado, mas simplesmente não é exibido. Essa abordagem não só resulta em uma carga baixa de layout, mas também reduz significativamente a carga em lotes e renderização.
Em seguida, vamos examinar o impacto da alteração do SiblingIndex dentro do grupo de layout.
Alterando SiblingIndex dentro de um grupo de layout de 8 elementos:
Conforme observado, a carga continua significativa, em 0,7 ms para atualização do layout. Isso indica claramente que modificações no SiblingIndex também acionam uma reconstrução do layout.
Agora, vamos experimentar uma abordagem diferente. Em vez de alterar o SiblingIndex, trocaremos as texturas de dois elementos dentro do grupo de layout.
Trocando texturas de dois elementos em um grupo de layout de 8 elementos:
Como podemos ver, a situação não melhorou; na verdade, piorou. Substituir a textura também aciona uma reconstrução.
Agora, vamos criar um grupo de layout personalizado. Vamos construir 8 elementos e simplesmente trocar as posições de dois deles.
Grupo de layout personalizado com 8 elementos:
A carga realmente diminuiu significativamente — e isso é esperado. Neste exemplo, o script simplesmente troca as posições de dois elementos, eliminando operações pesadas de GetComponent e a necessidade de recalcular as posições de todos os elementos. Como resultado, há menos atualizações necessárias para o processamento em lote. Embora essa abordagem pareça uma bala de prata, é importante observar que executar cálculos em scripts também contribui para a carga geral.
À medida que introduzimos mais complexidade em nosso grupo de layout, a carga inevitavelmente aumentará, mas não necessariamente refletirá na seção Layout, pois os cálculos ocorrem em scripts. Então, é crucial monitorar a eficiência do código nós mesmos. No entanto, para grupos de layout simples, soluções personalizadas podem ser uma excelente opção.
Reconstruir o layout apresenta um desafio significativo. Para abordar esse problema, precisamos identificar suas causas raiz, que podem variar. Aqui estão os principais fatores que levam a reconstruções de layout:
É importante destacar alguns aspectos que não representam mais problemas nas versões mais recentes do Unity, mas que representavam nas versões anteriores: substituir o mesmo texto e definir repetidamente o mesmo valor com um animador.
Agora que identificamos os fatores que acionam uma reconstrução de layout, vamos resumir nossas opções de solução:
Encapsule um GameObject (GO) que acione uma reconstrução em um SubCanvas. Essa abordagem isola as alterações, impedindo que elas afetem outros elementos na hierarquia. No entanto, seja cauteloso — muitos SubCanvases podem aumentar significativamente a carga no lote.
Ligue e desligue o SubCanvas ou o Canvas Group em vez do GO. Use um pool de objetos em vez de criar novos GOs. Este método preserva o layout na memória, permitindo a ativação rápida de elementos sem a necessidade de uma reconstrução.
Utilize animações de shader. Alterar a textura usando um shader não acionará uma reconstrução de layout. No entanto, tenha em mente que as texturas podem se sobrepor a outros elementos. Este método efetivamente serve a um propósito semelhante ao uso de SubCanvases, mas requer escrever um shader.
Substitua o grupo de layout do Unity por um grupo de layout personalizado. Um dos principais problemas com os grupos de layout do Unity é que cada LayoutElement chama GetComponent durante a reconstrução, o que consome muitos recursos. Criar um grupo de layout personalizado pode resolver esse problema, mas tem seus próprios desafios. Os componentes personalizados podem ter requisitos operacionais específicos que você precisa entender para uso eficaz. No entanto, essa abordagem pode ser mais eficiente, especialmente para cenários de grupos de layout mais simples.