paint-brush
Comemorando 10 anos de robôs de guerra e refletindo de uma perspectiva técnicapor@pauxi
348 leituras
348 leituras

Comemorando 10 anos de robôs de guerra e refletindo de uma perspectiva técnica

por Paul Xi31m2024/04/30
Read on Terminal Reader

Muito longo; Para ler

Neste post, analisamos o lado técnico do War Robots ao longo de 10 anos: as coisas legais, os problemas, os experimentos, a remasterização do jogo e muito mais!
featured image - Comemorando 10 anos de robôs de guerra e refletindo de uma perspectiva técnica
Paul Xi HackerNoon profile picture
0-item
1-item


The War Robots comemora seu aniversário de 10 anos em abril! E até hoje continuamos desenvolvendo e apoiando-o, não apenas lançando novos recursos para nossos jogadores, mas também melhorando-o tecnicamente.


Neste artigo discutiremos nossos muitos anos de experiência no desenvolvimento técnico deste grande projeto. Mas primeiro, aqui está um instantâneo do projeto tal como está:


  • Centenas de milhares de DAU (Usuários Ativos Diários)
  • Centenas de milhões de instalações
  • Dezenas de milhares de partidas simultâneas de 12 pessoas
  • Disponível em quatro plataformas principais (iOS, Android, Steam, Amazon)
  • Dezenas de milhares de dispositivos suportados, incluindo Android e PC
  • Centenas de servidores
  • Aproximadamente 20 desenvolvedores de clientes, 9 desenvolvedores de servidores, uma equipe de desenvolvimento de projetos cruzados de 8 pessoas e 3 DevOps
  • Cerca de 1,5 milhão de linhas de código do lado do cliente
  • Usando Unity a partir da versão 4 em 2014 e atualmente usando a versão 2022 LTS em 2024.


Para manter a funcionalidade deste tipo de projeto e garantir um maior desenvolvimento de alta qualidade, não é suficiente trabalhar apenas nas tarefas imediatas do produto; também é fundamental melhorar sua condição técnica, simplificar o desenvolvimento e automatizar processos – inclusive aqueles relacionados à criação de novos conteúdos. Além disso, devemos nos adaptar constantemente às mudanças no mercado de dispositivos de usuário disponíveis.


O texto é baseado em entrevistas com Pavel Zinov, Chefe do Departamento de Desenvolvimento da Pixonic (MY.GAMES), e Dmitry Chetverikov, Desenvolvedor Líder de War Robots.


O início

Voltemos a 2014: o projeto War Robots foi inicialmente criado por uma pequena equipa, e todas as soluções técnicas enquadram-se no paradigma de rápido desenvolvimento e entrega de novas funcionalidades ao mercado. Nessa época, a empresa não dispunha de grandes recursos para hospedar servidores dedicados e, assim, a War Robots entrou no mercado com um multiplayer em rede baseado na lógica P2P.


Photon Cloud foi usado para transferir dados entre clientes: cada comando do jogador, seja movimento, tiro ou qualquer outro comando, foi enviado ao servidor Photon Cloud usando RPCs separados. Como não existia um servidor dedicado, o jogo contava com um cliente master, que era responsável pela confiabilidade do estado da partida para todos os jogadores. Ao mesmo tempo, os jogadores restantes processaram totalmente o estado de todo o jogo em seus clientes locais.


Por exemplo, não houve verificação de movimentação — o cliente local moveu seu robô como bem entendesse, esse estado foi enviado ao cliente mestre, e o cliente mestre acreditou incondicionalmente nesse estado e simplesmente o encaminhou para outros clientes na partida. Cada cliente manteve independentemente um registro da batalha e o enviou ao servidor no final da partida, então o servidor processou os registros de todos os jogadores, concedeu uma recompensa e enviou os resultados da partida a todos os jogadores.


Usamos um Profile Server especial para armazenar informações sobre os perfis dos jogadores. Consistia em muitos servidores com um banco de dados Cassandra. Cada servidor era um servidor de aplicativos simples no Tomcat e os clientes interagiam com ele via HTTP.


Desvantagens da nossa abordagem inicial

A abordagem usada na jogabilidade tinha uma série de desvantagens. A equipe, claro, sabia disso, mas devido à rapidez de desenvolvimento e entrega do produto final ao mercado, vários compromissos tiveram que ser feitos.


Entre essas desvantagens, em primeiro lugar, estava a qualidade da conexão do cliente master. Se o cliente tivesse uma rede ruim, todos os jogadores em uma partida sofreriam lag. E, se o cliente master não estivesse rodando em um smartphone muito potente, então, devido à alta carga colocada nele, o jogo também apresentava atraso na transferência de dados. Então, nesse caso, além do cliente master, outros jogadores também sofreram.


A segunda desvantagem era que essa arquitetura estava sujeita a problemas com trapaceiros. Como o estado final foi transferido do cliente master, este cliente poderia alterar muitos parâmetros da partida a seu critério, por exemplo, contando o número de zonas capturadas na partida.


Terceiro, existe um problema com a funcionalidade de matchmaking no Photon Cloud: o jogador mais velho na sala de matchmaking do Photon Cloud se torna o cliente mestre e se algo acontecer com ele durante o matchmaking (por exemplo, uma desconexão), a criação do grupo será impactada negativamente. Curiosidade relacionada: para evitar que o matchmaking no Photon Cloud feche após enviar um grupo para uma partida, por algum tempo, até montamos um PC simples em nosso escritório, e isso sempre suportou o matchmaking, o que significa que o matchmaking com o Photon Cloud não poderia falhar .


Em algum momento, o número de problemas e solicitações de suporte atingiu uma massa crítica, e começamos a pensar seriamente em evoluir a arquitetura técnica do jogo – essa nova arquitetura foi chamada de Infraestrutura 2.0.


A transição para a Infraestrutura 2.0

