GameDev

Aprendendo a programar jogos em Unity: finalizando a construção de Motorista da Pesada

Após a realização de ajustes finais no projeto, concluiremos a criação do nosso primeiro platformer 2D.

em 30/03/2024
Seja bem-vindo(a) ao GameDev: Aprendendo a programar jogos em Unity de hoje! Após tantos aprendizados que realizamos nos últimos encontros, iremos finalmente concluir a construção de nosso primeiro platformer bidimensional.


Caso esta seja a primeira vez que você acessa nossa série, sinta-se à vontade para juntar-se a nós nesta divertida trilha de aprendizagem. Por meio do desenvolvimento de projetos práticos de codificação de jogos, vamos conhecer mais sobre o funcionamento da ferramenta Unity, suas características e como utilizá-la de forma satisfatória para criar games de diversos gêneros.

A partir do primeiro tópico, você terá acesso a conteúdos sobre os mais diferentes aspectos de programação de jogos, desde a instalação e configuração do Unity em seu computador até os tópicos que envolvem, de fato, a programação e a criação dos desafios que compõem um game, tais como o posicionamento dos objetos que compõem cenas e fases, a codificação de scripts controladores de comportamentos, dentre outros.

No momento, estamos desenvolvendo Motorista da Pesada, um platformer 2D cujo desafio proposto ao gamer é coletar diversas caixas de presente espalhadas pelas fases da forma mais rápida possível. Para isso, o jogador deverá escolher um veículo para percorrer os diferentes caminhos de três fases criadas para essa aventura. Além disso, ele deverá tomar cuidado com os diversos perigos que poderão encerrar precocemente sua missão, como bombas posicionadas pelos cenários, abismos sem fim e o implacável passo constante do cronômetro.


No tópico anterior de nossa série, começamos a implementar a funcionalidade de escolha do veículo que acompanhará o jogador pelos desafios de Motorista da Pesada, alterando alguns aspectos do menu inicial e implementando scripts de controle, para que o jogo possa interpretar corretamente a decisão do jogador. 

Hoje, após finalizarmos a implementação dessa funcionalidade e realizarmos alguns ajustes importantes no comportamento da movimentação no jogo, chegaremos ao fim do processo de criação de nosso segundo game desta série. Venha conosco e vamos juntos nesta jornada rumo a novos conhecimentos!

Novo personagem nas fases

Após codificarmos o script SelecionarPersonagem e alterarmos os elementos de MenuInicial para permitir a escolha do sprite do personagem principal, vamos adicionar esse novo elemento aos prefabs dos objetos que precisam ter seus sprites alterados em todas as fases do game.

Vamos iniciar esse procedimento abrindo o Unity Hub e clicando duas vezes sobre o item referente ao projeto Motorista da Pesada. Na aba Project, abra a pasta Assets e, por fim, Scenes. Clique duas vezes sobre o ícone da cena Fase02 (se preferir, pode abrir qualquer uma das outras cenas de fases que criamos).

Na aba Hierarchy, selecione o GameObject Personagem, subordinado a Cenario. Como o objeto que representa o personagem principal na fase é derivado de um prefab, se editarmos o prefab em si, todas as fases receberão as devidas correções e atualizações para o objeto em questão.

Na aba Inspector, mais especificamente na seção Prefab, clique sobre o botão Open, assim como demonstrado na imagem a seguir:

Perceba que, na aba Scene, a cena e os outros objetos são exibidos em um tom acinzentado, dando destaque apenas ao prefab. É possível notar esse destaque, também, pela composição da aba Hierarchy, que exibe apenas o objeto do prefab e seus objetos subordinados, caso existam. 

Com o objeto Personagem selecionado, na aba Inspector adicione um componente do tipo Selecionar Personagem. Seu parâmetro Personagem A deverá receber a imagem de nome “carrinho” e o parâmetro Personagem B deverá receber a imagem de nome “perua”. Confira, também, se a opção Auto Save está ativada, conforme exemplo a seguir:

Após realizar as alterações, na aba Hierarchy, clique na seta ao lado do título do prefab Personagem para voltarmos à cena Fase02 (em destaque, na imagem a seguir).


Além do personagem principal, também temos representações gráficas do veículo no contador de vidas restantes. Como ele também é um prefab, faremos algo bem semelhante para refletir a escolha do sprite pelo usuário.

Na aba Hierarchy, selecione o GameObject ContadorVidas, subordinado a CanvasFase. Na aba Inspector, mais especificamente na seção Prefab, clique sobre o botão Open.

Com o prefab em modo de edição, iremos selecionar três de seus objetos subordinados: Vida01, Vida02 e Vida03. Repita o que fizemos anteriormente para Personagem: adição de componentes do tipo Selecionar Personagem e concessão dos sprites de nome “carrinho” e “perua” para os parâmetros Personagem A e Personagem B, respectivamente.

