Introdução

Antes de começarmos, assumimos conhecimentos prévios em Unity e C# pois esse não é o foco deste artigo. O que vamos ver aqui são dicas rápidas e simples para garantir que o seu App ou Jogo feito em Unity irá ter o mínimo de otimizações para rodar em dispositivos móveis como Android e iOS. Por isso não espere tutorial avançado de como melhorar modelos 3D ou modificar o render pipeline para tirar o melhor proveito da GPU dos dispositivos, mas sim como um tutorial e cuidados básicos que devem ser tomados durante o desenvolvimento.

Android vs iOS

Vamos falar um pouco sobre Android e iOS. Ambas as plataformas possuem suas particularidades que serão citadas no decorrer desse artigo. Para todos os efeitos vamos assumir que estamos otimizando nosso app para as duas as plataformas, ou seja, quando não citarmos uma plataforma específica significa que tanto Android quanto iOS podem se beneficiar da otimização e que o procedimento é o mesmo.

É importante frisar que as necessidades de ambas as plataformas podem ser completamente diferentes: enquanto existem, literalmente, milhares de dispositivos Android com diferentes resoluções e poderes de processamento nós temos um punhado (comparativamente) de dispositivos iOS com especificações mais controladas e previsíveis. A consequência disso é que não podemos assumir que dispositivos Android possuam espaço em disco suficiente para rodar nossa aplicação e que, na média, também não possuem capacidade de processamento e memória disponíveis graças a extensiva quantidade de dispositivos mid/low-end disponíveis no mercado. Por mais que exista suporte para novas tecnologias como, por exemplo, Vulkan somente dispositivos com Android Nougat (2017) em diante podem se beneficiar desse tipo de otimização e, por isso, nem sempre é uma alternativa viável.

Em contrapartida, dispositivos iOS podem se beneficiar, por exemplo, das poucas resoluções disponíveis, ou do poder de processamento que será suportado. Por exemplo: podemos montar nossa aplicação pensando que irá rodar a partir do iPhone 6 em diante pois estes têm grandes chances de suportarem aplicações mais complexas.

Quer dizer que temos que aplicar TODAS as otimizações aqui disponíveis? Não necessariamente, é importante levar em conta os requisitos de seu projeto e em quais plataformas ele estará disponível.

Redução de buscas

Objetivos:

  • Reduzir consumo de memória em tempo de execução.
  • Reduzir overhead de acesso a objetos e scripts.

Cuidados necessários:

  • Algumas implementações podem causar perda de link entre objetos, o que dificulta a detecção de erros.

A Unity disponibiliza uma série de ferramentas que facilitam bastante a vida de quem quer procurar Game Objects ou scripts em tempo de execução, um exemplo disso são os seguintes métodos (retirados da documentação da Unity):

  • public static GameObject Find(string name);
  • public Component GetComponent(Type type);
  • public static Object FindObjectOfType(Type type);
  • public static GameObject[] FindGameObjectsWithTag(string tag);

Certamente você já deve ter utilizado, ou visto, em algum lugar de seu código. É prático, é fácil, reduz a dependência dos objetos e de quebra é possível obter Game Objects ou Scripts em tempo de execução. O problema é que todos eles (em diferentes proporções) podem ser bastante custosos se utilizados frequentemente, dentro de loops ou a cada frame (método Update, por exemplo):

Component _component1 = GetComponent<Component>();
GameObject _player = FindObjectOfType<Player>();
GameObject _gun = GameObject.FindGameObjectWithTag("Gun");

Por isso, recomendamos uma outra abordagem: em muitas situações é melhor se beneficiar do aspecto “Data-driven” da Unity e realizarmos o link dessas dependências através do inspector. Para isso basta declarar as seguintes variáveis no corpo de sua classe:

[SerializeField] Component _component1;
[SerializeField] GameObject _player;
[SerializeField] GameObject _gun;

Isso fará com que essas variáveis apareçam no Inspector da seguinte forma:

image1.png

Basta arrastar os objetos que deseja acessar para esses campos que o link estará feito.