A ideia principal por trás desta arquitetura foi a criação de servidores de jogos dedicados. Naquela época, a equipe do cliente carecia de programadores com ampla experiência em desenvolvimento de servidores. No entanto, a empresa estava trabalhando simultaneamente em um projeto de análise de alta carga baseado em servidor, que chamamos de AppMetr. A equipe criou um produto que processava bilhões de mensagens por dia e tinha amplo conhecimento no desenvolvimento, configuração e arquitetura correta de soluções de servidor. E assim, alguns membros dessa equipe juntaram-se ao trabalho da Infraestrutura 2.0.


Em 2015, em um período bastante curto, um servidor .NET foi criado, envolvido na estrutura Photon Server SDK em execução no Windows Server. Decidimos abandonar o Photon Cloud, mantendo apenas a estrutura de rede Photon Server SDK no projeto do cliente e criamos um Master Server para conectar clientes e servidores correspondentes para matchmaking. Parte da lógica foi movida do cliente para um servidor de jogo dedicado. Em particular, foi introduzida a validação básica de danos, bem como a colocação de cálculos de resultados de partidas no servidor.



O surgimento dos microsserviços

Depois de criarmos com sucesso a Infraestrutura 2.0, percebemos que precisávamos continuar avançando no sentido de dissociar as responsabilidades dos serviços: o resultado disso foi a criação da arquitetura de microsserviços. O primeiro microsserviço criado nesta arquitetura foram os clãs.


A partir daí surgiu um serviço de Comunicação separado, responsável pela transferência de dados entre microsserviços. O cliente foi ensinado a estabelecer conexão com o “hangar” responsável pela metamecânica no servidor do jogo e a fazer solicitações de API (ponto de entrada único para interação com serviços) utilizando UDP sobre Photon Cloud. Aos poucos, o número de serviços cresceu e, como resultado, refatoramos o microsserviço de matchmaking. Com isso, foi criada a funcionalidade de notícias, Inbox.


Quando criamos os clãs, ficou claro que os jogadores queriam se comunicar uns com os outros no jogo – eles precisavam de algum tipo de bate-papo. Este foi originalmente criado com base no Photon Chat, mas todo o histórico do chat foi apagado assim que o último cliente foi desconectado. Portanto, convertemos esta solução em um microsserviço separado baseado no banco de dados Cassandra.


Infraestrutura 4.0 e locais de servidores

A nova arquitetura de microsserviços nos permitiu gerenciar convenientemente um grande número de serviços independentes entre si, o que significou dimensionar horizontalmente todo o sistema e evitar tempos de inatividade.


Em nosso jogo, as atualizações ocorreram sem a necessidade de pausar os servidores. O perfil do servidor era compatível com diversas versões de clientes, e quanto aos servidores de jogos, sempre temos um pool de servidores para as versões antigas e novas do cliente, esse pool mudou gradativamente após o lançamento de uma nova versão nas lojas de aplicativos, e os servidores de jogos desapareceram da versão antiga com o tempo. O esboço geral da infraestrutura atual foi denominado Infraestrutura 4.0 e ficou assim:



Além de mudar a arquitetura, também enfrentamos um dilema sobre onde nossos servidores deveriam estar localizados, já que deveriam cobrir jogadores de todo o mundo. Inicialmente, muitos microsserviços estavam localizados no Amazon AWS por vários motivos, em particular, pela flexibilidade que este sistema oferece em termos de escala, pois isso era muito importante em momentos de aumento do tráfego de jogos (como quando era apresentado em lojas e para impulsionar a AU). Além disso, naquela época, era difícil encontrar um bom provedor de hospedagem na Ásia que fornecesse boa qualidade de rede e conexão com o mundo exterior.


A única desvantagem do Amazon AWS era seu alto custo. Portanto, com o tempo, muitos de nossos servidores migraram para nosso próprio hardware – servidores que alugamos em data centers em todo o mundo. No entanto, o Amazon AWS continuou a ser uma parte importante da arquitetura, pois permitiu o desenvolvimento de código que estava em constante mudança (em particular, o código para os serviços de Ligas, Clãs, Chats e Notícias) e que não era suficientemente coberto pela estabilidade. testes. Mas, assim que percebemos que o microsserviço estava estável, transferimos ele para nossas próprias instalações. Atualmente, todos os nossos microsserviços são executados em nossos servidores de hardware.


Controle de qualidade do dispositivo e mais métricas

Em 2016, mudanças importantes foram feitas na renderização do jogo: surgiu o conceito de Ubershaders com um atlas de textura único para todos os mechs em batalha, o que reduziu bastante o número de draws e melhorou o desempenho do jogo.


Ficou claro que nosso jogo estava sendo jogado em dezenas de milhares de dispositivos diferentes e queríamos oferecer a cada jogador as melhores condições de jogo possíveis. Assim, criamos o Quality Manager, que é basicamente um arquivo que analisa o dispositivo de um jogador e ativa (ou desativa) determinados recursos de jogo ou de renderização. Além disso, esse recurso nos permitiu gerenciar essas funções até o modelo específico do dispositivo para que pudéssemos corrigir rapidamente os problemas que nossos jogadores estavam enfrentando.


Também é possível baixar as configurações do Quality Manager do servidor e selecionar dinamicamente a qualidade no dispositivo dependendo do desempenho atual. A lógica é bastante simples: todo o corpo do Gestor da Qualidade é dividido em blocos, cada um deles responsável por algum aspecto do desempenho. (Por exemplo, a qualidade das sombras ou anti-aliasing.) Se o desempenho de um usuário for prejudicado, o sistema tentará alterar os valores dentro do bloco e selecionar uma opção que levará a um aumento no desempenho. Voltaremos à evolução do Quality Manager um pouco mais tarde, mas nesta fase a sua implementação foi bastante rápida e proporcionou o nível de controlo necessário.


O trabalho no Quality Manager também foi necessário à medida que o desenvolvimento gráfico continuava. O jogo agora apresenta sombras dinâmicas em cascata, implementadas inteiramente pelos nossos programadores gráficos.


