Construindo Westeros para dispositivos móveis em Game of Thrones: Fogo de Dragão

Para a Warner Bros. A Games Boston: trazer o mundo de Westeros para os dispositivos móveis exigiu mais do que apenas adaptar uma franquia tão querida. Com base em *House of the Dragon*, *Game of Thrones*: Dragonfire combina estratégia Multiplayer em grande escala e combate com dragões em uma experiência gratuita desenvolvida para dispositivos móveis modernos.
Os jogadores assumem o papel de um descendente de Valíria encarregado de chocar, criar e comandar dragões, enquanto disputam com milhares de outros jogadores o controle do Trono de Ferro. Para concretizar essa visão, foi necessário equilibrar gráficos de alta qualidade, desempenho escalável e sistemas Multiplayer em tempo real em uma ampla variedade de dispositivos.
Conversamos com Ara Yessayan, diretor técnico, e Taia Lee, artista técnica avançada, sobre a criação de dragões para dispositivos móveis, o suporte a jogabilidade de estratégia em grande escala e o uso do Unity para dar vida ao mundo de Westeros.

Quais foram seus principais objetivos e limitações em relação à experiência do jogador na primeira sessão?
Ara Yessayan: Numa primeira sessão, queremos manter o jogador envolvido e evitar qualquer tempo ocioso que possa interromper sua imersão. É importante apresentar gradualmente os fundamentos da nossa série e a história que temos para contar no mundo de “House of the Dragon”.
Que estratégias vocês utilizaram para minimizar os tempos de carregamento tanto para jogadores novos quanto para os que já conhecem o jogo, e em que essas experiências diferem?
AY: Em uma nova instalação, o objetivo é exigir o mínimo possível de dados iniciais. Exploramos técnicas para reduzir a quantidade de dados que precisamos baixar ou carregar na memória antes de levar os jogadores ao início da nossa experiência, além de usar as transições como oportunidades para lidar com parte desses carregamentos. Para um jogador que está retornando, precisamos de mais dados para recriar seu estado no jogo e posicioná-lo no local correto no mapa. Embora a premissa seja semelhante (minimizar a espera por dados), as técnicas se concentram mais nos custos de desserialização e em formas estratégicas de exigir menos dados antecipadamente.

Como você identificou os principais gargalos no tempo de carregamento durante o desenvolvimento?
AY: Para melhorar os tempos de carregamento, adotamos algumas estratégias. Para ter uma visão geral de onde estavam nossos gargalos, configuramos eventos de perfilagem personalizados para cada etapa do nosso processo de carregamento, cujos dados foram gravados em um arquivo CSV. Agregamos os valores de várias sessões para identificar quais etapas eram os pontos críticos. Também transformamos esses dados em eventos de rastreamento do Chrome e rastreamentos do OpenTelemetry para podermos visualizar melhor como as etapas estavam sendo carregadas em paralelo.
A partir daí, nos dedicamos a uma etapa específica. O módulo de CPU do Unity Profiler nos proporcionou uma visão mais aprofundada do código ineficiente que pudemos otimizar. Em alguns casos, o registro de vários perfis e o uso do Unity Profile Analyzer nos ajudaram a avaliar como o ajuste de alguns valores de carregamento melhorou (ou piorou) os tempos de carregamento.
O CPU Profiler sempre se mostrou útil ao analisar quadros com grandes atrasos, investigando as causas das quedas na taxa de quadros e nos ajudando a encontrar técnicas mais eficazes.
Além do carregamento, o módulo de renderização nos ajudou a identificar ineficiências na renderização durante o jogo, e o RenderDoc foi outra ferramenta que utilizamos quando precisávamos realizar uma análise mais aprofundada de um problema de execução.
Por fim, para manter as sessões ativas, tivemos que garantir que o consumo de memória permanecesse sob controle. Identificamos cargas desnecessárias de recursos e objetos por meio de instantâneos do Memory Profiler, especialmente no que diz respeito ao mapa e às marchas, o que, por sua vez, reduziu os requisitos de carregamento para entrar no jogo.
Como você utilizou o Memory Profiler do Unity para analisar o uso de memória dos pacotes de recursos, incluindo a detecção de duplicações e a verificação do descarregamento de recursos? Você poderia dar um exemplo específico?
Taia Lee: Normalmente, usamos o Memory Profiler para identificar casos em que os recursos são carregados em momentos inesperados do jogo e permanecem na memória. Por exemplo, isso pode ocorrer quando uma textura é usada em vários lugares, mas está contida em um único pacote, fazendo com que todo o pacote seja carregado mesmo quando apenas essa textura é necessária.
Essa é mais uma razão pela qual pretendemos criar pacotes compartilhados específicos para evitar isso. A ferramenta também é útil para identificar os principais consumidores de memória, especialmente aqueles dos quais talvez não tenhamos conhecimento ou que ocupam mais espaço do que o esperado.

Quais foram os problemas de desempenho mais inesperados que você encontrou no início, especialmente no que diz respeito à entrega de conteúdo e ao desempenho da jogabilidade?
AY: Uma surpresa foi a quantidade de memória necessária para carregar os dados do arquivo de mapa que indicavam o layout do mapa. Em Game of Thrones: Em Dragonfire, os jogadores usam seus exércitos e dragões para conquistar territórios (casas) no mapa. Isso ajuda o jogador a coletar recursos e limita os locais para onde ele pode enviar seus exércitos, com base na exigência de que ele ou outro membro de sua facção possua um terreno adjacente.
Sabíamos que precisávamos dividir os dados do mapa em partes para carregar o conteúdo. Esses dados eram necessários para que o jogo pudesse identificar o que havia em cada coordenada, especialmente considerando os dados adicionais que precisávamos armazenar para os nós que abrangem vários blocos. Carregar todas as estruturas associadas a um mapa de 2000×4000 consumiu tanta memória que causou a falha de alguns dispositivos.
À medida que fomos implementando melhorias e otimizações para carregar apenas as partes relevantes do mapa, em vez do mapa inteiro, esse trabalho reduziu significativamente os tempos de carregamento para nossos jogadores assíduos.
Outra técnica que utilizamos para otimizar ainda mais o mapa consistiu em substituir os GameObjects que representavam o terreno no mapa pela renderização direta das malhas. Isso nos permitiu evitar o consumo de memória associado à instanciação desses GameObjects. Ao combinar isso com o carregamento estratégico apenas das malhas e modelos necessários para a área circundante, melhorou-se tanto o desempenho na entrada no mapa quanto o desempenho na rolagem.

Como vocês decidem quais conteúdos devem estar disponíveis no lançamento e quais podem ser transmitidos ou carregados posteriormente?
AY: O primeiro passo é identificar o que precisamos para a experiência do usuário iniciante (FTUE) antes que os jogadores entrem na fase Multiplayer do nosso jogo. Isso nos dá a oportunidade de baixar quaisquer dados que os jogadores utilizem ao acessarem o jogo completo.
Existem outros tipos de conteúdo relacionados a operações em tempo real ou funcionalidades em fase avançada que também podem ser baixados mais adiante no processo. Queremos garantir que os jogadores possam aproveitar o jogo assim que se depararem com um sistema.
Em relação aos carregamentos futuros, trata-se de um equilíbrio delicado entre carregar os elementos antecipadamente (o que pode aumentar o tempo de carregamento) e o que carregamos de forma assíncrona (o que pode ativar um indicador de carregamento antes de entrar em uma tela ou área). Continuamos a fazer ajustes nessa área para oferecer a melhor experiência possível ao usuário.
Como você estruturou e automatizou seu fluxo de trabalho de pacotes de recursos para equilibrar o tamanho do download, o uso de memória e a flexibilidade de execução?
TL: Normalmente, procuramos manter nossos pacotes de recursos abaixo de 8 MB, com algumas exceções dependendo dos casos de uso e dos recursos necessários em cada pacote. Isso nos levou a estruturar os pacotes de forma que os recursos comumente usados em conjunto durante a execução estejam disponíveis simultaneamente.
Por outro lado, evitamos pacotes excessivamente grandes, nos quais apenas uma parte dos recursos é utilizada. Temos pacotes organizados por área do jogo, recurso ou tipos de recursos compartilhados. Por exemplo, temos diferentes biomas no nosso mapa e cada bioma possui recursos próprios, adequados àquele local específico.
Não precisamos incluir as montanhas nevadas do norte no mesmo pacote que as montanhas desérticas do sul. No entanto, algumas malhas e texturas são compartilhadas entre biomas, portanto, esses recursos são incluídos em um pacote compartilhado.
É um equilíbrio que exige compreender onde os recursos são utilizados ao longo do jogo para manter o desempenho otimizado. Como em qualquer jogo em serviço, este é um processo contínuo que precisamos revisar e reorganizar à medida que mais recursos são adicionados.
AY: Antes do lançamento do Addressables, desenvolvemos internamente um conjunto de ferramentas para nos ajudar a resolver muitos dos problemas que o Addressables agora resolve. Algumas dessas ferramentas internas nos permitem compreender a composição dos nossos pacotes e possibilitam técnicas avançadas para baixar patches a fim de atualizá-los (chamamos isso de “correção de binários”).

