Engenharia Insper / Laboratório de Realidade Virtual e Jogos Digitais

Universo 2D Parte 02: Personagem

Agora que já temos um cenário, iremos criar um personagem, mais complexo que do projeto anterior. Existem diversas abordagens para isso, aqui temos uma proposta para que se entenda os conceitos de uma forma mais fácil

Objetivos de Aprendizagem

https://arks.itch.io/dino-characters 


Roteiro

Spritesheets

Chamamos de spritesheets uma imagem que conta com todas as posições da animação de um personagem, podendo inclusive conter outros elementos, como poderes, UI específicas e afins.

Veja imagem completa em https://www.spriters-resource.com/fullview/5150/ 

Assim como os tilesets, é possível encontrar spritesheets, em uma única imagem, ou ainda em diversas imagens. O processo não muda muito, apenas sendo necessário fatiar o spritesheet nas poses desejadas, como feito com o tileset.

Para organizar sua animação de personagem, crie uma pasta chamada Sprites, e dentro dessa pasta uma outra chamada MainChar, e então nessa pasta importe o spritesheet selecionado.

Assim como no caso do tileset, caso sua imagem seja única, será necessário fatiá-la. Para isso, selecione a imagem que acabou de importar, modifique o Sprite Mode para multiple, ajuste o Pixel Per unit, de acordo com a imagem. Abra o Sprite Editor, e selecione o modo de Slice que melhor se aplica a sua imagem.

Temos uma configuração especialmente importante para animações, que é o pivot, dos sprites que serão criados, a depender do tipo de jogo que será feito, ele deve ser posicionado em um lugar diferente. Para este momento escolha bottom, para verificar se o pivot está no lugar correto, selecione um dos sprites recém cortados, e um círculo deverá estar na posição do pivot.

Clique no apply no sprite editor para salvar suas alterações. Agora, assim como no tileset, um ícone de seta aparece ao lado do spritesheet.

Ao clicar nele, será aberta uma “gaveta” com todos os sprites que foram recortados.

Animações

Agora que temos sprites separados, podemos criar as animações que farão parte do nosso sistema.

O primeiro passo é selecionar um dos sprites para ser a representação do personagem no mundo. Para isso arraste esse sprite para a hierarquia.

E renomeie para MainCharacter.

Enfim podemos começar a criar nossas animações vá em Windows > Animation > Animation ou Ctrl/Cmd + 6

Irá abrir a janela de controle das animações.

Perceba a mensagem: To begin animating <Personagem Selecionado>, create as Animator and an Animation Clip.

Animator é a ferramenta da Unity que gerencia as animações e transições, Animation Clip é a animação de fato. Garanta que seu personagem esteja selecionado, e clique em Create. Isso irá criar os dois arquivos citados:

A janela animação terá mudado sua apresentação

Agora podemos criar todas as animações que iremos utilizar em nosso jogo, para isso clique no dropdown em que podemos selecionar as animações, que na imagem está “New Animation”

E clique em Create New Clip.

Para de fato criar a animação, seleciona as sprites, em sequência, que quer que faça parte da animação, e arraste para a linha do tempo do Animation Clip selecionado.

Sua janela deverá ficar como abaixo:

Clique em Preview, e no botão de play comum.

Sua animação provavelmente estará muito rápida, para ajustá-la clique no menu escondido, e selecione Show Sample Rate,

Ao fazer isso, uma nova caixa de texto irá aparecer na janela de animação.

Altere o valor de Samples até que sua animação fique com o efeito desejado, no caso do exemplo foi utilizado o valor 12.

Podemos seguir criando novas animações utilizando o mesmo processo. Mais adiante será abordado a integração de animações com código, mas por enquanto seguiremos com apenas uma.

Controlador

Nesse momento vamos criar o controlador do personagem. Para isso, crie um novo script com nome CharacterController2D.

Neste tutorial temos por objetivo passar um controlador genérico para que você entenda os conceitos utilizados.