Volte à cena Fase02, clicando na seta ao lado do título do prefab ContadorVidas, na aba Hierarchy.

Para finalizarmos a adição dessa funcionalidade em Motorista da Pesada, iremos realizar uma pequena alteração em parte do código do script SelecionarPersonagem. Para tal, na aba Project, abra a pasta Assets e, por fim, Scripts. Clique duas vezes sobre o ícone do referido script para realizarmos sua edição no Visual Studio.

Logo após a abertura do primeiro colchete da função void Start(), altere a linha que contém o código if (gameObject.name.StartsWith("Personagem")) para o conteúdo a seguir:

if (gameObject.name.StartsWith("Personagem") || gameObject.name.StartsWith("Vida"))

Essa alteração é necessária para que as mudanças de sprite não impactem apenas os objetos que tenham nome começando por “Personagem”, mas também os contadores de vidas restantes, cujos nomes de seus respectivos GameObjects começam com a palavra “Vida”.

Salve o script, minimize o Visual Studio e vamos voltar ao editor do Unity para prosseguirmos realizando os ajustes finais de funcionamento para nosso jogo.

Descongelando animação

Durante os testes de execução do game, depois de jogarmos uma partida, você pode ter percebido em algum momento que, após retornarmos ao menu inicial, independentemente se voltamos por meio do menu de pause ou pela tela de fim de partida, o elemento visual que indica qual veículo escolhemos para de realizar sua animação típica.


Esse comportamento ocorre pois, ao serem ativadas as telas de pause ou de fim de partida, estamos alterando via código o parâmetro Time.timeScale para zero, ou seja, “congelando o tempo”. Esse congelamento impacta, inclusive, a realização de animações, o que acaba interferindo no funcionamento de nosso menu inicial.

Para resolvermos essa questão, criaremos um script bem simples para fazer com que o tempo “volte a andar normalmente”, ao se carregar a cena de menu inicial. Primeiro, salve as alterações na cena Fase02, indo ao menu File e selecionando a opção Save. Posteriormente, na aba Project, abra a pasta Assets e, em seguida, Scenes. Clique duas vezes sobre o ícone que representa a cena MenuInicial.

Na aba Scene, agora iremos abrir a pasta Assets e, em seguida, Scripts. Clique com o botão direito do mouse em alguma área vazia da janela da pasta, selecione a opção Create e, em seguida, C# Script. Dê o nome de “RestaurarTime”, sem as aspas, para o script recém-criado e clique duas vezes sobre seu ícone para iniciarmos sua edição no Visual Studio.

Dentro dos colchetes da função void Start(), insira o seguinte trecho de código:

   Time.timeScale = 1;

Como iremos utilizar apenas a funcionalidade presente em void Start(), se desejar, pode apagar a declaração da função void Update().

Ao ser instanciado um objeto com esse script atrelado, a função void Start() será executada e o parâmetro timeScale do controlador interno de tempo (Time) do Unity voltará ao seu valor “normal”. Em outras palavras, no momento em que um objeto com um componente Restaurar Time “vier à vida” (em nosso caso concreto, ao se carregar a cena), o passo do tempo voltará ao normal, permitindo, dentre outros, que as animações sejam executadas corretamente.

Salve o script, minimize o Visual Studio e vamos voltar ao editor do Unity. Na aba Hierarchy, selecione o GameObject CanvasMenuInicial. Na aba Inspector, adicione um componente do tipo Restaurar Time. A partir de agora, ao se voltar de uma partida, as animações no menu inicial ocorrerão normalmente.

Chegamos a um momento muito importante agora, pois realizaremos a última modificação em nosso projeto antes de finalizarmos sua construção. Vamos em frente!

Um caminhar suave

As movimentações que realizamos durante a execução das fases, até o momento, somente são realizadas enquanto o jogador pressiona as teclas correspondentes ao deslocamento para esquerda ou para direita em seu teclado. Porém, sabemos que, na maioria dos jogos de plataforma, ao se interromper o pressionamento de uma tecla para movimentação de um personagem, não há a interrupção brusca do movimento, mas sim uma desaceleração gradual até a parada do personagem. 

Iremos implementar essa atualização em nosso modo de operar a movimentação de Motorista da Pesada por meio de edições no script MovFundo, responsável por esse comportamento. Na aba Project, ainda com a pasta Scripts aberta, clique duas vezes sobre o ícone do referido script.

A primeira etapa de alterações que iremos realizar diz respeito à variável direcao, que é utilizada no cálculo de movimentação para saber se o personagem irá se deslocar para a direita ou para a esquerda. Hoje ela está declarada dentro da estrutura de void Update(), o que faz com que, a cada frame executado, seu valor seja inicialmente zerado. 