Aos poucos, muitas entidades apareceram no código do projeto, então decidimos que seria bom começar a gerenciar seu tempo de vida, bem como delimitar o acesso a diferentes funcionalidades. Isso foi especialmente importante para separar o hangar e o código de combate porque muitos recursos não estavam sendo liberados quando o contexto do jogo mudava. Começamos a explorar vários contêineres de gerenciamento de dependências de IoC. Em particular, analisamos a solução StrangeIoC. Naquela época, a solução parecia um tanto complicada para nós e, por isso, implementamos nossa própria DI simples no projeto. Também introduzimos contextos de hangar e batalha.


Além disso, começamos a trabalhar no controle de qualidade do projeto; O FirebaseCrashlytics foi integrado ao jogo para identificar falhas, ANRs e exceções no ambiente de produção.


Utilizando nossa solução analítica interna chamada AppMetr, criamos os painéis necessários para monitoramento regular do status do cliente e comparação das alterações feitas durante os lançamentos. Além disso, para melhorar a qualidade do projeto e aumentar a confiança nas alterações feitas no código, começamos a pesquisar autotestes.


O aumento do número de assets e bugs únicos no conteúdo nos fizeram pensar em organizar e unificar os assets. Isso era especialmente verdadeiro para os mechas porque sua construção era única para cada um deles; para isso criamos ferramentas em Unity, e com elas fizemos manequins mecânicos com os principais componentes necessários. Isso significava que o departamento de design de jogos poderia editá-los facilmente – e foi assim que começamos a unificar nosso trabalho com mechs.


Mais plataformas

O projeto continuou crescendo e a equipe começou a pensar em lançá-lo em outras plataformas. Além disso, como outro ponto, para algumas plataformas móveis da época, era importante que o jogo suportasse o máximo possível de funções nativas da plataforma, pois isso permitia que o jogo fosse apresentado. Então, começamos a trabalhar em uma versão experimental do War Robots para Apple TV e em um aplicativo complementar para Apple Watch.


Além disso, em 2016, lançamos o jogo em uma plataforma que era nova para nós — a Amazon AppStore. Em termos de características técnicas, esta plataforma é única porque possui uma linha unificada de dispositivos, como a Apple, mas o poder desta linha está ao nível dos Androids low-end. Pensando nisso, ao lançar nesta plataforma, foi feito um trabalho significativo para otimizar o uso de memória e desempenho, por exemplo, trabalhamos com atlas, compressão de texturas. O sistema de pagamento Amazon, análogo ao Game Center para login e conquistas, foi integrado ao cliente e, portanto, o fluxo de trabalho com o login do jogador foi refeito e as primeiras conquistas foram lançadas.


Vale ressaltar também que, simultaneamente, a equipe do cliente desenvolveu pela primeira vez um conjunto de ferramentas para o Unity Editor, que possibilitaram a gravação de vídeos do jogo no motor. Isso tornou mais fácil para nosso departamento de marketing trabalhar com ativos, controlar o combate e utilizar a câmera para criar os vídeos que nosso público tanto adorava.


Lutando contra trapaceiros

Devido à ausência do Unity, e em particular do motor de física no servidor, a maioria das partidas continuou a ser emulada nos dispositivos dos jogadores. Por causa disso, os problemas com trapaceiros persistiram. Periodicamente, nossa equipe de suporte recebia feedback de nossos usuários com vídeos de trapaceiros: eles aceleravam em um momento conveniente, voavam pelo mapa, matavam outros jogadores em uma esquina, tornavam-se imortais e assim por diante.


Idealmente, transferiríamos toda a mecânica para o servidor; entretanto, além do fato de o jogo estar em constante desenvolvimento e já ter acumulado uma boa quantidade de código legado, uma abordagem com um servidor autoritativo também pode ter outras desvantagens. Por exemplo, aumentando a carga na infra-estrutura e a procura por uma melhor ligação à Internet. Dado que a maioria dos nossos utilizadores jogava em redes 3G/4G, esta abordagem, embora óbvia, não foi uma solução eficaz para o problema. Como abordagem alternativa para combater trapaceiros dentro da equipe, tivemos uma nova ideia: criar um “quórum”.


Quorum é um mecanismo que permite comparar múltiplas simulações de diferentes jogadores ao confirmar o dano causado; sofrer danos é uma das principais características do jogo, da qual depende praticamente o resto do seu estado. Por exemplo, se você destruir seus oponentes, eles não conseguirão capturar os faróis.


A ideia dessa decisão foi a seguinte: cada jogador ainda simulava o mundo inteiro (inclusive atirando em outros jogadores) e enviava os resultados para o servidor. O servidor analisou os resultados de todos os jogadores e tomou uma decisão sobre se o jogador foi danificado e em que medida. Nossas partidas envolvem 12 pessoas, então para o servidor considerar que houve dano, basta que esse fato seja registrado nas simulações locais de 7 jogadores. Esses resultados seriam então enviados ao servidor para validação adicional. Esquematicamente, este algoritmo pode ser representado da seguinte forma:



Este esquema nos permitiu reduzir significativamente o número de trapaceiros que se tornavam invencíveis em batalha. O gráfico a seguir mostra o número de reclamações contra esses usuários, bem como as etapas de habilitação dos recursos de cálculo de danos no servidor e habilitação do quorum de danos:



Este mecanismo de causar danos resolveu problemas de trapaça por muito tempo porque, apesar da falta de física no servidor (e, portanto, da incapacidade de levar em conta coisas como a topografia da superfície e a posição real dos robôs no espaço), os resultados de a simulação para todos os clientes e o seu consenso proporcionaram uma compreensão inequívoca de quem causou danos a quem e em que condições.


Desenvolvimento adicional do projeto

Com o tempo, além das sombras dinâmicas, adicionamos pós-efeitos ao jogo usando a pilha de pós-processamento do Unity e a renderização em lote. Exemplos de gráficos aprimorados como resultado da aplicação de efeitos de pós-processamento podem ser vistos na imagem abaixo:



Para entender melhor o que estava acontecendo nas compilações de depuração, criamos um sistema de log e implantamos um serviço local na infraestrutura interna que poderia coletar logs de testadores internos e facilitar a localização das causas dos bugs. Essa ferramenta ainda é utilizada para playtests pelo QA, e os resultados de seu trabalho são anexados aos tickets no Jira.