Que compromissos ou desafios você encontrou ao trabalhar com pacotes de recursos, e como você os superou?
TL: O maior desafio que enfrentamos é que o tamanho dos pacotes de recursos pode aumentar drasticamente se alguém editar um prefab existente ou adicionar muitos recursos novos sem perceber o impacto potencial no tamanho e na organização dos pacotes.
Já tivemos casos em que os pacotes aumentaram mais de 5 MB de uma só vez sem que ninguém percebesse e, no pior dos casos, isso fez com que nosso arquivo .aab ultrapassasse o limite de tamanho exigido para envio à loja. Desde então, adicionamos alertas ao nosso pipeline de compilação para detectar esses casos e ajudamos os desenvolvedores a entender melhor quando suas alterações podem aumentar o tamanho dos pacotes de forma inesperada.
Como você lida com as dependências de recursos para evitar downloads redundantes e o uso desnecessário de memória?
TL: Em nossas ferramentas internas de gerenciamento de pacotes de recursos, podemos identificar recursos duplicados entre os pacotes. Em geral, não queremos ter muitos recursos duplicados, especialmente os de tamanho maior; por isso, adicionamos esses recursos diretamente a um pacote, em vez de permitir que sejam incluídos como dependência em vários pacotes. Precisamos garantir que ele seja adicionado a um pacote que possa ser usado em vários lugares, mas geralmente criamos um pacote compartilhado separado.

Que técnicas você utilizou para reduzir picos de uso da CPU ou atrasos causados pela desserialização de recursos durante a inicialização do aplicativo?
AY: Uma das técnicas que utilizamos para nossos dados de design é usar o formato Protocol Buffers (Protobuf) para armazenamento, em vez do JSON convencional. O Protobuf (o formato binário utilizado pelo gRPC) oferece um armazenamento mais compacto e uma desserialização mais rápida.
Ao utilizar um arquivo de esquema estruturado associado, podemos carregar dados na memória muito mais rapidamente, sem precisar analisar o conteúdo das cadeias JSON e tokenizar sua estrutura. Exploramos outras opções, como o BSON e o Odin Serializer, para armazenar e deserializar dados com mais eficiência, mas a possibilidade de usar o gRPC para nos comunicarmos com nossos servidores de forma mais eficiente também fez com que essa fosse a escolha certa para nós.
Uma gestão eficaz das threads também é fundamental. Identifique quais tarefas você pode transferir para fora da thread principal do Unity, para que possa se concentrar no carregamento de recursos e cenas no único local onde é possível realizar esse trabalho.
Como otimizar o tamanho das compilações e os pipelines de implantação para garantir correções e atualizações de conteúdo mais rápidas?
AY: Existem algumas técnicas que utilizamos. Em primeiro lugar, nosso foco é encontrar o equilíbrio certo entre os recursos necessários que já estão integrados no arquivo binário do jogo e aqueles que podem ser baixados posteriormente. Nosso jogo possui um tutorial que leva alguns minutos para ser concluído, o que nos permite baixar recursos adicionais conforme necessário, sem interromper os jogadores durante seu primeiro login.
O uso do Play Asset Delivery do Android também nos ajudou a ter mais recursos disponíveis desde o início. Começamos a incluir tabelas de dados dinâmicos selecionadas no cliente do jogo, prevendo que parte delas estaria desatualizada. Ao baixar apenas as tabelas que sofreram alterações, reduzimos o tempo de carregamento.
A partir daí, adotamos nossa técnica de correção de binários, o que nos permitiu baixar diferenças binárias mais compactas e corrigir os arquivos modificados, em vez de baixar a nova versão na íntegra. Também podemos usar isso com pacotes de recursos, atualizando o conteúdo do jogo conforme necessário para novos eventos ao vivo.

Olhando para trás, qual foi a mudança mais significativa que você fez para melhorar os tempos de carregamento para os jogadores?
AY: A resposta é simples: garantir que os jogadores carreguem apenas o que precisam. Antes do lançamento preliminar, identificamos o mapa como um dos nossos maiores gargalos no tempo de carregamento. Na época, o jogo carregava todos os recursos do mapa de uma vez, antes de começarmos nossas otimizações para exibir apenas as regiões próximas à base do jogador.
Identificar o que era necessário e implementar técnicas para carregar o restante de forma assíncrona posteriormente reduziu em vários segundos o tempo de carregamento, mesmo em dispositivos de última geração. Nossa equipe cumpriu a missão de melhorar os tempos de carregamento para os jogadores e nos colocou no caminho para uma melhor experiência do usuário, e não tenho palavras para agradecer o empenho deles neste projeto.
Para saber mais sobre projetos Made with Unity, acesse a página de Recursos.