Porém, a ideia para que a aceleração ou interrupção da movimentação do personagem seja gradual envolve aumentar ou reduzir o valor da variável também gradativamente. Então, inicialmente iremos remover a linha que contém o código "float direcao = 0;" de dentro da estrutura de void Update() para declarar a variável antes da referida função. Após a remoção da linha, iremos introduzir o seguinte trecho de código, posicionando-o logo após a declaração de audioPitch, assim como o exposto na imagem de exemplo a seguir:

   internal float direcao = 0;

A segunda etapa de alterações envolve condicionar a execução da funcionalidade de redução ou aceleração gradual de movimento apenas nos momentos em que o jogo não estiver pausado, ou seja, enquanto o valor de Time.timeScale for maior do que zero.

Atualmente, apenas a parte do código que determina se a movimentação é para a direita ou para a esquerda respeita essa condição, mas, como agora, mesmo sem pressionar teclas, teremos um “efeito inercial com atrito” para que o personagem possa ir desacelerando seu movimento aos poucos, é importante que ele não “saia andando por aí” durante os momentos de pausa do game. 

Dentro da função void Update(), remanejamos então todo o código que está fora dos colchetes de if (Time.timeScale > 0) para dentro, conforme indicação gráfica a seguir:

Agora, iremos de fato alterar o cálculo da movimentação do personagem. A ideia geral para a implementação do movimento gradual está descrita a seguir:
  • O valor da variável direcao receberá um acréscimo (ou decréscimo) em seu valor, proporcional ao tempo em que o jogador estiver com uma das teclas de movimentação pressionada, até que direcao chegue a um valor limite pré-estabelecido (‘-1’, em caso de movimentação à direita, ou ‘1’, em caso de movimentação à esquerda).
  • O valor de acréscimo (ou decréscimo) será equivalente a Time.deltaTime * 4, permitindo a aceleração gradual do personagem.
  • Logo após o cálculo realizado, independentemente de o gamer ter pressionado ou não teclas para movimentação do personagem, haverá um acréscimo (ou decréscimo) de valor para a variável direcao correspondente à metade do acréscimo calculável em caso de movimentação (ou seja, Time.deltaTime * 2).
  • Esse novo acréscimo (ou decréscimo) tenderá a levar o valor de direcao para zero. Como ele será executado mesmo sem haver o pressionamento de teclas para movimentação, na prática é o nosso “efeito inercial com atrito” em ação.
  • Pelo fato de o valor desse desconto ser equivalente à metade do que será somado (ou decrescido) à direcao em caso de movimentação, na prática o carrinho sairá de um estado parado até a aceleração máxima em até meio segundo e vice-versa.
Dadas as explicações conceituais, vamos pôr a mão na massa. Troque o seguinte código, presente no script logo após a abertura do primeiro colchete de if (Time.timeScale > 0), para o descrito em sua sequência:

De: 
            if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow))
                direcao = -1;
            else if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow))
                direcao = 1;

Para:
            if (Input.GetKey(KeyCode.D) || Input.GetKey(KeyCode.RightArrow))
                direcao = Mathf.Max(-1, direcao - (Time.deltaTime * 4));
            else if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.LeftArrow))
                direcao = Mathf.Min(1, direcao + (Time.deltaTime * 4));

Logo após o código recém-alterado, insira o conteúdo do seguinte trecho, responsável pelo "efeito inercial com atrito":

            if (direcao > 0)
                direcao = Mathf.Max(0, direcao - (Time.deltaTime * 2));
            else
                direcao = Mathf.Min(0, direcao + (Time.deltaTime * 2));

Por fim, para que a aceleração do ronco do motor do veículo também corresponda ao que implementamos agora, iremos trocar o trecho de código presente quase ao fim de void Update(), especificamente o contido dentro dos colchetes de if (audioAceleracao), para o trecho logo a seguir:

De:
   if (direcao != 0)
      audioPitch = Mathf.Min(3, audioPitch + Time.deltaTime);
   else
      audioPitch = Mathf.Max(1, audioPitch - Time.deltaTime);

   audioAceleracao.pitch = audioPitch;

Para:
   float novoPitch = (Mathf.Abs(direcao) * 2 + 1);

   if (novoPitch >= audioPitch)
      audioPitch = novoPitch;
   else audioPitch = Mathf.Max(1, audioPitch - (Time.deltaTime * 1.5f));

   audioAceleracao.pitch = audioPitch;

Essa correção no som levará ao aumento de seu parâmetro pitch em sintonia com a aceleração observada do veículo ao se movimentar, e a diminuição de pitch será efetuada de forma ligeiramente mais suave do que a desaceleração observada do personagem no cenário, mimetizando de forma mais semelhante à realidade o comportamento sonoro de um motor a combustão.