Também adicionamos um gerenciador de pacotes escrito por nós mesmos ao projeto. Inicialmente queríamos melhorar o trabalho com diversos pacotes e plugins externos (dos quais o projeto já havia acumulado em número suficiente). Porém, a funcionalidade do Unity não era suficiente naquele momento, então nossa equipe interna da plataforma desenvolveu sua própria solução com versionamento de pacotes, armazenamento usando NPM e a capacidade de conectar pacotes ao projeto simplesmente adicionando um link para o GitHub. Como ainda queríamos usar uma solução de mecanismo nativo, consultamos colegas da Unity e várias de nossas ideias foram incluídas na solução final da Unity quando eles lançaram seu Gerenciador de Pacotes em 2018.


Continuamos a expandir o número de plataformas onde nosso jogo estava disponível. Eventualmente, apareceu o Facebook Gameroom, uma plataforma que suporta teclado e mouse. Trata-se de uma solução do Facebook (Meta), que permite aos usuários rodar aplicativos no PC; essencialmente, é um análogo da loja Steam. É claro que o público do Facebook é bastante diversificado, e o Facebook Gameroom foi lançado em um grande número de dispositivos, principalmente aqueles que não são de jogos. Por isso, decidimos manter os gráficos mobile no jogo para não sobrecarregar os PCs do público principal. Do ponto de vista técnico, as únicas mudanças significativas que o jogo exigiu foram a integração do SDK, sistema de pagamento e suporte para teclado e mouse usando o sistema de entrada nativo Unity.


Tecnicamente, diferia pouco da construção do jogo para Steam, mas a qualidade dos dispositivos era significativamente inferior porque a plataforma se posicionava como um local para jogadores casuais com dispositivos bastante simples que podiam rodar nada mais do que um navegador, com isso em mente, o Facebook esperava entrar em um mercado bastante grande. Os recursos desses dispositivos eram limitados e a plataforma, em particular, apresentava problemas persistentes de memória e desempenho.


Posteriormente, a plataforma fechou e transferimos nossos jogadores para o Steam, onde foi possível. Para isso desenvolvemos um sistema especial com códigos para transferência de contas entre plataformas.


Dando uma chance aos robôs de guerra em VR

Outro acontecimento digno de nota em 2017: o lançamento do iPhone X, o primeiro smartphone com notch. Naquela época, o Unity não suportava dispositivos com entalhes, então tivemos que criar uma solução personalizada baseada nos parâmetros UnityEngine.Screen.safeArea, o que forçava a escala da UI e evitava diferentes zonas seguras na tela do telefone.


Além disso, em 2017, decidimos tentar outra coisa – lançar uma versão VR do nosso jogo no Steam. O desenvolvimento durou cerca de seis meses e incluiu testes em todos os capacetes disponíveis na época: Oculus, HTC Vive e Windows Mixed Reality. Este trabalho foi realizado utilizando a API Oculus e a API Steam. Além disso, foi disponibilizada uma versão demo para headsets PS VR, bem como suporte para MacOS.


Do ponto de vista técnico, era necessário manter um FPS estável: 60 FPS para PS VR e 90 FPS para HTC Vive. Para isso, foi utilizado Zone Culling auto-roteirizado, além do Frustum e Occlusão padrão, bem como reprojeções (quando alguns frames são gerados com base nos anteriores).


Para resolver o problema do enjôo, foi adotada uma interessante solução criativa e técnica: nosso jogador tornou-se um piloto robô e sentou-se na cabine. A cabine era um elemento estático, então o cérebro a percebia como uma espécie de constante no mundo e, portanto, o movimento no espaço funcionava muito bem.


O jogo também tinha um cenário que exigia o desenvolvimento de vários gatilhos, scripts e cronogramas caseiros separados, já que o Cinemachine ainda não estava disponível naquela versão do Unity.


Para cada arma, a lógica de mira, travamento, rastreamento e mira automática foi escrita do zero.


A versão foi lançada no Steam e foi bem recebida pelos nossos jogadores. No entanto, após a campanha Kickstarter, decidimos não continuar o desenvolvimento do projeto, uma vez que o desenvolvimento de um jogo de tiro PvP completo para VR estava repleto de riscos técnicos e da falta de preparação do mercado para tal projeto.


Lidando com nosso primeiro bug sério

Enquanto trabalhávamos na melhoria da componente técnica do jogo, bem como na criação de novas e no desenvolvimento de mecânicas e plataformas existentes, deparamo-nos com o primeiro bug grave no jogo, que afetou negativamente as receitas ao lançar uma nova promoção. O problema era que o botão “Preço” estava localizado incorretamente como “Prêmio”. A maioria dos jogadores ficou confusa com isso, fez compras com moeda real e depois pediu reembolso.


O problema técnico era que, naquela época, todas as nossas localizações estavam integradas no cliente do jogo. Quando esse problema surgiu, entendemos que não poderíamos mais adiar o trabalho em uma solução que nos permitisse atualizar as localizações. Foi assim que foi criada uma solução baseada em CDN e ferramentas de acompanhamento, como projetos em TeamCity, que automatizam o upload de arquivos de localização para o CDN.


Isso nos permitiu baixar localizações dos serviços que usamos (POEditor) e carregar dados brutos para o CDN.

Depois disso, o perfil do servidor passou a configurar links para cada versão do cliente, correspondentes aos dados de localização. Quando o aplicativo foi iniciado, o servidor de perfil começou a enviar esse link ao cliente e esses dados foram baixados e armazenados em cache.


Otimizando o trabalho com dados

Avancemos para 2018. À medida que o projeto crescia, também crescia a quantidade de dados transferidos do servidor para o cliente. No momento da conexão ao servidor, foi necessário baixar muitos dados sobre saldo, configurações atuais, etc.


Os dados atuais foram apresentados em formato XML e serializados pelo serializador Unity padrão.

