O que você obterá nesta página: dicas de Michelle Martin, engenheira de software da MetalPop Games, sobre como otimizar jogos para uma variedade de dispositivos móveis, para que você possa alcançar o maior número possível de jogadores em potencial.
Com seu jogo de estratégia para dispositivos móveis, Galactic Colonies, a MetalPop Games enfrentou o desafio de possibilitar que os jogadores construíssem cidades enormes em seus dispositivos de baixo custo, sem que a taxa de quadros caísse ou o dispositivo superaquecesse. Veja como eles encontraram um equilíbrio entre um visual bonito e um desempenho sólido.
Por mais poderosos que os dispositivos móveis sejam atualmente, ainda é difícil executar ambientes de jogos grandes e de boa aparência com uma taxa de quadros sólida. Alcançar 60 fps sólidos em um ambiente 3D de grande escala em um dispositivo móvel antigo pode ser um desafio.
Como desenvolvedores, poderíamos simplesmente visar telefones de ponta e presumir que a maioria dos jogadores terá hardware suficiente para executar nosso jogo sem problemas. Mas isso resultará no bloqueio de um grande número de jogadores em potencial, pois ainda há muitos dispositivos mais antigos em uso. Todos esses são clientes em potencial que você não quer excluir.
Em nosso jogo, Galactic Colonies, os jogadores colonizam planetas alienígenas e constroem enormes colônias formadas por um grande número de edifícios individuais. Enquanto as colônias menores podem ter apenas uma dúzia de edifícios, as maiores podem facilmente ter centenas deles.
Essa era a nossa lista de objetivos quando começamos a desenvolver o pipeline:
- Queremos mapas enormes com muitas construções
- Queremos executar rapidamente em dispositivos móveis mais baratos e/ou antigos
- Queremos luzes e sombras com boa aparência
- Queremos um pipeline de produção fácil e sustentável
Uma boa iluminação em seu jogo é fundamental para que os modelos 3D tenham uma ótima aparência. No Unity, isso é fácil: configure seu nível, coloque suas luzes dinâmicas e pronto. E se você precisar ficar de olho no desempenho, basta preparar todas as suas luzes e adicionar um pouco de SSAO e outros elementos visuais por meio da pilha de pós-processamento. Pronto, envie-o!
Para jogos móveis, você precisa de um bom conjunto de truques e soluções alternativas para configurar a iluminação. Por exemplo, a menos que você tenha como alvo dispositivos de última geração, não deve usar nenhum efeito de pós-processamento. Da mesma forma, uma cena grande cheia de luzes dinâmicas também reduzirá drasticamente sua taxa de quadros.
A iluminação em tempo real pode ser cara em um PC de mesa. Em dispositivos móveis, as limitações de recursos são ainda maiores e nem sempre é possível oferecer todos os recursos interessantes que você gostaria de ter.
Portanto, você não quer esgotar as baterias dos telefones dos usuários mais do que o necessário com muitas luzes sofisticadas na cena.
Se você constantemente ultrapassar os limites do hardware, o telefone ficará quente e, consequentemente, será reduzido para se proteger. Para evitar isso, você pode assar todas as luzes que não projetam sombras em tempo real.
O processo de light baking é o pré-cálculo de realces e sombras para uma cena (estática), cujas informações são armazenadas em um mapa de luz. O renderizador sabe então onde tornar um modelo mais claro ou mais escuro, criando a ilusão de luz.
A renderização dessa forma é rápida porque todos os cálculos de luz caros e lentos foram feitos off-line e, em tempo de execução, o renderizador (shader) só precisa procurar o resultado em uma textura.
A desvantagem é que você terá que enviar algumas texturas de lightmap extras, o que aumentará o tamanho da compilação e exigirá alguma memória de textura extra em tempo de execução. Você também perderá espaço porque suas malhas precisarão de UVs de lightmap e ficarão um pouco maiores. Mas, de modo geral, você ganhará um enorme aumento de velocidade.
Mas para o nosso jogo isso não era uma opção, pois o mundo do jogo é construído em tempo real pelo jogador. O fato de que novas regiões são constantemente descobertas, novos edifícios são adicionados ou os já existentes são atualizados, impede qualquer tipo de distribuição eficiente de luz. Simplesmente apertar o botão Bake não funcionará quando você tiver um mundo dinâmico que pode ser constantemente alterado pelo jogador.
Portanto, enfrentamos uma série de problemas que surgem ao preparar luzes para cenas altamente modulares.
Os dados de light-baking no Unity são armazenados e diretamente associados aos dados da cena. Isso não é um problema se você tiver níveis individuais e cenas pré-construídas e apenas alguns objetos dinâmicos. Você pode pré-assar a iluminação e pronto.
Obviamente, isso não funciona quando você cria níveis dinamicamente. Em um jogo de construção de cidades, o mundo não é pré-criado. Em vez disso, ele é montado de forma dinâmica e imediata pela decisão do jogador sobre o que construir e onde construir. Isso geralmente é feito instanciando Prefabs onde quer que o jogador decida construir algo.
A única solução para esse problema é armazenar todos os dados relevantes de iluminação dentro do Prefab em vez de na cena. Infelizmente, não há uma maneira fácil de copiar os dados de qual lightmap usar, suas coordenadas e escala em um Prefab.
A melhor abordagem para obter um pipeline sólido que lide com Prefabs light baked é criar os Prefabs em uma cena diferente e separada (várias cenas, na verdade) e depois carregá-los no jogo principal quando necessário. Cada peça modular é preparada com luz e será carregada no jogo quando necessário.
Dê uma olhada de perto em como o light baking funciona no Unity e você verá que renderizar uma malha light baked é, na verdade, apenas aplicar outra textura a ela e clarear, escurecer (ou, às vezes, colorir) um pouco a malha. Tudo o que você precisa é da textura do mapa de luz e das coordenadas UV, que são criadas pelo Unity durante o processo de preparação da luz.
Durante o light baking, o processo Unity cria um novo conjunto de coordenadas UV (que apontam para a textura do mapa de luz) e um deslocamento e escala para a malha individual. O recozimento das luzes altera essas coordenadas a cada vez.
Para desenvolver uma solução para esse problema, é útil entender como os canais UV funcionam e como utilizá-los da melhor forma.
Cada malha pode ter vários conjuntos de coordenadas UV (chamados de canais UV no Unity). Na maioria dos casos, um conjunto de UVs é suficiente, pois as diferentes texturas (Diffuse, Spec, Bump etc.) armazenam as informações no mesmo local da imagem.
Mas quando os objetos compartilham uma textura, como um mapa de luz, e precisam procurar as informações de um local específico em uma textura grande, geralmente não há como adicionar outro conjunto de UVs para usar com essa textura compartilhada.
A desvantagem de várias coordenadas UV é que elas consomem mais memória. Se você usar dois conjuntos de UVs em vez de um, dobrará a quantidade de coordenadas de UV para cada um dos vértices da malha. Cada vértice agora armazena dois números de ponto flutuante, que são carregados para a GPU durante a renderização.
O Unity gera as coordenadas e o mapa de luz, usando a funcionalidade regular de light baking. O mecanismo gravará as coordenadas UV para o mapa de luz no segundo canal UV do modelo. É importante observar que o conjunto primário de coordenadas UV não pode ser usado para isso, pois o modelo precisa ser desembrulhado.
Imagine uma caixa com a mesma textura em cada um de seus lados: Todos os lados individuais da caixa têm as mesmas coordenadas UV, pois reutilizam a mesma textura. Mas isso não funcionará em um objeto mapeado por luz, pois cada lado da caixa é atingido por luzes e sombras individualmente. Cada lado precisa de seu próprio espaço no mapa de luz com seus dados de iluminação individuais. Por isso, a necessidade de um novo conjunto de UVs.
Para configurar um novo Prefab light-baked, tudo o que precisamos fazer é armazenar a textura e suas coordenadas para que não sejam perdidas e copiá-las para o Prefab.
Depois que o light baking é concluído, executamos um script que percorre todas as malhas da cena e grava as coordenadas UV no canal UV2 real da malha, com os valores de deslocamento e escala aplicados.
O código para modificar as malhas é relativamente simples (veja o exemplo abaixo).
Para ser mais específico: Isso é feito em uma cópia das malhas, e não no original, porque faremos mais otimizações em nossas malhas durante o processo de preparação.
As cópias são geradas automaticamente, salvas em um Prefab e recebem um novo material com um sombreador personalizado e o mapa de luz recém-criado. Isso deixa nossas malhas originais intactas, e os Prefabs leves ficam imediatamente prontos para uso.
Isso torna o fluxo de trabalho muito simples. Para atualizar o estilo e a aparência dos gráficos, basta abrir a cena apropriada, fazer todas as modificações até ficar satisfeito e, em seguida, iniciar o processo automatizado de bake-and-copy. Quando esse processo for concluído, o jogo começará a usar os Prefabs e malhas atualizados com a iluminação atualizada.
A textura real do mapa de luz é adicionada por um sombreador personalizado, que aplica o mapa de luz como uma segunda textura de luz ao modelo durante a renderização. O sombreador é muito simples e curto e, além de aplicar a cor e o mapa de luz, calcula um efeito de brilho/especular falso e barato.
Aqui está o código do shader; a imagem acima é de uma configuração de material usando esse shader.
Em nosso caso, temos quatro cenas diferentes com todos os Prefabs configurados. Nosso jogo apresenta diferentes biomas, como tropical, gelo, deserto etc., e dividimos nossas cenas de acordo com eles.
Todos os Prefabs usados em uma determinada cena compartilham um único mapa de luz. Isso significa uma única textura extra, além do fato de os Prefabs compartilharem apenas um material. Como resultado, conseguimos renderizar todos os modelos como estáticos e renderizar em lote quase todo o nosso mundo em apenas uma chamada de desenho.
As cenas de light baking, nas quais todos os nossos ladrilhos/edifícios estão configurados, têm fontes de luz adicionais para criar destaques localizados. Você pode colocar quantas luzes precisar nas cenas de configuração, já que todas elas serão queimadas de qualquer maneira.
O processo de bake é tratado em uma caixa de diálogo de interface do usuário personalizada que cuida de todas as etapas necessárias. Isso garante que:
- O material correto seja atribuído a todas as malhas
- Tudo o que não precisa ser cozido durante o processo fica oculto
- As malhas são combinadas/assadas
- Os UVs são copiados e os Prefabs são criados
- Tudo é nomeado corretamente e os arquivos necessários do sistema de controle de versão são verificados.
Prefabs com nomes apropriados são criados a partir das malhas para que o código do jogo possa carregá-los e usá-los diretamente. Os arquivos meta também são alterados durante esse processo, para que as referências às malhas dos Prefabs não se percam.
Esse fluxo de trabalho nos permite ajustar nossos edifícios o quanto quisermos, iluminá-los da maneira que preferirmos e deixar que o script cuide de tudo.
Quando voltamos à nossa cena principal e executamos o jogo, ele simplesmente funciona, sem necessidade de envolvimento manual ou outras atualizações.
Uma das desvantagens óbvias de uma cena em que 100% da iluminação é pré-fabricada é que é difícil ter objetos dinâmicos ou movimento. Qualquer coisa que lance uma sombra exigiria o cálculo de luz e sombra em tempo real, o que, obviamente, gostaríamos de evitar completamente.
Mas sem nenhum objeto em movimento, o ambiente 3D pareceria estático e morto.
Estávamos dispostos a conviver com algumas restrições, é claro, pois nossa principal prioridade era obter bons recursos visuais e renderização rápida. Para criar a impressão de uma colônia ou cidade viva e em movimento, não são necessários muitos objetos para se movimentar. E a maioria delas não exigia necessariamente sombras, ou pelo menos a ausência de sombras não seria notada.
Começamos dividindo todos os blocos de construção da cidade em dois Prefabs separados. Uma parte estática, que continha a maioria dos vértices, todas as partes complexas de nossas malhas, e uma parte dinâmica, que continha o menor número possível de vértices.
As partes dinâmicas de um Prefab são bits animados colocados sobre as partes estáticas. Eles não são iluminados e usamos um sombreador de iluminação falsa muito rápido e barato para criar a ilusão de que o objeto era iluminado dinamicamente.
Os objetos também não têm sombra ou criamos uma sombra falsa como parte da parte dinâmica. A maioria de nossas superfícies é plana, portanto, em nosso caso, isso não foi um grande obstáculo.
Não há sombras nas partes dinâmicas, mas isso é quase imperceptível, a menos que você saiba procurar por elas. A iluminação dos Prefabs dinâmicos também é falsa - não há iluminação em tempo real.
O primeiro atalho barato que tomamos foi codificar a posição da nossa fonte de luz (sol) no shader de iluminação falsa. É uma variável a menos que o sombreador precisa procurar e preencher dinamicamente a partir do mundo.
É sempre mais rápido trabalhar com uma constante do que com um valor dinâmico. Isso nos proporcionou iluminação básica, lados claros e escuros das malhas.
Para tornar as coisas um pouco mais brilhantes, adicionamos um cálculo de brilho/especular falso aos shaders para os objetos dinâmicos e estáticos. Os reflexos especulares ajudam a criar uma aparência metálica, mas transmitem a curvatura de uma superfície.
Como os realces especulares são uma forma de reflexão, o ângulo da câmera e da fonte de luz em relação um ao outro é necessário para calculá-lo corretamente. Quando a câmera se move ou gira, o especular muda. Qualquer cálculo de shader exigiria acesso à posição da câmera e a todas as fontes de luz na cena.
No entanto, em nosso jogo, temos apenas uma fonte de luz que estamos usando para especular: o sol. Em nosso caso, o sol nunca se move e pode ser considerado uma luz direcional. Podemos simplificar bastante o sombreador usando apenas uma única luz e assumindo uma posição fixa e um ângulo de entrada para ela.
Melhor ainda, nossa câmera em Galactic Colonies mostra a cena de uma visão de cima para baixo, como a maioria dos jogos de construção de cidades. A câmera pode ser inclinada um pouco e aumentar e diminuir o zoom, mas não pode girar em torno do eixo para cima.
Em geral, ele está sempre observando o ambiente de cima. Para fingir uma aparência especular barata, fingimos que a câmera estava completamente fixa e que o ângulo entre a câmera e a luz era sempre o mesmo.
Dessa forma, poderíamos novamente codificar um valor constante no shader e obter um efeito de especificação/brilho barato dessa forma.
É claro que usar um ângulo fixo para o especular é tecnicamente incorreto, mas é praticamente impossível perceber a diferença, desde que o ângulo da câmera não mude muito.
Para o jogador, a cena ainda parecerá correta, o que é o objetivo da iluminação em tempo real.
A iluminação de um ambiente em um videogame em tempo real é e sempre foi uma questão de parecer visualmente correta, em vez de ser fisicamente simulada corretamente.
Como quase todas as nossas malhas compartilham um único material, com muitos detalhes provenientes do mapa de luz e dos vértices, adicionamos um mapa de textura especular para informar ao sombreador quando e onde aplicar o valor de especificação e com que intensidade. A textura é acessada usando o canal UV primário, portanto, não requer um conjunto adicional de coordenadas. E como não há muitos detalhes, a resolução é muito baixa, ocupando pouco espaço.
Para alguns de nossos bits dinâmicos menores com uma contagem baixa de vértices, poderíamos até mesmo usar o batching dinâmico automático do Unity, acelerando ainda mais a renderização.
Todas essas sombras cozidas podem, às vezes, criar novos problemas, especialmente quando se trabalha com edifícios relativamente modulares. Em um caso, tínhamos um armazém que o jogador podia construir e que exibia o tipo de mercadoria armazenada no próprio edifício.
Isso causa problemas, pois temos um objeto de cozimento leve em cima de um objeto de cozimento leve. Uma exceção ao Lightbake!
Abordamos o problema usando mais um truque barato:
- A superfície em que o objeto adicional seria adicionado tinha que ser plana e usar uma cor cinza específica que combinasse com a construção base
- Nessa troca, podíamos fazer bake dos objetos em uma superfície plana menor e colocá-la sobre a área apenas com um leve deslocamento
- Luzes, destaques, brilho colorido e sombras eram todos colocados por bake no ladrilho
Construir e assar nossos pré-fabricados dessa forma nos permite ter mapas enormes com centenas de edifícios e, ao mesmo tempo, manter uma contagem superbaixa de draw calls. Todo o nosso mundo de jogo é mais ou menos renderizado com apenas um material e estamos em um ponto em que a interface do usuário usa mais draw calls do que o nosso mundo de jogo. Quanto menos materiais diferentes o Unity tiver que renderizar, melhor será o desempenho do seu jogo.
Isso nos deixa um amplo espaço para adicionar mais coisas ao nosso mundo, como partículas, efeitos climáticos e outros elementos atraentes.
Dessa forma, até mesmo os jogadores com dispositivos mais antigos podem construir grandes cidades com centenas de edifícios, mantendo um 60fps estável.