Por fim, salve o script, feche o Visual Studio e volte ao editor do Unity. Experimente a execução do game e se surpreenda com a movimentação bem mais fluida em todas as fases do jogo!

Gerando um executável

Chegou a tão esperada hora de gerarmos o executável de nosso segundo game, para podermos, de fato, apresentar os desafios de Motorista da Pesada ao mundo.

Primeiramente, devemos salvar as alterações realizadas na cena (menu File, opção Save) e no projeto (menu File, opção Save Project). Após isso, também no menu File, devemos escolher a opção Build Settings..., conforme exemplo ilustrado na imagem a seguir:


Na janela Build Settings, certifique-se de que todas as cenas do projeto estão na listagem de cenas a se incluir (com suas respectivas caixas de seleção marcadas), além de termos como primeira cena listada MenuInicial. Depois, selecione a opção PC, Mac & Linux Standalone e, por fim, clique no botão Build.


Assim como fizemos anteriormente para criar o executável de Forest Ping Pong, crie uma pasta em seu computador para armazenar os arquivos do jogo; por exemplo, “Meu jogo 02 - Motorista da Pesada”. Selecione-a e clique no botão Selecionar pasta.

Após aguardar o processo de compilação, que costuma demorar alguns minutos, se tudo ocorrer de forma satisfatória, a pasta em que seu jogo foi salvo será aberta. Basta clicar duas vezes sobre o arquivo “Motorista da Pesada.exe” para executar seu mais novo game.

Parabéns! Você acaba de criar seu segundo jogo!!!

Modelo para referência

Estão disponíveis no repositório GitHub do GameBlast os arquivos de uma versão já pronta (o “gabarito”) do projeto que desenvolvemos. Se quiser, baixe-os para comparar com o que você codificou em sua máquina.

Desta vez, os códigos disponibilizados no repositório não receberam melhorias em seu código, mas podem ser aperfeiçoados, de acordo com o que você considerar importante, em relação a boas práticas de programação ou composição de cenários e fases dentro do projeto Unity gerado.

Conclusão e próximos passos

Chegamos ao término de nosso segundo projeto, dentro de nossa trilha de aprendizado na programação de jogos em Unity. Caso esteja lendo este texto no computador, se quiser, pode testar o jogo diretamente de seu navegador para comparar o game com o que você desenvolveu.

Não há uma regra fixa sobre a lógica utilizada no desenvolvimento de um game. Em Motorista da Pesada, por exemplo, implementamos o game de forma que a movimentação do personagem não ocorresse “de fato”, mas sim por meio do deslocamento de outros elementos das fases, tais como o fundo, os itens e as plataformas, mantendo o objeto que representa o veículo e a câmera do jogo centralizados na tela.

Foi uma escolha deste projeto em específico, mas, no desenvolvimento de seus próprios jogos, você poderá implementar de formas diferentes, movimentando o personagem e a câmera e determinando limites para essa movimentação.

Diferentemente do primeiro projeto, que serviu como uma breve introdução à programação de jogos, desta vez nos aprofundamos em conceitos importantes do desenvolvimento de games, tais como a utilização de prefabs, sprites, scripts controladores de fase e de partida, aspectos de física de jogos, dentre diversos outros tópicos importantes. Gradativamente, estamos adquirindo novos conhecimentos, que poderão ser bem úteis no desenvolvimento de jogos futuros.

Agora, conte-nos como está sendo para você até o momento: o espaço dos comentários aqui no GameBlast está aberto. Aguardamos ansiosamente para ler o seu relato!

Em nossos próximos encontros abordaremos aspectos importantes para o desenvolvimento de novos jogos, envolvendo, por exemplo, a tridimensionalidade de ambientes, objetos e personagens. Posteriormente, iremos iniciar mais um projeto para colocarmos a mão na massa e gerarmos mais um interessante e divertido jogo. Por ora, aproveite para se divertir com seu mais novo título desenvolvido.

Nosso próximo texto já encontra-se disponível, continue conosco nessa jornada de conhecimento e fique ligado sempre aqui no GameBlast!

Revisão: Ives Boitano

Entendo videogames como sendo uma expressão de arte e lazer e, também, como uma impactante ferramenta de educação. No momento, doutorando em Sistemas da Informação pela EACH-USP, desenvolvendo jogos e sistemas desde 2020. Se quiser bater um papo comigo, nas redes sociais procure por @RodrigoGPontes.
Este texto não representa a opinião do GameBlast. Somos uma comunidade de gamers aberta às visões e experiências de cada autor. Escrevemos sob a licença Creative Commons BY-SA 3.0 - você pode usar e compartilhar este conteúdo desde que credite o autor e veículo original.