O que você está procurando?
Games

Estratégias multijogo

LUCY ANNUNZIATA / UNITY TECHNOLOGIESContributor
Jul 17, 2024|9 undefined
Otimize o menu do jogo para carregamento mais rápido
Esta página da Web foi automaticamente traduzida para sua conveniência. Não podemos garantir a precisão ou a confiabilidade do conteúdo traduzido. Se tiver dúvidas sobre a precisão do conteúdo traduzido, consulte a versão oficial em inglês da página da Web.

Durante as revisões de projetos como consultor da equipe de sucesso do cliente, frequentemente trabalho com clientes que criam aplicativos inovadores. Esses aplicativos têm um menu principal ou menu temático, apresentando diversas opções de jogos para o jogador escolher. Nessas configurações, as principais preocupações são como garantir que o tempo entre as trocas de jogos seja o mais curto possível e como garantir o desempenho ideal em todos os jogos. Nesta postagem do blog, exploraremos diferentes abordagens com base nas necessidades do projeto, bem como algumas práticas recomendadas que podem ser úteis para qualquer ambiente de jogo, com ou sem uma configuração de troca de jogo.

Manipulando executáveis

Ao planejar um ambiente de múltiplas aplicações, seja para jogos, entretenimento ou simulação industrial, a decisão mais importante a tomar é como gerenciar os executáveis dos jogos. Existem muitos fatores que podem influenciar esta decisão:

  • Quantos jogos a plataforma suporta?
  • Qual é o tamanho dos jogos?
  • Os jogos são feitos com as mesmas versões do Unity ? Quais são os gargalos do aplicativo?
  • Outros fatores são hardware de destino, memória e CPU, e velocidade do disco (SSD vs HDD vs Cartão SD).

Responder a essas perguntas e decidir como lidar com executáveis é crucial para entender se precisamos de executáveis separados para cada jogo; um executável compartilhado para vários jogos ou uma combinação de ambos para garantir que os aplicativos tenham um desempenho ideal.

Ter vários executáveis ​​é uma ótima opção para lidar com jogos feitos com diferentes versões do Unity . Com essa abordagem, é possível reduzir o tempo de alternância entre jogos armazenando em cache o executável na memória e deixando cada instância em segundo plano. No entanto, manter todos os executáveis na memória nem sempre é a melhor escolha, pois pode sobrecarregar a memória. Deve ser evitado em casos em que os jogos individuais ocupam mais espaço na memória e/ou quando há muitos jogos no aplicativo de troca de jogos.

Para aliviar a restrição de memória, é possível que os jogos compartilhem um único executável. Os jogos podem estar em um único projeto do Unity , ou cada um ter seu próprio projeto, desde que os jogos compartilhem a mesma versão do Unity . Desde o Unity 2022 LTS no Windows, é possível usar o argumento -datafolder para passar um caminho de variável via linha de comando ( -datafolder <caminho_para_pasta> ), especificando a pasta de dados dos jogos selecionados para alternar a alteração. Uma possível desvantagem dessa abordagem é a lentidão na troca de jogos; portanto, é importante seguir as práticas recomendadas de carregamento para reduzir essa desvantagem.

Carregando as melhores estratégias

Não importa a natureza do jogo que estamos desenvolvendo ou em qual plataforma, é importante gastar o mínimo de tempo possível desde o momento da seleção do jogo até que ele esteja totalmente carregado na tela. Esse objetivo se torna particularmente importante para aplicações de troca de jogos.

Uma ótima maneira de lidar com o carregamento é usando Addressables. Com o Addressables, os conteúdos são baixados e liberados conforme a necessidade. Essa estratégia de carregamento diferido é a maneira mais eficiente de reduzir o tempo de carregamento dos jogos, pois limita a quantidade de dados que precisam ser carregados durante a inicialização. Além disso, pode ajudar a evitar quaisquer atividades em segundo plano da CPU relacionadas a jogos em segundo plano, o que pode contribuir para gargalos da CPU. Addressables: Planejamento e melhores práticas A postagem do blog é um ótimo ponto de partida para aprender mais sobre endereçáveis e como eles podem ajudar a melhorar seu jogo.

Uma ótima maneira de garantir um carregamento mais rápido, independentemente de quantos executáveis estamos usando, é por meio das APIs de carregamento assíncrono. Ao carregar de forma assíncrona, o thread principal do Unity executará um processo chamado “integração do thread principal”, que é responsável pela inicialização de objetos nativos e gerenciados de forma fragmentada. Como esse processo executa algumas operações que não são seguras para o thread, ele ocorrerá no thread principal, e o tempo permitido para executar a integração do thread principal é limitado para evitar que o jogo congele por muito tempo. A quantidade de tempo que pode ser gasta nas integrações é definida pela propriedade Application.backgroundLoadingPriority . Recomendamos definir backgroundLoadingPriority como Alto, ou 50 ms, durante as telas de carregamento e, em seguida, retorná-lo para Abaixo do Normal (4 ms) ou Baixo (2 ms) quando o carregamento estiver concluído.

Uma maneira adicional de acelerar o carregamento é por meio do Upload de Textura Assíncrona. O carregamento de textura assíncrona pode diminuir o tempo de carregamento ao coordenar quanto tempo e memória são usados para carregar texturas e malhas na configuração da GPU. A postagem do blog Compreendendo o pipeline de upload assíncrono fornece informações detalhadas sobre como esse processo funciona.

