top of page
u2373996638_information_overload_represented_using_only_circles_916add5a-f0e7-4a75-9043-9f

Trixter Hub

Otimização de Jogos com Unity 3D

por André "Intentor" Martins - 18/03/2015

Captura de tela 2025-03-09 140225.png
De 600 a 60 draw calls em uma jornada épica!

Se criar games já é algo difícil, imagine otimizá-los para rodar nos mais diversos dispositivos, nas mais hostis condições de recursos (memória e processamento) que se pode (ou não) imaginar?

Enquanto artistas buscam beleza (e muitos polígonos e efeitos e outras coisas pesadas), programadores buscam velocidade. Para cada nova arte que consome alguns suados frames do jogo, lá estão os programadores procurando maneiras de trazê-los de volta. Porém otimização não é apenas uma tarefa de programação, sendo muito mais uma combinação de todas as partes em prol de uma tarefa comum. Artistas têm de entender sua participação no processo, bem como os tais programadores e também os responsáveis por game design. De nada adianta uma arte leve com um código performático se o design de fases lança trocentos inimigos diante do jogador sem se preocupar com o quanto isso pode afetar o funcionamento do game.

Meu maior interesse por otimização em jogos – embora isso já tenha feito parte de meus dias desde que comecei a programar – surgiu quando um game 3D que estávamos desenvolvendo rodava a praticamente 0 FPS em um dos dispositivos alvo (um tablet genérico single core de 1GHz com 512 MB RAM). O jogo em si, apesar do grande cenário e da razoável quantidade de elementos, não era necessariamente pesado no desktop e rodava muito bem em máquinas modestas. Mas aí estava o catch da coisa: um PC não é um tablet. Então, após algum período de muitas dores e inúmeras pesquisas, eis o que o jogo passou a rodar a 30 FPS com tranquilidade no tal tablet com configurações modestas.

Este artigo não pretende ser um primer no assunto, mas apenas um guia em direção a uma melhor otimização de jogos (principalmente mobile) com Unity 3D. De maneira geral, alguns conselhos valem para qualquer engine, sendo que outros são mais específicos para a popularíssima Unity.

O artigo está dividido em quatro partes, cada qual focada em uma das principais disciplinas envolvidas em games, trazendo alguns conselhos práticos que aprendi ao longo dos anos criando jogos. 