Além de transferir uma quantidade bastante grande de dados ao iniciar o cliente, bem como ao se comunicar com o servidor de perfil e o servidor de jogo, os dispositivos dos jogadores gastavam muita memória armazenando o saldo atual e serializando/desserializando-o.


Ficou claro que para melhorar o desempenho e o tempo de inicialização da aplicação seria necessário realizar P&D para encontrar protocolos alternativos.


Os resultados do estudo sobre nossos dados de teste são exibidos nesta tabela:


Protocolo

tamanho

alocar

tempo

XML

2,2MB

18,4 MiB

518,4ms

MessagePack (sem contrato)

1,7MB

2 MiB

32,35ms

MessagePack (chaves str)

1,2MB

1,9 MiB

25,8ms

MessagePack (chaves internas)

0,53MB

1,9

16,5

FlatBuffers

0,5MB

216B

0ms/12ms/450 KB


Como resultado, escolhemos o formato MessagePack porque a migração era mais barata do que mudar para FlatBuffers com resultados de saída semelhantes, especialmente ao usar MessagePack com chaves inteiras.


Para FlatBuffers (e com buffers de protocolo) é necessário descrever o formato da mensagem em uma linguagem separada, gerar código C# e Java e utilizar o código gerado na aplicação. Como não queríamos incorrer nesse custo adicional de refatoração do cliente e do servidor, mudamos para o MessagePack.


A transição foi quase perfeita e, nas primeiras versões, apoiamos a capacidade de reverter para XML até estarmos convencidos de que não havia problemas com o novo sistema. A nova solução cobriu todas as nossas tarefas – reduziu significativamente o tempo de carregamento do cliente e também melhorou o desempenho ao fazer solicitações aos servidores.


Cada recurso ou nova solução técnica em nosso projeto é lançada sob uma “sinalização”. Essa flag fica armazenada no perfil do jogador e chega ao cliente no início do jogo, junto com o saldo. Via de regra, quando uma nova funcionalidade é lançada, especialmente uma técnica, vários lançamentos de clientes contêm ambas as funcionalidades – antigas e novas. A ativação de novas funcionalidades ocorre estritamente de acordo com o estado da bandeira descrito acima, o que nos permite monitorar e ajustar o sucesso técnico de uma determinada solução em tempo real.


Aos poucos, chegou a hora de atualizar a funcionalidade de Injeção de Dependência no projeto. O atual não conseguia mais lidar com um grande número de dependências e, em alguns casos, introduzia conexões não óbvias que eram muito difíceis de quebrar e refatorar. (Isso foi especialmente verdadeiro para inovações relacionadas à IU de uma forma ou de outra.)


Na hora de escolher uma nova solução, a escolha foi simples: o conhecido Zenject, que já havia sido comprovado em nossos outros projetos. Este projeto está em desenvolvimento há muito tempo, tem suporte ativo, novos recursos estão sendo adicionados e muitos desenvolvedores da equipe estão familiarizados com ele de alguma forma.


Aos poucos, começamos a reescrever War Robots usando Zenject. Todos os novos módulos foram desenvolvidos com ele, e os antigos foram gradativamente refatorados. Desde que usamos o Zenject, recebemos uma sequência mais clara de carregamento de serviços dentro dos contextos que discutimos anteriormente (combate e hangar), e isso permitiu aos desenvolvedores mergulhar com mais facilidade no desenvolvimento do projeto, bem como desenvolver novos recursos com mais confiança. dentro desses contextos.


Em versões relativamente novas do Unity, tornou-se possível trabalhar com código assíncrono via async/await. Nosso código já usava código assíncrono, mas não havia um padrão único em toda a base de código e, como o back-end de script do Unity começou a oferecer suporte a assíncrono/espera, decidimos realizar pesquisa e desenvolvimento e padronizar nossas abordagens para código assíncrono.


Outra motivação foi remover o callback hell do código – é quando há uma cadeia sequencial de chamadas assíncronas e cada chamada aguarda o resultado da próxima.


Naquela época, existiam diversas soluções populares, como RSG ou UniRx; comparamos todos eles e os compilamos em uma única tabela:



RSG.Promessa

TPL

TPL com espera

UniTask

UniTask com assíncrono

Tempo até terminar, s

0,15843

0,1305305

0,1165172

0,1330536

0,1208553

Primeiro quadro Tempo/Auto, ms

105,25/82,63

13,51/11,86

21,89/18,40

28,80/24,89

19.27/15.94

Alocações do primeiro quadro

40,8MB

2,1MB

5,0MB

8,5MB

5,4MB

Tempo/próprio do segundo quadro, ms

55,39/23,48

0,38/0,04

0,28/0,02

0,32/0,03

0,69/0,01

Alocações de segundo quadro

3,1MB

10,2 KB

10,3 KB

10,3 KB

10,4 KB


Por fim, decidimos usar async/await nativo como padrão para trabalhar com código C# assíncrono com o qual a maioria dos desenvolvedores está familiarizada. Decidimos não utilizar UniRx.Async, pois as vantagens do plugin não cobriam a necessidade de contar com uma solução de terceiros.


Optamos por seguir o caminho da funcionalidade mínima exigida. Abandonamos o uso de RSG.Promise ou titulares, porque, em primeiro lugar, era necessário treinar novos desenvolvedores para trabalhar com essas ferramentas, geralmente desconhecidas, e em segundo lugar, era necessário criar um wrapper para o código de terceiros que usa tarefas assíncronas em RSG.Promise ou titulares. A escolha do async/await também ajudou a padronizar a abordagem de desenvolvimento entre os projetos da empresa. Essa transição resolveu nossos problemas – terminamos com um processo claro para trabalhar com código assíncrono e removemos do projeto o problema de callback de difícil suporte.


A IU foi gradualmente melhorada. Como em nossa equipe a funcionalidade de novos recursos é desenvolvida por programadores clientes, e o desenvolvimento de layout e interface é realizado pelo departamento UI/UX, precisávamos de uma solução que nos permitisse paralelizar o trabalho no mesmo recurso – para que o o designer de layout poderia criar e testar a interface enquanto o programador escreve a lógica.