Essa implementação não só garante a eliminação da busca como também evidencia as dependências do nosso script. É importante notar que essa abordagem não vem sem seus problemas: mudanças no script ou nos objetos correlacionados podem causar quebras de links que, muitas vezes, são difíceis de serem encontrados. Além disso, não é possível utilizar essa abordagem caso algum objeto seja criado e que precise ser acessado somente em tempo de execução. Uma outra alternativa é garantir que os métodos de busca sejam chamados única e exclusivamente na inicialização e salvos para usos futuros:

Component _component1;
GameObject _player;
GameObject _gun;
void Start ()
{
    _component1 = GetComponent<Component>();
    _player = FindObjectOfType<Player>();
    _gun = GameObject.FindGameObjectWithTag("Gun");
}

Depois de inicializados, o ideal é que esses objetos sejam repassados para todos aqueles que necessitam utiliza-los. Essa abordagem permite que objetos sejam acessados em tempo de execução, mas causa um overhead na inicialização dos scripts que, se possível, deve ser evitado

Compressão de Texturas

Objetivos:

  • Reduzir tamanho do App em disco.
  • Reduzir consumo de memória de GPU (Graphical Processing Unit).
  • Reduzir tempo de carregamento de texturas.

Cuidados necessários:

  • Pode causar perda de nitidez de imagens. Perceptível principalmente em dispositivos com resolução muito alta (IPad Retina, por exemplo).
  • É importante manter o tamanho das imagens como potência de 2 (16 pixels, 32, 64, 128 e assim por diante) pois é o pré-requisito para o funcionamento de alguns algoritmos de compressão.

Android

Para a maioria dos casos recomendamos a utilização da compressão RGB ETC 1 (Ericsson Texture Compression) para dispositivos antes de 2015 e ETC2 para dispositivos mais novos (com suporte para OpenGL ES 3.0). Ambas funcionam muito bem para casos em que as imagens não possuam canal Alpha (transparência) ou grande dependência em gradientes de cores por se tratar de um tipo de compressão “Lossy” (com perda de qualidade).

Existe uma técnica muito interessante que compensa a falta de suporte de canal alpha para a compressão ETC 1 chamada alpha splitted. Essa opção consiste em dividir a textura em duas imagens distintas onde, a primeira, trata-se da imagem sem qualquer transparência e todas as características principais (cores, formas, etc) enquanto a segunda textura contém apenas as formas e o canal alpha separados como no exemplo abaixo:

Pasted image at 2018_03_26 04_44 PM (1).png

Infelizmente esta técnica não funciona para imagens com gradiente de alpha e, para esses casos a melhor opção é RGBA ETC 2. Os cuidados que devem ser tomados ao adotar o ETC2 são: compressão de imagens muito detalhadas ou com muitos gradientes de cores pode gerar perda de nitidez e a questão desse tipo de compressão não ser suportada por devices mais antigos. As edições mais novas da Unity possuem um sistema de “fallback” para outras compressões caso o device não possua suporte para ETC2. Recomendamos testes para verificar se não existe aumento no tamanho da aplicação ou consumo de memória uma vez que pode acarretar em duplicação de imagens ou conversão de tipos de compressão em tempo de execução.

image2.jpg

Os ganhos podem ser ainda mais impressionantes se utilizarmos a técnica “Crunched” introduzida na versão 2017 da Unity. Essa técnica reduz a imagem para até 1/6 de seu tamanho em disco.

sE5SpLGpUGhItUu8o0dmKGg.png

No exemplo a seguir é possível notar a diferença de tamanho entre texturas com ou sem compressão:

image4.png

E também a diferença de qualidade da imagem:

image9.png

Imagem gerada por Alexander Suvorov no artigo: Updated Crunch texture compression library.

iOS

No caso de dispositivos com tela retina não é recomendado o uso de compressão para imagens detalhadas ou com gradiente de cores/alpha, pois, na maioria dos casos, evidencia a queda de qualidade e deixa um aspecto “borrado” que pode causar estranheza ao usuário (principalmente para elementos de UI). Para imagens mais simples ou texturas de modelos 3d pode-se utilizar a compressão RGB PVRTC 4 bits ou RGBA PVRTC 4 bits para imagens com canal alpha.  Exemplos de tamanho de imagem e qualidade:

image5.png

image6.png
Imagem gerada por Alex Voica no seguinte artigo: PVRTC: the most efficient texture compression standard for the mobile graphics world.

Otimização de Draw Calls Batching

Objetivo:

  • Otimizar tempo de render (aumento de Frames Per Second).

Cuidados necessários:

  • Não sacrificar organização de seu projeto sem um ganho significativo.

O que é Draw Call Batching? A Unity, ao renderizar uma cena, tenta agrupar diversas imagens de uma mesma camada e que compartilham o mesmo material em uma única “passada de render”, ou seja, todas as imagens de uma camada são desenhadas de uma só vez, depois as imagens de cada uma das camadas superiores:

image7.png

Para que seja possível identificar, agregar (batch) e reduzir o número de Draw Calls é necessário que a disposição das imagens não intercale materiais distintos, ou seja, tente sempre manter os mesmos materiais no maior número de camadas consecutivas. Além disso é importante frisar que outras regras podem interferir no batch, especialmente quando tratamos de UI:

    • Imagens em Canvas diferentes podem ser colocados em batchs diferentes.
    • Devem utilizar o mesmo material.
    • Utilizar o mesmo atlas facilita a utilização de um mesmo material (será explicado mais adiante).

O Editor da Unity disponibiliza uma ferramenta chamada Frame Debugger onde é possível visualizar quais imagens fazem parte do render em cada “passada” e, assim, ajustar o seu projeto de acordo:

image10.png

Atlas

Objetivos:

  • Reduzir tempo de carregamento de texturas para GPU.
  • Reduzir número de Draw Calls

Cuidados necessários:

  • Atlas só fazem sentido para imagens que estão visíveis simultaneamente.
  • Pode aumentar o consumo de memória de vídeo desnecessariamente.

Um Atlas nada mais é do que uma junção de várias imagens diferentes em um único arquivo, isso permite carrega-lo para a GPU de uma única vez ao invés de causar um overhead de leitura para múltiplas imagens diferentes. Existe um mapeamento (feito pela própria Unity) entre as texturas menores dentro do Atlas, para exibi-las individualmente. O Atlas também permite que a Unity realize um batch durante o draw call e desenhe todas as imagens em uma única passada de renderização.

shVhQYWA4TAmg_Trfr-buMw.png

Pasted image at 2018_03_26 04_44 PM.png

Para criar um atlas basta clicar com o botão direito em uma pasta do projeto -> Create Atlas Sprite. Depois selecione e arraste a pasta que contenha as imagens que deseja juntar (ou cada uma das imagens individualmente) para o Inspector. Para utiliza-la basta inserir as imagens em sua cena que a Unity irá automaticamente verificar que existe um atlas associado e irá carrega-la. Uma consequência do uso de Atlas é o aumento do tempo de build pois o Editor necessita compactar todas essas imagens a cada nova alteração. Por isso recomendamos configurar o build de atlas (em Project Settings -> Editor) para “somente em build”. Isso reduz o tempo de espera quando realizamos testes com o editor e só compacta quando gerarmos uma versão que irá rodar nos dispositivos.

Conclusão

Com essas medidas simples é possível conseguir um ganho considerável de performance com pouco esforço. Existem ainda muitas outras técnicas e melhorias que podem ser feitas para conseguir um desempenho ainda melhor, mas as técnicas aqui descritas são mais do que o suficiente para garantir que seu projeto irá rodar na maioria dos dispositivos mais populares do mercado.

Referências

Desenvolvedor Unity na PlayKids e um apaixonado por jogos. Formado em Bacharelado em Ciências de Computação na Universidade de São Paulo – Campus São Carlos
Posted by:Matheus Cardoso

<div id=":lz.ma" class="Mu SP"><span id=":lz.co" class="tL8wMe EMoHub" dir="ltr">Desenvolvedor Unity na PlayKids e um apaixonado por jogos. </span>Formado em Bacharelado em Ciências de Computação na Universidade de São Paulo - Campus São Carlos</div>

Deixe seu comentário