Programação
  1. Use o profiler. O profiler é a maior ferramenta do programador para descobrir aonde estão os gargalos do projeto. Seja no código, na arte ou em algum pequeno detalhe de renderização, é com o profiler que você irá descobrir aonde o problema se encontra – ou, ainda que não descubra diretamente, poderá entender o que está indiretamente sendo afetado e daí tirar suas conclusões.

  2. Continue usando o profiler. De fato, o profiler deve ser utilizado desde o início do projeto e nunca mais deixar de ser usado – não espere o problema surgir para depois fazer profiling. Encontre-os antes que eles aconteçam!

  3. Use o Update() apenas se isso fizer sentido. Existem muitas atividades do jogo nas quais uma corotina se encaixa muito melhor do que checagem frame a frame.

  4. Desabilite scripts em objetos que não precisam deles no momento. Um objeto escondido no cenário não necessariamente precisa ter todos aqueles scripts de animação rodando no momento.

  5. Cuidado com o uso de GameObject.Find() (e similares). Pesquisar por GameObjects pode ser custoso em uma hierarquia complexa. Sempre que possível mantenha referência aos seus objetos, seja a partir da pesquisa deles durante Start() ou a partir de variáveis públicas populadas via Inspector.

  6. Cuidado com o uso de GetComponent(). GetComponent() é uma lindeza só, porém pode gerar overhead se usado sem sabedoria. Vale a mesma dica de GameObject.Find(): sempre que possível mantenha uma referência aos componentes necessários.

  7. Use tags. Uma boa (e mais rápida) maneira de localizar objetos é usando tags a partir de GameObject.FindWithTag(). Todavia, valem os mesmos cuidados dos dois itens acima.

  8. Ao comparar tags, prefira o método CompareTag(). Sempre que comparar tags em GameObjects, utilize obj.CompareTag() ao invés de obj.tag == “tag”. Isso irá reduzir alocações desnecessárias para a comparação.

  9. .transform, .gameObject e outros geram chamadas a GetComponent(). Boa parte das propriedades de MonoBehaviour e seus derivados fazem chamadas a GetComponent(). Sempre que possível, como nos casos acima, mantenha uma referência a esses componentes ao invés de chamar suas contrapartes em propriedades.

  10. Evite LINQ (C#) durante gameplay. LINQ (Language-Integrated Query) tem uma sintaxe sexy que faz com qualquer código fique mais bonito. Todavia, sua performance não é tão bela quanto seu léxico faz-nos crer, causando costumeiramente alocações de memória que podem prejudicar o gameplay.

  11. Use string.Concat ou StringBuilder ao invés de concatenação com +. Strings são imutáveis e toda vez que uma concatenação com + ocorre uma nova string é gerada. Utilize string.Concat ou StringBuilder se precisar concatenar uma grande quantidade de strings.

  12. Na criação de shaders, evite operações matemáticas complexas. É tentador em muitos momentos, mas potências, senos, cossenoss e outras operações matemágicas complexas apenas tornarão seu shader mais pesado.

  13. Use pooling. Pooling é o ato de reutilização de objetos, reduzindo a necessidade de criação/destruição e por consequência I/O e chamadas ao Garbage Collector. A Unity provê nativamente uma robusta solução de pooling.

  14. Use Rigid Bodies apenas se necessário. Se seu objeto não irá ter nenhuma simulação física, não há porque haver um Rigid Body anexado a ele. Se o objeto é apenas usado para gerar triggers, habilite IsKinematic, desabilitando assim as simulações físicas no objeto.

  15. Tente manter o número de draw calls abaixo de 100. Embora isso irá requerer uma boa ajuda dos artistas, um bom número de draw calls em mobile gira em torno de 100. 

Arte
  1. 1 material por objeto. Sempre que possível, tenha 1 material por objeto. De maneira geral, cada material gera 1 draw call. Logo, quanto mais materiais, maior será a quantidade de chamadas de renderização.

  2. Compartilhe materiais sempre que possível. Como extensão do item acima, sempre que possível use um mesmo material para mais de um objeto. Uma boa forma de obter isso é usando atlas de texturas.

  3. Use Texture Atlases. Como extensão do item acima, busque sempre utilizar atlases para minimizar a quantidade de texturas separadas, principalmente em UI.

  4. Tamanho de texturas sempre em potências de 2. GPUs amam texturas com tamanhos que sejam potências de 2. Logo 32x32, 512x512 e 2048x2048 são resoluções que faram qualquer placa gráfica feliz.

  5. Mantenha o número de vértices por frame abaixo de 200 mil. Se você pode simplificar a geometria de um objeto, FAÇA-O!

  6. Para personagens, matenha o número de triângulos entre 1500-2000. Obviamente este valor pode variar muito dependendo de cada projeto, porém é um bom ponto de partida.

  7. Habilite compressão de malha sempre que possível. Nas configurações de importação de um objeto, é possível habilitar compressão de malha. Sempre que possível, busque habilitar tal compressão. É importante ressaltar que a compressão pode fazer com que o objeto perca detalhes e as UVs se comportem de maneira de estranha. Teste sempre!

  8. Static batching para objetos estáticos. Se seu objeto é estático, marque-o como static no Inspector. Isso permitirá à Unity fazermos otimizações durante as renderizações e reduzir o número de draw calls. Para entender melhor batching, vale a leitura sobre Draw Call Batching, o qual explica também como obter o dynamic batching, o qual ocorre por padrão caso certas condições sejam atingidas.

  9. Compartilhar animações sempre que possível. Com o uso de Animation Controllers, ficou muita mais fácil compartilhar animações entre diferentes objetos com riggings similares.

  10. Reduza o número de partículas. Mantenha o número de partículas dentro de um sistema sempre no mínimo necessário. Não há porque ter 1000 partículas sendo criadas quanto apenas 100 já trariam um ótimo detalhamento visual.

  11. Use shaders otimizados para mobile. De maneira geral, quanto mais simples o shader mais rápido este será.

  12. Modere o uso de post processing. Infelizmente post processing de maneira geral é pesado em mobile. Se for utilizá-lo, teste bastante!

  13. Evite sombras em tempo real. Sombras em tempo real são muito custosas. Utilize lightmaps, light probes ou sombras pintadas diretamente na textura.

  14. Use skyboxes para objetos muito distantes. Ao invés de criar toda uma cadeia de montanhas pela qual o jogador nunca irá passar, utilize um skybox para deixá-la como pano de fundo do cenário.

  15. Use LOD para simplificar geometrias distantes. LOD (Level of Detail) ajuda a poupar vértices permitindo que objetos tenham sua máxima qualidade apenas quando perto do jogador.

  16. Use Occlusion Culling para desabilitar renderização de objetos não visíveis pelo jogador. Occlusing Culling pode poupar muitos draw calls. De maneira geral, se um objeto não está visível pelo jogador, não há porque renderizá-lo.

 

Áudio
  1. OGG/MP3 para músicas, WAV para efeitos sonoros. De maneira geral, o ideal é sempre utilizar formatos sem compressão (WAV ou AIFF) para todas as situações, uma vez que a unity irá comprimir os áudios durante a compilação. Porém, até para simplificar aquele update do seu repositório de versão predileto, prefira OGG para músicas e WAV para efeitos sonoros.

  2. Use um Audio Manager. Ter um componente responsável por tocar músicas e efeitos sonoros, controlar volume e gerir a habilitação de áudio é uma boa forma de centralizar a utilização dos objetos de áudio.

  3. Use Audio Pooling. Assim como pooling de objetos, sempre que possível utilize pooling para áudio. Um bom asset (pago) que permite uma excelente gestão de áudio e também de pooling é o Master Audio.

 

Game Design
  1. Atente para a quantidade de elementos em tela. Querer colocar 500 inimigos em um gigantesco campo aberto repleto de estruturas pode não ser uma ideia muito performática.

  2. Utilize o level design como aliado na exibição de elementos mais pesados. Como extensão do item acima, se o artista criou todo um complexo conjunto de props com mais tris do que qualquer programador jamais poderia desejar em uma cena, utilize o level design para assegurar que poucas coisas estejam em tela no momento em que tal conjunto esteja em exibição.

bottom of page