A solução para este problema foi encontrada na transição para o modelo MVVM de trabalho com interface. Este modelo permite ao designer de interface não apenas projetar uma interface sem envolver um programador, mas também ver como a interface reagirá a determinados dados quando nenhum dado real ainda tiver sido conectado. Após algumas pesquisas sobre soluções prontas para uso, bem como a prototipagem rápida de nossa própria solução chamada Pixonic ReactiveBindings, compilamos uma tabela de comparação com os seguintes resultados:



Tempo de CPU

Mem. Alocar

Mem. Uso

Vinculação de dados Peppermint

367ms

73 KB

17,5MB

DisplayFab

223ms

147 KB

8,5MB

Solda Unidade

267ms

90 KB

15,5MB

Ligações reativas Pixonic

152ms

23 KB

3MB


Assim que o novo sistema se comprovou, principalmente em termos de simplificação da produção de novas interfaces, passamos a utilizá-lo para todos os novos projetos, bem como para todas as novas funcionalidades que surgissem no projeto.


Remasterização do jogo para novas gerações de dispositivos móveis

Em 2019, foram lançados vários dispositivos da Apple e Samsung que eram bastante poderosos em termos gráficos, então nossa empresa teve a ideia de uma atualização significativa dos War Robots. Queríamos aproveitar o poder de todos esses novos dispositivos e atualizar a imagem visual do jogo.


Além da imagem atualizada, também tínhamos vários requisitos para nosso produto atualizado: queríamos oferecer suporte a um modo de jogo de 60 FPS, bem como diferentes qualidades de recursos para diferentes dispositivos.


O atual gerente de qualidade também exigiu um retrabalho, pois crescia o número de qualidades dentro dos blocos, que eram trocadas na hora, reduzindo a produtividade, além de criar um grande número de permutações, cada uma das quais precisava ser testada, o que impunha custos adicionais custos na equipe de controle de qualidade em cada versão.


A primeira coisa com que começamos ao retrabalhar o cliente foi refatorar a filmagem. A implementação original dependia da velocidade de renderização do quadro porque a entidade do jogo e sua representação visual no cliente eram o mesmo objeto. Decidimos separar essas entidades para que a entidade de jogo da cena não dependesse mais de seu visual, e isso permitiu um jogo mais justo entre dispositivos com diferentes taxas de quadros.


O jogo lida com um grande número de tiros, mas os algoritmos para processar os dados destes são bastante simples – mover-se, decidir se o inimigo foi atingido, etc. O conceito do Entity Component System foi perfeito para organizar a lógica do movimento do projétil. no jogo, então decidimos continuar.


Além disso, em todos os nossos novos projetos, já usávamos o ECS para trabalhar com lógica, e era hora de aplicar esse paradigma ao nosso projeto principal de unificação. Como resultado do trabalho realizado, percebemos um requisito fundamental – garantir a capacidade de executar imagens a 60 FPS. No entanto, nem todo o código de combate foi transferido para o ECS, pelo que o potencial desta abordagem não pôde ser plenamente realizado. No final das contas, não melhoramos o desempenho tanto quanto gostaríamos, mas também não o degradamos, o que ainda é um indicador importante com uma mudança de paradigma e arquitetura tão forte.


Ao mesmo tempo, começamos a trabalhar em nossa versão para PC para ver que nível de gráficos poderíamos alcançar com nossa nova pilha de gráficos. Começou a aproveitar o Unity SRP, que na época ainda estava na versão preview e em constante mudança. A imagem que saiu para a versão Steam foi bastante impressionante:



Além disso, alguns dos recursos gráficos mostrados na imagem acima poderiam ser transferidos para dispositivos móveis poderosos. Isso ocorreu especialmente para dispositivos Apple, que historicamente apresentam boas características de desempenho, drivers excelentes e uma combinação estreita de hardware e software; isto permite-lhes produzir uma imagem de altíssima qualidade sem quaisquer soluções temporárias da nossa parte.


Rapidamente entendemos que apenas alterar a pilha de gráficos não alteraria a imagem. Também ficou óbvio que, além de dispositivos poderosos, precisaríamos de conteúdo para dispositivos mais fracos. Assim, começamos a planejar o desenvolvimento de conteúdo para diferentes níveis de qualidade.


Isso significava que mechs, armas, mapas, efeitos e suas texturas tinham que ser separados em qualidade, o que se traduz em entidades diferentes para qualidades diferentes. Ou seja, os modelos físicos, texturas e conjunto de texturas de mechs que funcionarão em qualidade HD serão diferentes dos mechs que funcionarão em outras qualidades.


Além disso, o jogo dedica uma grande quantidade de recursos aos mapas, e estes também precisam ser refeitos para se adequarem às novas qualidades. Também ficou evidente que o atual Gerente de Qualidade não atendeu às nossas necessidades, pois não controlava a qualidade dos ativos.


Então, primeiro tivemos que decidir quais qualidades estariam disponíveis em nosso jogo. Tendo em conta a experiência do nosso actual Quality Manager, percebemos que na nova versão queríamos várias qualidades fixas que o utilizador pudesse alterar de forma independente nas configurações dependendo da qualidade máxima disponível para um determinado dispositivo.


O novo sistema de qualidade determina o dispositivo que o usuário possui e, com base no modelo do dispositivo (no caso do iOS) e no modelo da GPU (no caso do Android), permite-nos definir a qualidade máxima disponível para um jogador específico. Neste caso, esta qualidade, assim como todas as qualidades anteriores, está disponível.


Além disso, para cada qualidade, existe uma configuração para alternar o FPS máximo entre 30 e 60. Inicialmente, planejamos ter cerca de cinco qualidades (ULD, LD, MD, HD, UHD). Porém, durante o processo de desenvolvimento, ficou claro que essa quantidade de qualidades levaria muito tempo para ser desenvolvida e não nos permitiria reduzir a carga de controle de qualidade. Com isso em mente, acabamos com as seguintes qualidades no jogo: HD, LD e ULD. (A título de referência, a distribuição atual do público que joga com essas qualidades é a seguinte: HD - 7%, LD - 72%, ULD - 21%.)