Vamos precisar de algumas definições, que irão variar bastante dependendo do jogo que se está fazendo. Por hora vamos criar um atributo para a velocidade de locomoção do personagem

using UnityEngine;

public class CharacterController2D: MonoBehaviour {
   
   
public float speed = 10.0f;

}

Relembrando que quando definimos um atributo público em um Componente, teremos acesso a ele pelo inspector.

Podemos criar um C# Attribute para que o character controller tenha um RigidBody2D como pré-requisito. Isso irá garantir que tenha o componente necessário. https://docs.unity3d.com/ScriptReference/RequireComponent.html 

[RequireComponent(typeof(Rigidbody2D))]
public class CharacterController2D: MonoBehaviour {

Adicione esse componente que acabamos de criar em seu MainChararacter

Perceba o atributo Speed no inspector, e o Rigidbody2D automaticamente adicionado.

Na Start, vamos salvar a referência do Rigidbody2D que iremos utilizar para controlar o personagem.

    private Rigidbody2D rb;

   
private void Start() {
       rb = GetComponent<Rigidbody2D>();
   }

Para manter a Upadate legível, crie um método HandleMovement que será responsável por toda a movimentação do personagem.

public class CharacterController2D: MonoBehaviour {

   
private void Update() {
       HandleMovement();
   }

   
private void HandleMovement() {
       
//Todo
   }
}

Coloque o Gravity Scale do Rigidbody 2D em 0,

E vamos utilizar o Input.GetAxis para movimentar nosso personagem, a forma mais simples é criar um vetor normalizado com o valor de entrada dos eixos.

    private void HandleMovement() {

       
float hAxis = Input.GetAxis("Horizontal");
       
float vAxis = Input.GetAxis("Vertical");

       Vector3 direction =
new Vector3(hAxis, vAxis).normalized;

       rb.MovePosition(transform.position + direction * speed * Time.deltaTime);

   }

 

O personagem já está se movimentando pela tela, Adicione um BoxCollider2D para que as colisões sejam calculadas. E para testar se está funcionando crie um quadrado com um Colisor, dê o play e verifique se seu Controlador está funcionando.

Podemos utilizar o motor de Física da Unity para fazer algumas verificações utilizando o Raycast.  https://docs.unity3d.com/ScriptReference/Physics2D.Raycast.html 

O Raycast irá emitir um raio em uma direção específica, e caso tenha alguma colisão, poderemos acessar a informação dessa colisão a partir do retorno da função, que é um RaycastHit2D, no caso.

 private void FixedUpdate() {
           RaycastHit2D hit = Physics2D.Raycast(transform.position, transform.right,
2);
        }

No código acima criamos um Raycast que se inicia no pivot do personagem com direção para a direita, que é a frente do personagem no momento. Foi colocado ainda uma distância máxima de 2 unidades nesse teste.

Por muitas vezes é bastante difícil validar os raycasts, para isso podemos desenhar alguns elementos visuais para melhor visualização chamados de gizmos, esses elementos são apenas para facilitar o desenvolvimento do jogo e não aparecem na versão final.

private void OnDrawGizmos() {
           Gizmos.DrawRay(transform.position, transform.right *
2);
}

Com essa última adição podemos visualizar o raycast que estamos fazendo.

Prosseguindo com uso do Raycasthit.

private void FixedUpdate() {
           RaycastHit2D hit = Physics2D.Raycast(transform.position, transform.right,
2);

           
//Se houve colisão
           
if (hit.collider != null)
           {
               print(hit.collider.name);
           }
}

Podemos ainda utilizar layers para isolar os elementos que queremos verificar. https://docs.unity3d.com/ScriptReference/LayerMask.html Para isso vá em Edit->Settings->Tags and Layers

Temos 32 camadas disponíveis, sendo que as primeiras 8 são reservadas a Unity, então é uma boa prática utilizar as camadas a partir da camada no 8.

E podemos então utilizar essa camada em nosso código agora, e então o raycast só verificará apenas os GameObjects que estiverem nessa Layer.

Para colocar os GameObjects em uma layer específica, selecione-o na hierarquia, e logo acima do componente transform, temos dois dropdowns um de tags e outro de layers.

Selecione o de Layers e selecione a camada que acabou de criar.

E no código

            

LayerMask mascara = LayerMask.GetMask("Collectable");
           RaycastHit2D hit = Physics2D.Raycast(transform.position, transform.right,
2, mascara);

Vamos utilizar a tecla E para interagir com o quadrado e capturá-lo, para isso será necessário criar uma variável para “salvar” o objeto que estamos interagindo.

public class CharacterController2D: MonoBehaviour {
   
   
public float speed = 10.0f;
   
private Rigidbody2D rb;
   
//Vamos salvar o objeto que atingimos com o raycast aqui
   
private GameObject raycastTarget;
...

...

//Se houve colisão
       
if (hit.collider != null) {
           print(hit.collider.name);
           raycastTarget = hit.collider.gameObject;
       }
       
else { //Quando o raycast para de atingir o objeto limpo a referência.
           raycastTarget =
null;
       }

...

Agora criamos mais uma função para gerenciar os diversos input que não sejam referentes ao movimento.

