Compressão de Asset Bundles na Unity

24 de Agosto de 2020 · 9 min de leitura

A utilização de Asset Bundles na Unity é uma boa solução da própria engine para download de recursos. É comum utilizar esta forma de distribuição de assets para poder atualizar ou adicionar conteúdo ao jogo após o lançamento, não sendo necessário publicar um novo binário do jogo. Desta forma, jogos podem adicionar personagens, níveis, ou fazer correções em modelos e imagens sem precisar de uma versão nova do jogo. A única limitação é não poder adicionar novos scripts ou utilizar scripts em versões diferentes dos que estão no binário, o que faz os desenvolvedores pensarem em formas de tornar seus jogos mais flexíveis e extrair o máximo dos asset bundles.

Porém, existem mais vantagens em utilizar asset bundles do que apenas atualizar o conteúdo do jogo: é possível reduzir o tamanho do jogo, o uso de memória, uso de CPU e até tempo de loading ajustando algumas opções de compressão na Unity. O objetivo deste post é explorar algumas combinações de compressão, utilizando a plataforma Android como exemplo, e comparar os resultados. Se você quiser saber mais sobre asset bundles, recomendo este curso gratuito da Unity.

Configuração

Escolhi a plataforma Android para o teste por ser mais simples de compilar e testar em um aparelho. Utilizei um Samsung Galaxy S9 com Android 8.0.0 nos testes e instalei um total de 27 builds nele, cada uma contendo uma combinação de compressão no APK e nos asset bundles. Para deixar o teste mais simples, todos asset bundles foram carregados a partir da pasta StreamingAssets. A versão da Unity utilizada foi a 2019.4.7f1 e todas as builds foram geradas em modo release.

A primeira variação foi a configuração do APK gerado pela Unity no Player Settings. Realizei testes com o Scripting Backend Mono e IL2CPP, além de testar com as arquiteturas ARMv7 e AMR64, já que o aparelho utilizado no teste suporta ambas.

Player Settings

No Build Settings testei os métodos de compressão do APK: Default (ZIP), LZ4 (recomendado para development) e LZ4HC (melhor compressão, porém processo de build mais lento). Esta configuração, assim como as anteriores, não afetam os asset bundles, porém são algumas mudanças no APK que quis testar para ver a influência na build.

Build Settings

As últimas configurações foram feitas na geração dos asset bundles, utilizando o Unity package Asset Bundle Browser versão 1.0.7. Para o teste foram gerados 3 conjuntos de asset bundles, cada um com opção de compressão:

  • No compression: sem compressão, carregamento rápido.
  • LZMA: menor tamanho de arquivo, porém mais lento para carregar por causa da descompressão.
  • LZ4: compressão com tamanho maior, porém, descompressão mais rápida.

Asset Bundle Build

Teste

Para realizar o teste escrevi um script bem simples para gerar todas as builds Android, as quais iriam utilizar no teste, considerando as seguintes variações e resultado em um total de 27 APKs:

  • Scripting backend: Mono, IL2CPP
  • Architecture: ARMv7, ARM64
  • Build compression: Default, LZ4, LZ4HC
  • Asset bundles compression: No compression, LZMA, LZ4

Com os 27 APKs gerados, instalei cada um no aparelho, sempre desinstalando o anterior. Cada APK foi iniciado apenas uma vez, com os seguintes dados coletados:

  • Tamanho da builds
  • Tempo até executar o método Awake() no único MonoBehaviour em um Game Object na Scene
  • Tamanho do asset bundle
  • Média do tempo para carregar um asset bundle usando a API AssetBundle.LoadFromFileAsync()

O projeto de teste contém 5 asset bundles, todos são cubos padrões da Unity com um material diferente em cada para modificar a cor (green, orange, purple, red, yellow). Cada asset bundle foi carregado sequencialmente, e o tempo considerado foi a média do tempo para carregar cada um deles.

private IEnumerator Start()
{
    yield return CreateCube("green", "cube_green", new Vector3(0, 0, 0));
    yield return CreateCube("orange", "cube_orange", new Vector3(0, 1.5f, 0));
    yield return CreateCube("purple", "cube_purple", new Vector3(0, -1.5f, 0));
    yield return CreateCube("red", "cube_red", new Vector3(1.5f, 0, 0));
    yield return CreateCube("yellow", "cube_yellow", new Vector3(-1.5f, 0, 0));
}

private IEnumerator CreateCube(string bundleName, string assetName, Vector3 position)
{
    string path = Path.Combine(Application.streamingAssetsPath, bundleName);

    AssetBundleCreateRequest assetBundleRequest = AssetBundle.LoadFromFileAsync(path);
    yield return assetBundleRequest;

    GameObject sphere = assetBundleRequest.assetBundle.LoadAsset<GameObject>(assetName);
    Instantiate(sphere, position, sphere.transform.rotation, transform);
}

O script acima é uma versão simplificada do que utilizei para o teste. Omiti os logs e medições para mostrar como carreguei cada asset bundle e instanciei o game object.