Assim que entendemos que precisávamos implementar mais qualidades, começamos a trabalhar em como classificar os ativos de acordo com essas qualidades. Para simplificar esse trabalho, concordamos com o seguinte algoritmo: os artistas criariam ativos de qualidade máxima (HD) e, então, usando scripts e outras ferramentas de automação, partes desses ativos seriam simplificadas e usadas como ativos para outras qualidades.


No processo de trabalho neste sistema de automação, desenvolvemos as seguintes soluções:

  • Importador de ativos – permite definir as configurações necessárias para texturas
  • Mech Creator – ferramenta que permite a criação de diversas versões de mech baseadas em Unity Prefab Variants, bem como configurações de qualidade e textura, distribuídas nas pastas correspondentes do projeto.
  • Texture Array Packer – uma ferramenta que permite empacotar texturas em matrizes de textura
  • Quality Map Creator – uma ferramenta para criar múltiplas qualidades de mapa a partir de um mapa de origem com qualidade UHD


Muito trabalho foi feito para refatorar recursos existentes de mecanismos e mapas. Os mechas originais não foram projetados com qualidades diferentes em mente e, portanto, foram armazenados em pré-fabricados únicos. Após a refatoração, foram extraídas partes básicas deles, contendo principalmente sua lógica, e utilizando Prefab Variants, foram criadas variantes com texturas para uma qualidade específica. Além disso, adicionamos ferramentas que podem dividir automaticamente um mech em diferentes qualidades, levando em consideração a hierarquia das pastas de armazenamento de texturas dependendo das qualidades.


Para entregar ativos específicos para dispositivos específicos, foi necessário dividir todo o conteúdo do jogo em pacotes e pacotes. O pacote principal contém os ativos necessários para rodar o jogo, ativos que são usados em todas as capacidades, bem como pacotes de qualidade para cada plataforma: Android_LD, Android_HD, Android_ULD, iOS_LD, iOS_HD, iOS_ULD e assim por diante.


A divisão dos ativos em pacotes foi possível graças à ferramenta ResourceSystem, criada pela nossa equipe da Plataforma. Esse sistema nos permitiu coletar ativos em pacotes separados e depois incorporá-los ao cliente ou levá-los separadamente para carregá-los em recursos externos, como um CDN.


Para entregar o conteúdo, utilizamos um novo sistema, também criado pela equipe da plataforma, o chamado DeliverySystem. Este sistema permite receber um manifesto criado pelo ResourceSystem e baixar recursos da fonte especificada. Essa fonte pode ser uma compilação monolítica, arquivos APK individuais ou um CDN remoto.


Inicialmente, planejamos usar os recursos do Google Play (Play Asset Delivery) e AppStore (Recursos sob demanda) para armazenar pacotes de recursos, mas na plataforma Android houve muitos problemas associados às atualizações automáticas do cliente, bem como restrições em o número de recursos armazenados.


Além disso, nossos testes internos mostraram que um sistema de entrega de conteúdo com CDN funciona melhor e é mais estável, por isso abandonamos o armazenamento de recursos em lojas e começamos a armazená-los em nossa nuvem.


Liberando a remasterização

Quando o trabalho principal da remasterização foi concluído, chegou a hora do lançamento. No entanto, percebemos rapidamente que o tamanho original planejado de 3 GB era uma experiência de download ruim, apesar de vários jogos populares no mercado exigirem que os usuários baixassem de 5 a 10 GB de dados. Nossa teoria era que os usuários já estariam acostumados com essa nova realidade quando lançarmos nosso jogo remasterizado no mercado, mas, infelizmente.


Em outras palavras, os jogadores não estavam acostumados com esse tipo de arquivo grande para jogos mobile. Precisávamos encontrar uma solução para esse problema rapidamente. Decidimos lançar uma versão sem qualidade HD várias iterações depois disso, mas ainda assim a entregamos aos nossos usuários mais tarde.


Esquerda – antes da remasterização, direita – depois


Paralelamente ao desenvolvimento da remasterização, trabalhamos ativamente para automatizar os testes do projeto. A equipe de controle de qualidade fez um excelente trabalho garantindo que o status do projeto pudesse ser rastreado automaticamente. No momento, temos servidores com máquinas virtuais onde roda o jogo. Usando uma estrutura de teste escrita por nós mesmos, podemos pressionar qualquer botão no projeto com scripts – e os mesmos scripts são usados para executar vários cenários ao testar diretamente nos dispositivos.


Continuamos a desenvolver este sistema: centenas de testes em execução constante já foram escritos para verificar a estabilidade, o desempenho e a execução correta da lógica. Os resultados são exibidos em um painel especial onde nossos especialistas em controle de qualidade, juntamente com os desenvolvedores, podem examinar detalhadamente as áreas mais problemáticas (incluindo capturas de tela reais do teste).


Antes de colocar o sistema atual em operação, os testes de regressão demoravam muito (cerca de uma semana na versão antiga do projeto), que também era bem menor que o atual em termos de volume e quantidade de conteúdo. Mas graças aos testes automáticos, a versão atual do jogo é testada por apenas duas noites; isso pode ser melhorado ainda mais, mas atualmente estamos limitados pelo número de dispositivos conectados ao sistema.


Perto da conclusão da remasterização, a equipe da Apple nos contatou e nos deu a oportunidade de participar de uma apresentação de um novo produto para demonstrar as capacidades do novo chip Apple A14 Bionic (lançado com os novos iPads no outono de 2020) usando nosso novos gráficos. Durante o trabalho neste miniprojeto, foi criada uma versão HD totalmente funcional, capaz de rodar em chips Apple a 120 FPS. Além disso, diversas melhorias gráficas foram adicionadas para demonstrar o poder do novo chip.


Como resultado de uma seleção competitiva e bastante acirrada, nosso jogo foi incluído na apresentação de outono do Evento Apple! Toda a equipe se reuniu para assistir e comemorar esse evento, e foi muito legal!