    private void HandleInteractions() {
       
if (raycastTarget) {
           
if (Input.GetAxis("Interaction") != 0) {
               GameObject temp = raycastTarget;
               raycastTarget =
null;
               Destroy(temp);
           }
       }
   }

Precisamos ainda criar o eixo Interaction, para isso vá em Edit -> Project Settings -> Input Manager.

Adicione mais um no tamanho do vetor Axes

Altere o último elemento da seguinte forma

Por fim, precisamos montar o Animator com as animações e transições que iremos utilizar, e criar mais uma função responsável por controlar essas animações.

Abra o animator em Window -> Animation -> Animator

No animator podemos criar a máquina de estados de toda a animação, e definir as condições para as transições, que depois iremos gerenciar via código.

Crie um isMoving para o animator, para isso clique no símbolo de adição na aba parameters

E selecione bool

Agora conseguiremos definir as transições, clique com o botão direito em um dos animation clips, que você criou, e em seguida em Make Transition, feito isso, uma seta cinza começará a seguir o mouse, clique no Animation Clip que pretende fazer a transição

Podemos criar uma transição de volta do estado Walking para o Idle, lembre-se a transição sempre será a partir do estado que você começou a criar a transição.

Clicando em uma das setas, podemos ver suas propriedades no inspector.

Vamos colocar as condições para esta transição. Em conditions, clique no sinal de adição, para criar uma nova condição para essa transição.

Para esta animação, podemos ainda Desabilitar a opção Has Exit Time, e Zerar o Transition Duration.

E faremos o mesmo na transição de volta.

No código, precisaremos criar mais um atributo da classe para salvar a direção que o personagem está andando, e outro para o Anumator.

    private Vector3 movingDirection;

   private Animator animator;

Atualizamos a Start

   

private void Start() {
       rb = GetComponent<Rigidbody2D>();
       animator = GetComponent<Animator>();
       raycastTarget =
null;
   }

Atualizamos a HandleMovement

        movingDirection = new Vector3(hAxis, vAxis).normalized;

       rb.MovePosition(transform.position + movingDirection * speed * Time.deltaTime);

E por fim utilizamos essa informação para atualizar a animação

    private void HandleAnimation() {
       
if (movingDirection != Vector3.zero) {
           animator.SetBool(
"isMoving", true);
       }
       
else {
           animator.SetBool(
"isMoving", false);
       }
   }

Perceba como ficou a função Update de nosso Character Controller:

    private void Update() {
       HandleAnimation();
       HandleMovement();
       HandleInteractions();
   }

Criamos uma função específica para cada funcionalidade que precisaremos rodar na update, isso retoma o S dos princípios SOLID, em que cada uma dessas funções deverá ter uma responsabilidade única. Facilitando a manutenção do código a longo praso.