Essas práticas ajudarão a acelerar os tempos de carregamento:

  • Minimize o conteúdo da sua cena o máximo possível. Use uma cena de bootstrap para carregar apenas o que é necessário para que o jogo fique em um estado jogável e, em seguida, carregue cenas adicionais quando necessário.
  • Desative as câmeras durante as telas de carregamento.
  • Desabilite as telas de interface do usuário enquanto elas estiverem sendo preenchidas durante o carregamento.
  • Paralelize solicitações de rede.
  • Evite implementações complexas de Awake/Start e faça uso de threads de trabalho.
  • Sempre use compressão de textura.
  • Transmita grandes arquivos de mídia (como arquivos de áudio e texturas) em vez de mantê-los na memória.
  • Evite o serializador JSON e, em vez disso, use serializadores binários.
Uso de CPU em segundo plano

Como mencionado anteriormente, a memória não é a única preocupação em ambientes multijogos; a atividade da CPU em segundo plano também é algo que pode prejudicar a experiência de jogo do jogador. Quando os jogos não estão sendo jogados ativamente, a CPU ainda está em execução, fazendo com que o jogo ativo tenha um desempenho abaixo do ideal, criando privação de CPU. Uma maneira de evitar a falta de CPU no jogo ativo e em quaisquer outros processos de plataforma de backend é definir o player Executar em Segundo Plano como falso nas Configurações do Unity . Executar em segundo plano fará com que o loop do jogo Unity pare enquanto o jogo não estiver em foco. A configuração também pode ser alterada dinamicamente via script

public class ExampleClass : MonoBehaviour 
{
    void Example() 
    {
        Application.runInBackground = false;
    }
}

Uma coisa a ser observada é que a configuração Executar em Segundo Plano não impedirá a execução de nenhum thread de script personalizado, por isso é importante colocar para hibernar quaisquer threads de jogos que não estejam em execução por meio do método Thread.Sleep do C#. Lembre-se de que trabalhar com threads em segundo plano no Unity requer uma programação cuidadosa. Como esses threads não têm acesso direto à API do Unity, pode haver uma chance maior de criar problemas, como deadlocks e condições de corrida. Para evitar isso, é necessária a sincronização adequada com o thread principal do Unity . Para implementar corretamente o multithreading, revise a seção Limitações das tarefas async e await da página do manual Visão geral do .NET no Unity e o artigo do MSDN sobre o uso de threads e threading. O Unity 6 apresenta a classe Awaitable que oferece melhor suporte para async/await.

Evitando vazamentos de memória

Pode ser difícil e demorado identificar e corrigir as causas de vazamentos de memória, especialmente nos estágios posteriores do desenvolvimento. Por mais clichê que pareça, prevenir é sempre melhor que remediar. Aqui estão algumas recomendações que podem ajudar a evitar vazamentos em qualquer ambiente de jogo:

  1. Ao criar novos objetos/ativos na memória, certifique-se de excluí-los quando não forem necessários. Se usar o Addressable, certifique-se de liberar os ativos não utilizados.
  2. Ao carregar/descarregar cenas, os ativos devem ser removidos corretamente da memória. O Unity não descarrega ativos automaticamente quando um nível é descarregado, portanto é importante remover qualquer acesso à memória. A API Resources.UnloadUnusedAssets pode ajudar a limpar ativos. No entanto, ele pode causar picos de CPU, pois retorna um objeto que cede até que a operação seja concluída. Portanto, ele deve ser usado em locais que não sejam sensíveis ao desempenho.
  3. Evite usar frequentemente Instantiate e Destroy GameObjects. Isso pode levar a alocações gerenciadas desnecessárias, além de ser uma operação custosa de CPU. Entretanto, nos casos em que for necessário usar Destroy, certifique-se de remover todas as referências ao objeto para evitar Leaked Shell Objects. Quando um objeto ou seus pais são destruídos via Destroy, um código C# mantém uma referência a um Objeto Unity , mantendo o objeto wrapper gerenciado — seu Shell Gerenciado — na memória. Sua Memória Nativa será descarregada quando a Cena em que ela reside for descarregada, ou o GameObject ao qual ela está anexada ou seus pais forem destruídos via Destruir. Portanto, se algo que não foi descarregado ainda fizer referência a ele, a memória gerenciada pode continuar existindo como um Objeto de Shell Vazado.
  4. Tenha cuidado ao implementar eventos usando Singletons. Instâncias singleton mantêm referências a todos os objetos que assinaram seus eventos. Se esses objetos não durarem tanto quanto a instância singleton e não cancelarem a assinatura desses eventos, eles permanecerão na memória, causando um vazamento de memória. Se a origem do evento for descartada antes dos ouvintes, a referência será apagada e, se os ouvintes forem devidamente cancelados, também não haverá nenhuma referência restante. Para resolver e prevenir esse problema, recomendamos implementar o Weak Event Pattern ou IDisposable em todos os objetos que escutam eventos singleton e garantir que eles sejam descartados corretamente no seu código. O Weak Event Pattern é um padrão de design que ajuda você a gerenciar memória e coleta de lixo em programação orientada a eventos, especialmente quando se trata de objetos de longa duração. É especialmente útil quando você tem assinantes de curta duração, mas o editor tem vida longa. Tenha em mente que essas são soluções específicas do C# e funcionam apenas com eventos C# e não são diretamente suportadas pelo UnityEvents ou pelo Unity UI Toolkit. Por isso, recomendamos implementar essas soluções somente em seus scripts que não sejam MonoBehaviour .

Por fim, a criação de perfis, a execução de testes de CI/CD e testes de estresse desde os estágios iniciais de desenvolvimento podem economizar muito tempo, pois detectar vazamentos à medida que eles surgem permitirá que você resolva o problema prontamente, economizando tempo na depuração e garantindo o desempenho ideal.