Abandonando Photon e migrando para a arquitetura WorldState.

Antes mesmo do lançamento da versão remasterizada do jogo em 2021, uma nova tarefa surgiu. Muitos usuários reclamaram de problemas de rede, cuja raiz era nossa solução atual na época, a camada de transporte Photon, que era usada para trabalhar com o Photon Server SDK. Outro problema era a presença de um número grande e não padronizado de RPCs que eram enviados ao servidor em ordem aleatória.


Além disso, também ocorreram problemas de sincronização de mundos e transbordamento da fila de mensagens no início da partida, o que poderia causar lag significativo.


Para remediar a situação, decidimos mover a pilha de jogos da rede para um modelo mais convencional, onde a comunicação com o servidor ocorre através de pacotes e recebimento do estado do jogo, e não através de chamadas RPC.


A nova arquitetura de partida online foi chamada de WorldState e tinha como objetivo remover Photon do jogo.

Além de substituir o protocolo de transporte de Photon para UDP baseado na biblioteca LightNetLib, essa nova arquitetura também envolveu a agilização do sistema de comunicação cliente-servidor.


Como resultado do trabalho nesse recurso, reduzimos custos no lado do servidor (mudando do Windows para Linux e abandonando as licenças do Photon Server SDK), acabamos com um protocolo muito menos exigente para os dispositivos finais do usuário, reduzindo o número de problemas com dessincronização de estado entre o servidor e o cliente, e criou uma oportunidade para desenvolver novo conteúdo PvE.


Seria impossível alterar todo o código do jogo durante a noite, então o trabalho no WorldState foi dividido em várias etapas.


A primeira etapa foi uma reformulação completa do protocolo de comunicação entre o cliente e o servidor, e a movimentação dos mechs foi transferida para novos trilhos. Isso nos permitiu criar um novo modo de jogo: PvE. Gradualmente, a mecânica começou a migrar para o servidor, em particular as mais recentes (danos e danos críticos aos mechs). O trabalho na transferência gradual do código antigo para os mecanismos do WorldState continua e também teremos diversas atualizações este ano.


Em 2022, lançamos uma plataforma nova para nós: Facebook Cloud. O conceito por trás da plataforma era interessante: rodar jogos em emuladores na nuvem e transmiti-los para o navegador em smartphones e PCs, sem a necessidade do jogador final ter um PC ou smartphone potente para rodar o jogo; apenas uma conexão estável com a Internet é necessária.


Do lado do desenvolvedor, dois tipos de builds podem ser distribuídos como o principal que a plataforma utilizará: o build Android e o build Windows. Escolhemos o primeiro caminho devido à nossa maior experiência com esta plataforma.


Para lançar nosso jogo no Facebook Cloud, precisamos fazer diversas modificações, como refazer a autorização no jogo e adicionar o controle do cursor. Também precisávamos preparar um build com todos os recursos integrados porque a plataforma não suportava CDN, e precisávamos configurar nossas integrações, que nem sempre funcionavam corretamente em emuladores.


Muito trabalho também foi feito no lado gráfico para garantir a funcionalidade da pilha gráfica porque os emuladores do Facebook não eram dispositivos Android reais e tinham características próprias em termos de implementação de driver e gerenciamento de recursos.


No entanto, vimos que os usuários desta plataforma encontraram muitos problemas – tanto conexões instáveis quanto operação instável de emuladores, e o Facebook decidiu encerrar sua plataforma no início de 2024.




O que acontece constantemente por parte dos programadores e que não pode ser discutido detalhadamente em um artigo tão curto é o trabalho regular com os riscos técnicos do projeto, monitoramento regular das métricas técnicas, trabalho constante com memória e otimização de recursos, busca de problemas em soluções de terceiros de SDKs de parceiros, integrações de publicidade e muito mais.


Além disso, continuamos a pesquisa e o trabalho prático na correção de falhas críticas e ANRs. Quando um projeto é executado em um número tão grande de dispositivos completamente diferentes, isso é inevitável.


Separadamente, gostaríamos de destacar o profissionalismo das pessoas que trabalham para encontrar as causas dos problemas. Esses problemas técnicos são muitas vezes de natureza complexa e é necessário sobrepor dados de vários sistemas analíticos, bem como realizar alguns experimentos não triviais antes que a causa seja encontrada. Freqüentemente, os problemas mais complexos não possuem um caso consistentemente reproduzível que possa ser testado, portanto, dados empíricos e nossa experiência profissional são frequentemente usados para corrigi-los.


Algumas palavras devem ser ditas sobre as ferramentas e recursos que utilizamos para encontrar problemas em um projeto.


  • AppMetr – uma solução analítica interna que permite coletar dados anonimizados de dispositivos para estudar os problemas dos jogadores. Algumas das ferramentas semelhantes que podem ser mais conhecidas pelo leitor são: Firebase Analytics, Google Analytics, Flurry, GameAnalytics, devtodev, AppsFlyer, etc.
  • Google Firebase Crashlytics
  • Google Play Console – Android Vitals
  • Logs – um sistema de logs em builds de depuração, permitindo minimizar a busca por problemas em playtests dentro do estúdio
  • Public Test Realm – nosso jogo possui servidores de teste públicos que permitem testar novas mudanças e realizar vários experimentos técnicos
  • Criador de perfil de unidade
  • Instrumentos XCode


Esta é apenas uma pequena lista de melhorias técnicas que o projeto sofreu ao longo de sua existência. É difícil listar tudo o que foi realizado, pois o projeto está em constante desenvolvimento e os gestores técnicos trabalham constantemente na elaboração e implementação de planos para melhorar o desempenho do produto tanto para os usuários finais quanto para quem trabalha com ele dentro do estúdio, incluindo o os próprios desenvolvedores e outros departamentos.


Ainda temos muitas melhorias que o produto terá na próxima década e esperamos poder compartilhá-las em nossos próximos artigos.


Feliz aniversário, War Robots, e obrigado à enorme equipe de especialistas técnicos que tornam tudo isso possível!