Resultado

O resultado das 27 execuções de cada build está na tabela abaixo, ordenada pelo menor tempo médio para carregar um asset bundle. A planilha com a tabela completa pode ser baixada aqui.

BuildBackendArch.APKStartupBundleSizeAvg. ^
LZ4HCIL2CPPARM6484.5MB2.04sNo15.4MB46.4ms
LZ4HCMonoARMv794.5MB2.14sNo15.4MB49.4ms
LZ4HCIL2CPPARM6446.2MB2.03sLZ47.7MB50ms
LZ4IL2CPPARM6484.6MB2.04sNo15.4MB52.4ms
DefaultIL2CPPARM6484.5MB2.04sNo15.4MB54.8ms
LZ4IL2CPPARM6446.2MB2.04sLZ47.7MB55.4ms
LZ4IL2CPPARMv745.6MB2.03sLZ47.7MB55.8ms
LZ4HCIL2CPPARMv784.0MB2.03sNo15.4MB56ms
LZ4IL2CPPARMv784.0MB2.04sNo15.4MB56.8ms
DefaultIL2CPPARM6446.1MB2.04sLZ47.7MB57.6ms
LZ4HCIL2CPPARMv745.6MB2.03sLZ47.7MB57.8ms
LZ4MonoARMv794.6MB2.14sNo15.4MB58.2ms
DefaultMonoARMv756.1MB2.14sLZ47.7MB59.8ms
DefaultIL2CPPARMv783.9MB2.04sNo15.4MB61.2ms
DefaultIL2CPPARMv745.5MB2.03sLZ47.7MB61.4ms
DefaultMonoARMv794.5MB2.13sNo15.4MB62.4ms
LZ4MonoARMv756.2MB2.13sLZ47.7MB62.8ms
LZ4HCMonoARMv756.2MB2.13sLZ47.7MB67.8ms
LZ4HCIL2CPPARM6439.4MB2.03sLZMA6.4MB1331.6ms
DefaultIL2CPPARM6439.3MB2.03sLZMA6.4MB1343ms
LZ4MonoARMv749.4MB2.13sLZMA6.4MB1355.8ms
LZ4IL2CPPARM6439.4MB2.03sLZMA6.4MB1358.6ms
LZ4HCMonoARMv749.4MB2.13sLZMA6.4MB1369.2ms
LZ4IL2CPPARMv738.8MB2.04sLZMA6.4MB1376.4ms
LZ4HCIL2CPPARMv738.8MB2.03sLZMA6.4MB1378.8ms
DefaultIL2CPPARMv738.7MB2.04sLZMA6.4MB1394ms
DefaultMonoARMv749.3MB2.13sLZMA6.4MB1414.8ms

Legenda da tabela:

  • Build: compression method do Build Settings
  • Backend: scripting backend do Player Settings
  • Arch.: architecture do Player Settings
  • APK: tamanho do APK
  • Startup: tempo até carregar o MonoBehaviour
  • Bundle: compression do Asset Bundle Build
  • Size: tamanho do asset bundle
  • Avg.: tempo médio para carregar um asset bundle

Conclusão

O projeto utilizado no teste era bem pequeno e, apesar do foco ser a compressão de asset bundles, foi interessante ver como as demais configurações influenciam no todo. Qual a melhor configuração para o seu projeto? Depende.

Não comprimir asset bundles faz o carregamento ser muito rápido, porém, isso aumenta tanto o tamanho dos arquivos baixados e da build (quando possuem asset bundles), o que é bem raro um jogo grande utilizar. A compressão LZMA deixa um tamanho menor para download, reduzindo o tempo para o jogador poder jogar e até diminuindo o custo do CDN, porém, aumenta bastante o tempo de loading por causa da descompressão. O LZ4 pode ser o melhor dos dois mundos, sendo relativamente rápido para carregar e não tão pesado, mas ainda assim depende muito do projeto.

Já nas configurações específicas do Android, LZ4HC mostrou um bom desempenho para carregar o jogo, assim como o IL2CPP. Sempre que possível inclua as duas arquiteturas, ARMv7 e ARM64, no APK ou no Android App Bundle (AAB) - o Android vai fazer o resto e decidir qual é a melhor arquitetura presente no app para ser utilizada. No caso do teste, eu forcei builds apenas com ARMv7 e ARM64 para poder comparar os resultados, já que se incluísse ambas apenas a ARM64 seria utilizada.

Lembrando novamente que, este teste foi realizado em um projeto pequeno e simples, mas que serviu para mostrar qual seria a melhor combinação para este jogo. Mais do que isso, este teste mostra que existem muitas variáveis nas configurações do projeto na Unity, as quais podem influenciar bastante a performance do jogo, e conhecê-las te ajudará a escolher qual é a melhor para seu projeto. Lembre-se de não decidir sem testar, algumas horas do seu dia podem melhorar muito o desempenho do seu jogo.