
Oi Habr! Nesta publicação, quero compartilhar a experiência de desenvolver um jogo móvel massivo, com uma cidade grande e tráfego. Os exemplos e técnicas descritas na publicação não pretendem ser chamados de referência e ideal. Não sou um especialista certificado e não desejo repetir minha experiência. O objetivo do jogo era obter experiência interessante, obter um jogo otimizado com um mundo aberto. Durante o desenvolvimento, tentei simplificar o código o máximo possível. Infelizmente, eu não usei ECS, mas pequei com singleton.
O jogo
Um jogo sobre o tema da máfia. No jogo, tentei recriar a América 30-40. Essencialmente, um jogo é uma estratégia econômica em primeira pessoa. O jogador captura o negócio e tenta mantê-lo à tona.
Implementado: tráfego de carros (semáforos, prevenção de colisões), tráfego humano, bar, cassino, clube, apartamento do jogador, compra de um terno, troca de terno, compra / pintura / reabastecimento, policiais, segurança / bandidos, economia, recursos de venda / compra.
Arquitetura

Lamento não ter usado o ECS, mas tentei andar de bicicleta. No final, tudo acabou sendo complicado e muito dependente. O aplicativo possui um ponto de entrada - o objeto de jogo do aplicativo (go), no qual a classe Application com o mesmo nome está pendurada. Ele é responsável pelo pré-carregamento do banco de dados, preenchendo pools e configurações iniciais. Além disso, várias outras classes de componentes do gerenciador singleton ficam sobre os ombros do aplicativo (go).
- Audiomanager
- UIManager
- Inputmanager
Fanaticamente, tentei criar uma arquitetura na qual eu pudesse gerenciar vários componentes do gerente. Por exemplo, o AudioManager gerencia todos os sons, o UIManager contém todos os elementos e métodos da interface do usuário para gerenciamento. Todas as entradas são processadas através do InputManager usando eventos e delegados.
AudioManager simplificado. Permite adicionar tantos componentes de áudio ao objeto do jogo e, se necessário, reproduzir som:
public class AudioManager : MonoBehaviour { public static AudioManager instance = null;
Na inicialização, o método AddAudio adiciona um componente e, em qualquer lugar, podemos reproduzir o som que precisamos:
AudioManager.instance.isMetalHit = true;
Neste exemplo, seria mais sensato colocar a reprodução em execução no método.
Como é um InputManager simplificado:
public class InputManager : MonoBehaviour { public static InputManager instance = null; public float horizontal, vertical; public delegate void ClickAction(); public static event ClickAction OnAimKeyClicked;
Coloquei o método
AimKeyDown no
botão e assino o script de controle de armas no OnAimKeyClicked:
InputManager.instance.OnAimKeyClicked += GunShot;
Todo o meu sistema de entrada é implementado de maneira semelhante. Não notei nenhum problema com a velocidade. Isso nos permitiu coletar todos os manipuladores de cliques em um único local - o InputManager.
Otimização
Vamos para o mais interessante. Para iniciantes, o tópico de otimização do Unity é doloroso e cheio de muitas armadilhas. Vou compartilhar com o que estava lidando.
1. Armazenamento em cache de componentes (comece com noções básicas simples)Muitas vezes, no Toster, você pode encontrar perguntas com exemplos de quando, onde GetComponent é usado na atualização. Você não pode fazer isso, GetComponent está procurando um componente no objeto. Essa operação é lenta e, na atualização, você corre o risco de perder o precioso FPS. Aqui está uma boa explicação do
armazenamento em cache do
componente .
2. Usando o SendMessageUsar SendMessage () é mais lento que GetComponent (). SendMessage percorre cada script para encontrar o método com o nome desejado usando a comparação de cadeias. GetComponent localiza o script através da comparação de tipos e chama o método diretamente.
3. Comparação de tags de objetoUse o método CompareTag em vez de obj.tag == "string". No Unity, a extração de cadeias de objetos de jogos cria uma cadeia duplicada, que adiciona trabalho ao coletor de lixo. É melhor evitar o nome do objeto do jogo. Você não pode chamar o CompareTag na atualização nem ler operações pesadas.
4. MateriaisQuanto menos materiais, melhor. Reduza a quantidade de materiais possível. Para conseguir isso, ajude a textura acetinada. Por exemplo, quase toda a cidade do meu jogo é composta de 2 a 3 atlas. Note-se que nem todos os dispositivos móveis são capazes de trabalhar com grandes atlas. Portanto, se você deseja oferecer suporte a dispositivos de 11 a 13 anos, vale a pena considerar. Decidi recusar o suporte para o Android abaixo da versão 5.1, pois esses dispositivos são na sua maioria antigos. Além disso, o jogo roda no OpenGL 3.x por causa da renderização linear.
5. FísicaÉ fácil reduzir o FPS para 10. Acontece que mesmo objetos estáticos interagem e participam de cálculos. Eu pensei erroneamente que objetos físicos estáticos (objetos que possuem um componente RigidBody) são completamente passivos sob demanda. Fui desviado pelo antigo tutorial, que dizia que onde quer que haja um colisor, deve haver RigidBody. Agora todos os meus objetos estáticos são Static + BoxCollider. Onde eu preciso de física, por exemplo, postes de iluminação que podem ser derrubados, acho que cortarei o componente RigidBody, se necessário.
Camadas são a tábua de salvação para otimização. Desative a interação desnecessária usando camadas. Ao reformular, use máscaras de camada. Por que precisamos de erros de cálculo extras? Lembre-se de que se o seu objeto tiver uma grade colisor complexa e você atirar nele com um raio, é melhor criar um colisor pai simples para "capturar" os raios. Quanto mais complexo o colisor, mais erros de cálculo.
6. Seleção de oclusão + LodEm uma cena ampla, o abate por oclusão é indispensável. Para desativar objetos (árvores, postes, etc.) a uma grande distância, eu uso o Lod.

7. Pool de ObjetosTodas as implementações prontas do pool de objetos que eu achei usam instanciar. Eles também excluem e criam objetos. Eu tenho medo de instanciar todas as suas manifestações. Operação lenta, que congela o jogo, com um objeto mais ou menos grande. Decidi seguir um caminho simples e rápido - toda a minha piscina existe na forma de objetos físicos de jogos que eu apenas desligo e ligo se necessário. Ele atinge a RAM, mas é melhor. RAM para dispositivos modernos de 1 GB, o jogo consome 300-500 MB.
Pool simples para gerenciar bots de combate:
public List<Enemy> enemyPool = new List<Enemy>(); private void Start() {
Banco de Dados
Eu uso o sqlite como um banco de dados - de forma conveniente e rápida. Os dados são apresentados na forma de uma tabela, você pode fazer consultas complexas. Na classe para trabalhar com o banco de dados, 800 linhas quando. Não consigo imaginar como ficaria em XML / JSON.
Problemas e planos para o futuro
Para passar da cidade para as "salas", escolhi a implementação de "teleporte". O jogador se aproxima da porta, a sala de cena é carregada e o jogador é teleportado. Isso evita que você tenha que manter quartos na cidade. Se você implementar salas na cidade, com mais de 15 salas com enchimento, o consumo de memória aumentará para um mínimo de 1 GB. Não gosto desta implementação, não é realista e impõe um monte de restrições. Unity recentemente mostrou uma demonstração do seu
Megacity , é impressionante. Quero transferir gradualmente o jogo para esc e usar a tecnologia da Megacity para carregar edifícios e instalações. Esta é uma experiência fascinante e interessante, acho que será uma cidade verdadeiramente vibrante. Por que não usei
cena de carregamento assíncrono ? É simples, não funciona, não há cena de carregamento assíncrona pronta para uso na versão 2018.3. Inicialmente, eu esperava uma cena de carregamento assíncrono ao planejar uma cidade, mas, como se vê, em cenas grandes, congela o jogo como uma cena de carregamento regular. Isso foi confirmado no fórum do Unity, você pode se locomover, mas são necessárias muletas.
Algumas estatísticas:
Texturas: 304 / 374.3 MB
Malhas: 295 / 304.0 MB
Materiais: 101 / 148,0 KB (provável discrepância aqui)
Clipes de animação: 24 / 2.8 MB
Clipes de áudio: 22 / 30,3 MB
Ativos: 21761
Objetos de Jogo em Cena: 29450
Total de objetos em cena: 111645
Contagem total de objetos: 133406
Alocações de GC por quadro: 70 / 2.0 KBUm total de 4800 linhas de código C #.
Alguém me disse que esse jogo pode ser feito em uma semana. Talvez eu não seja produtivo, talvez essa pessoa seja talentosa, mas eu entendi uma coisa: é difícil criar esses jogos sozinhos. Eu queria criar algo interessante no contexto de “dedos” casuais, parece-me que me aproximei do meu sonho.
Você pode testar a versão beta aberta e senti-la aqui:
play.google.com/store/apps/details?id=com.ag.mafiaProject01 (se a montagem não funcionar, você precisa adorá-la um pouco, as atualizações chegam todas as noites). Espero que isso não seja considerado um link de publicidade, pois esta versão beta e os downloads não me trarão uma classificação e dividendos. Além disso, não acho que habr seja o público-alvo do meu jogo.
Imagens:

