Seja bem-vindo(a) ao GameDev: Aprendendo a programar jogos em Unity de hoje! Continuando o processo de desenvolvimento de nosso platformer 2D, hoje iremos aperfeiçoar o sistema de pulos que desenvolvemos anteriormente para permitir um controle satisfatório de nosso personagem principal em fases que contenham plataformas inclinadas, aumentando o leque de possibilidades na hora de criarmos o layout de nossos estágios.
Caso seja a primeira vez que você acessa esta série, sinta-se à vontade para juntar-se a nós nesta divertida trilha de aprendizado. Realizando diversas atividades práticas, você terá a oportunidade de aprender mais sobre o processo de desenvolvimento de jogos utilizando a ferramenta Unity: a partir do primeiro tópico aprenderemos desde a configuração do ambiente de programação em sua máquina até a prática de codificação dos games, montagem das cenas e dos outros elementos que compõem os projetos.
No momento, estamos desenvolvendo o jogo Motorista da Pesada, um platformer 2D em que o jogador deverá coletar, no menor tempo possível, diversas caixas de presente espalhadas pelos caminhos das fases. Para isso, ele guiará um simpático carrinho por diversas plataformas e deverá tomar muito cuidado tanto com perigosas bombas estrategicamente posicionadas pelos caminhos quanto com o decréscimo constante do tempo restante para a conclusão das tarefas, evitando assim chegar ao fatídico game over dessa aventura.
No tópico anterior de nossa jornada demos continuidade ao processo de construção da segunda fase do game, deixando-a definitivamente com características próprias de uma fase situada em uma densa floresta: novas plataformas, incluindo algumas em plano inclinado, mais objetos coletáveis e perigosas bombas espalhadas pelos cenários garantiram um acréscimo interessante no senso de perigo e aventura.
Temos importantes ajustes a fazer antes de concluirmos a elaboração do segundo estágio, então venha conosco e vamos juntos nesta jornada rumo a novos conhecimentos!
Restrições ao pulo
Um dos pontos a serem corrigidos que observamos ao final de nosso último encontro foi o fato de que, em determinadas plataformas inclinadas, nosso carrinho simplesmente não consegue pular. Veja o que (não) ocorre, no exemplo ilustrado a seguir:
Este comportamento ocorre devido ao modo como codificamos o script responsável pelo controle dos saltos de nosso personagem. Quando implementamos no script Pulo restrições para que os saltos pudessem ser realizados apenas se o carrinho estivesse posicionado sobre o chão ou uma plataforma, desenvolvemos alguns códigos para detectar se haveria alguma plataforma abaixo das rodas de nosso personagem principal e, em caso positivo, autorizar a execução de um salto.
Com o código inserido no script, inicialmente, buscou-se disparar dois raycasts com sentido para baixo (Vector3.down) a partir das posições estimadas do desenho das rodas do carrinho. As posições consideradas foram:
- Uma unidade horizontal antes da posição central do objeto (transform.position + new Vector3(-1, 0, 0)), representando a posição aproximada da “roda esquerda” do desenho do veículo; e
- Uma unidade horizontal depois da posição central do objeto (transform.position + new Vector3(1, 0, 0)), representando a posição aproximada da “roda direita” do desenho do veículo.
A imagem a seguir mostra, de maneira ilustrada, os posicionamentos utilizados para a emissão dos raycasts em direção ao chão:
Para situações como a apresentada na primeira fase de nosso game, em que nenhuma plataforma apresenta inclinação, a abordagem realizada é interessante e funciona bem, pois consegue captar pontos de colisão entre as rodas do objeto e a plataforma (ou o chão) de forma satisfatória. Porém, observe na ilustração a seguir o que ocorre quando se tenta calcular a posição aproximada das rodas com o carrinho e as plataformas apresentando inclinação simultaneamente:
A imagem anterior ilustra bem o que está ocorrendo:
- Devido à inclinação do carrinho, o posicionamento estimado das rodas (círculos azul e amarelo), realizado com a soma ou a subtração de uma unidade no eixo horizontal (X), não está mais sendo realizado de forma correta;
- A localização estimada de forma incorreta, em associação à inclinação que a plataforma também apresenta, faz com que, mesmo o carrinho estando sobre um objeto sólido, os raycasts lançados (representados pelas linhas brancas) não colidam com o componente Collider da plataforma (colisor representado pela caixa de cor verde-claro);
- Sem a detecção de elementos categorizados com o layer Piso imediatamente abaixo das posições estimadas das rodas, não é autorizada a execução do salto pelo personagem.
Como um dos novos desafios propostos para a segunda fase foi a implementação da inclinação nas plataformas, nada melhor do que realizarmos as alterações necessárias para suportar, também, um cenário dessa natureza.
Vamos, então, editar o script Pulo para resolvermos essa questão. Abra o Unity Hub e clique duas vezes sobre o item referente ao projeto Motorista da Pesada. Com a cena Fase02 aberta, na aba Project, abra a pasta Assets e, por fim, Scripts. Clique duas vezes sobre o ícone do script Pulo para que ele seja aberto para edição no Visual Studio.
Propriedades Size e Bounds
O componente SpriteRenderer, que estamos utilizando em Motorista da Pesada para exibição dos sprites pelos GameObjects que compõem nossas cenas, apresenta duas propriedades muito interessantes que irão nos auxiliar a resolver o desafio da estimativa correta de posicionamento das rodas no desenho do carrinho: são as propriedades size e bounds, que, utilizadas em conjunto, nos permitirão estimar melhor a partir de qual posição iremos lançar “raios raycast” para detecção de eventuais plataformas posicionadas abaixo do personagem principal.
No contexto de um jogo 2D, a propriedade size de um SpriteRenderer é responsável por informar as dimensões horizontal e vertical que correspondam ao tamanho global do sprite inserido na cena. A imagem a seguir ilustra o conceito: é como se fossem as medidas do “contorno” de um retângulo que envolve o sprite, mesmo que ele tenha sido rotacionado:
Já a propriedade bounds também tem relação com o espaço ocupado por um sprite em uma cena, mas tendo como “âncoras” os eixos horizontal e vertical da cena, e não as do sprite.
Na prática, a propriedade bounds age como se fosse um “retângulo virtual” ao redor de um sprite, desenhado por uma “régua virtual” apoiada nos eixos X e Y da cena. A partir disso, pode-se aferir diversos parâmetros (por exemplo, o tamanho desse bounds), acessando sua propriedade bounds.size.
Veja na ilustração a seguir a diferença entre as propriedades size (representada pelo retângulo de cor branca) e bounds (representada pelo retângulo de cor verde) de um determinado objeto contendo um componente SpriteRenderer:
Iremos utilizar a diferença entre os valores dessas duas propriedades para calcular o posicionamento estimado das rodas do carrinho, já considerando sua inclinação.
Calculando diferenças
Vamos iniciar esse processo inserindo, logo após a abertura do primeiro colchete da função void Update(), o seguinte trecho de código:
Vector2 size = gameObject.GetComponent<SpriteRenderer>().size;
Vector2 bounds_size = gameObject.GetComponent<SpriteRenderer>().bounds.size;
Vector2 diferenca = bounds_size - size;
A variável diferenca irá armazenar, a cada frame processado, a diferença entre o tamanho do sprite desenhado e do espaço ocupado de fato (bounds) pelo desenho na tela, tanto em termos horizontais (diferenca.x) quanto verticais (diferenca.y). A ilustração a seguir contém um exemplo gráfico do que estamos realizando no momento:
Logo após as linhas recém-inseridas no script, vamos introduzir o seguinte trecho de código:
int inclinacaoCarro;
if (transform.eulerAngles.z < (360 - transform.eulerAngles.z))
inclinacaoCarro = 1;
else
inclinacaoCarro = -1;
Esse código está realizando a verificação da inclinação corrente apresentada pelo carro, com base na rotação que o componente transform do GameObject está apresentando. Resumidamente, se o lado esquerdo da imagem de nosso colega motorizado estiver “tombando para baixo”, inclinacaoCarro terá valor positivo (1); caso contrário, inclinacaoCarro terá valor negativo (-1).
Isso será importante para calcularmos, posteriormente, se adicionaremos ou subtrairemos valores da posição vertical da estimativa de posicionamento das rodas na imagem de nosso personagem principal.
Vamos realizar agora as alterações no núcleo do código responsável pela detecção das colisões. No código em que realizamos a emissão do raycast hitRodaDir, altere o seguinte trecho:
- De: new Vector3(1, 0, 0)
- Para: new Vector3(1 - (diferenca.x / 2), diferenca.y / 2 * inclinacaoCarro, 0)
Já no código em que realizamos a emissão do raycast hitRodaEsq, altere o seguinte trecho:
- De: new Vector3(-1, 0, 0)
- Para: new Vector3(-1 + (diferenca.x / 2), -diferenca.y / 2 * inclinacaoCarro, 0)
Basicamente, realizamos com essas alterações as seguintes ações:
- Corrigimos a estimativa horizontal de posicionamento das rodas, ao removermos metade de diferenca.x dos deslocamentos originalmente considerados (X = 1 ou X = -1); e
- Corrigimos a estimativa vertical de posicionamento das rodas, que não estava sendo levada em conta anteriormente, ao considerarmos um deslocamento de metade de diferenca.y para cima ou para baixo, observando-se o "lado" do raycast (valor positivo para hitRodaDir, negativo para hitRodaEsq) e a inclinação observada (inclinacaoCarro).
Por fim, em ambos os trechos (emissão de hitRodaDir e de hitRodaEsq), altere o seguinte código:
- De: Vector3.down, 0.85f
- Para: transform.TransformDirection(Vector3.down), 0.95f
A utilização da função transform.TransformDirection(Vector3.down) em substituição a Vector3.down será importante para modificar o sentido de disparo dos raycasts respeitando a rotação do GameObject: continuarão sendo disparados “para baixo”, mas o ângulo do disparo levará em conta a inclinação do carro, como a imagem a seguir ilustra de maneira adequada.
Levando em conta as alterações realizadas, ajustamos também o alcance dos raycasts para que se aprofundem um pouquinho mais, de 0,85 para 0,95 unidade vertical.
Agora, finalmente, temos um sistema de pulos plenamente funcional também para fases com plataformas inclinadas.
Testando e pulando
Salve o script e volte ao Unity para testarmos o funcionamento (aba Game, botão Play) e observarmos algumas situações interessantes.
É possível observar na imagem anterior que, agora, nosso companheiro motorizado já consegue vencer a antiga “rampa impossível” e realizar os pulos que tanto queríamos que fossem executáveis. Mas existem outras situações que, agora, também ficaram mais fidedignas à “realidade”:
Veja que, na situação demonstrada na imagem anterior, nosso personagem principal continua sem conseguir pular. Porém, desta vez, faz total sentido, visto que as rodas não estão em contato próximo com o solo, estando o carro “dependurado” na plataforma pela sua parte central. Nesse caso, os raycasts emitidos corretamente não irão colidir com a plataforma, conforme ilustração a seguir:
Não se esqueça de encerrar a simulação, clicando novamente sobre o botão Play. Salve a cena (menu File, Save) e o projeto (menu File, Save Project) antes de fechar o Unity.
Próximos passos
Após conhecermos mais sobre as propriedades dos SpriteRenderers e implementarmos uma abordagem diferente para o controle da realização de pulos em planos inclinados, vencemos a ladeira da dificuldade e estamos mais próximos que nunca de concluirmos mais esta etapa de nosso projeto.
Em nosso próximo encontro, iremos finalizar a construção do segundo estágio para, enfim, darmos início à elaboração do derradeiro desafio de Motorista da Pesada.
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