paint-brush
Como criar um controlador de personagem 2D no Unity: Parte 1por@deniskondratev
2,267 leituras
2,267 leituras

Como criar um controlador de personagem 2D no Unity: Parte 1

por Denis Kondratev15m2024/04/22
Read on Terminal Reader

Muito longo; Para ler

Este artigo se aprofunda no processo de criação de um controlador de personagem 2D no Unity, com foco na integração de um novo método `Slide` para `Rigidbody2D` que simplifica o movimento do personagem. Ele cobre configuração, comportamentos físicos, tratamento de colisões e restrições de movimento, fornecendo um guia básico para o desenvolvimento de um controlador de personagem que pode ser adaptado e estendido para vários jogos de plataforma 2D.
featured image - Como criar um controlador de personagem 2D no Unity: Parte 1
Denis Kondratev HackerNoon profile picture
0-item
1-item
2-item


Um dos elementos-chave de qualquer jogo de plataforma 2D é o personagem principal. A maneira como ele se move e é controlado molda significativamente a atmosfera do jogo – seja um jogo aconchegante da velha escola ou um jogo de terror dinâmico. Portanto, criar um controlador de personagem é um estágio inicial importante no desenvolvimento de um jogo de plataforma.


Neste artigo, examinaremos detalhadamente o processo de criação de um personagem do zero, ensinando-o a se mover pelo nível, respeitando as leis da física. Mesmo que você já tenha experiência na criação de controladores de personagem, terá interesse em aprender sobre as inovações do Unity 2023. Para minha surpresa, o tão esperado método Slide foi adicionado ao componente Rigidbody2D , o que simplifica muito a escrita de um controlador de personagem por permitindo o uso do Rigidbody2D no modo Kinematic de forma mais eficaz. Anteriormente, toda esta funcionalidade tinha que ser implementada manualmente.


Se você quiser não apenas ler o artigo, mas também experimentá-lo na prática, recomendo baixar um modelo de nível do repositório GitHub Treasure Hunters , onde já estão incluídos os assets necessários e um nível pronto para testar seu personagem.


Estabelecendo a Base para Nosso Caráter

Para o nosso jogo de plataforma, estabelecemos uma regra de que o jogo terá apenas superfícies verticais e horizontais, e a força da gravidade será direcionada estritamente para baixo. Isso simplifica significativamente a criação de um jogo de plataforma no estágio inicial, especialmente se você não quiser se aprofundar na matemática vetorial.


No futuro, posso me desviar dessas regras em meu projeto Treasure Hunters, onde exploro a criação de mecânicas para um jogo de plataforma 2D no Unity. Mas isso será assunto para outro artigo.


Com base na nossa decisão de que o movimento será realizado ao longo de superfícies horizontais, a base do nosso personagem será retangular. O uso de superfícies inclinadas exigiria o desenvolvimento de um colisor em forma de cápsula e mecânica adicional, como deslizamento.


Primeiro, crie um objeto vazio na cena e nomeie-o Capitão — este será nosso personagem principal. Adicione os componentes Rigidbody2D e BoxCollider2D ao objeto. Defina o tipo Rigidbody2D como Kinematic para que possamos controlar o movimento do personagem enquanto ainda utilizamos os recursos físicos integrados do Unity. Além disso, bloqueie a rotação do personagem ao longo do eixo Z ativando a opção Congelar rotação Z.


Sua configuração deve ser semelhante à ilustração abaixo.


Agora vamos adicionar uma aparência ao nosso capitão. Encontre a textura em Assets/Textures/Treasure Hunters/Captain Clown Nose/Sprites/Captain Clown Nose/Captain Clown Nose with Sword/09-Idle Sword/Idle Sword 01.png e defina seu valor de Pixel por unidade para 32. Freqüentemente, use este valor neste curso porque nossas texturas são criadas com esta resolução. Defina o Modo Sprite como Único e, por conveniência, defina o Pivô como Inferior. Não se esqueça de aplicar as alterações clicando no botão Aplicar. Certifique-se de que todas as configurações foram feitas corretamente.



Neste artigo, não abordaremos a animação de personagens, então, por enquanto, usaremos apenas um sprite. No objeto Captain, crie um objeto aninhado chamado Appearance e adicione um componente Sprite Renderer a ele, especificando o sprite configurado anteriormente.



Quando ampliei a imagem do capitão, percebi que ela estava bastante desfocada devido às configurações incorretas do sprite. Para corrigir isso, selecione a textura Idle Sword 01 na janela Project e defina o Filter Mode como Point (sem filtro). Agora a imagem parece muito melhor.


Configuração do Colisor de Personagens

No momento, a posição do colisor e a imagem do nosso capitão não coincidem, e o colisor acaba sendo grande demais para as nossas necessidades.

Vamos consertar isso e também posicionar corretamente o pivô do personagem na parte inferior. Aplicaremos esta regra a todos os objetos do jogo para facilitar sua colocação no mesmo nível, independentemente do tamanho.


Para facilitar o gerenciamento desse processo, use o conjunto Alternar posição da alça da ferramenta com pivô, conforme mostrado abaixo.


O próximo passo é ajustar o colisor do nosso herói para que seu ponto de articulação fique exatamente no centro e na parte inferior. O tamanho do colisor deve corresponder exatamente às dimensões do personagem. Ajuste os parâmetros Offset e Size do colisor, bem como a posição do objeto Appearance aninhado para obter a precisão necessária.


Preste atenção especial ao parâmetro Offset.X do colisor: seu valor deve ser estritamente 0. Isso garantirá o posicionamento simétrico do colisor em relação ao centro do objeto, o que é extremamente importante para rotações subsequentes do personagem para a esquerda e para a direita, onde o valor de Transform.Scale.X é alterado para -1 e 1. O colisor deve permanecer no lugar e a rotação visual deve parecer natural.


Introdução ao comportamento físico dos personagens

Em primeiro lugar, é essencial ensinar os personagens – sejam eles o herói principal, NPCs ou inimigos – a interagir com o mundo físico. Por exemplo, os personagens devem ser capazes de andar sobre uma superfície plana, pular dela e estar sujeitos à força da gravidade, puxando-os de volta ao chão.


O Unity possui um mecanismo de física integrado que controla o movimento dos corpos, lida com colisões e adiciona o efeito de forças externas aos objetos. Tudo isso é implementado usando o componente Rigidbody2D . No entanto, para personagens, é útil ter uma ferramenta mais flexível que interaja com o mundo físico e ao mesmo tempo dê aos desenvolvedores mais controle sobre o comportamento do objeto.


Como mencionei anteriormente, nas versões anteriores do Unity, os desenvolvedores tinham que implementar eles próprios toda essa lógica. Porém, no Unity 2023, um novo método Slide foi adicionado ao componente Rigidbody2D , permitindo o controle flexível do objeto físico ao mesmo tempo que fornece todas as informações necessárias sobre o movimento realizado.


Vamos começar criando uma classe CharacterBody , que conterá a lógica básica para mover personagens no mundo físico. Esta classe usará Rigidbody2D no modo Kinematic , que já adicionamos ao nosso personagem. Então, a primeira coisa é adicionar uma referência a este componente.


 public class CharacterBody : MonoBehaviour { [SerializeField] private Rigidbody2D _rigidbody; }


Às vezes, para a dinâmica do movimento do personagem, é necessário que a gravidade atue com mais força do que o normal. Para conseguir isso, adicionaremos um fator de influência da gravidade com valor inicial de 1, e esse fator não pode ser inferior a 0.


 [Min(0)] [field: SerializeField] public float GravityFactor { get; private set; } = 1f;


Também precisamos definir quais objetos serão considerados superfícies intransitáveis. Para isso, criaremos um campo que nos permitirá especificar as camadas necessárias.


 [SerializeField] private LayerMask _solidLayers;


Limitaremos a velocidade do personagem para evitar que ele desenvolva velocidades excessivamente altas, por exemplo, devido a alguma influência externa, incluindo a gravidade. Definimos o valor inicial como 30 e limitamos a capacidade de definir um valor menor que 0 no inspetor.


 [Min(0)] [SerializeField] private float _maxSpeed = 30;


Ao se mover ao longo de uma superfície, queremos que o personagem sempre se agarre a ela se a distância entre eles for suficientemente pequena.


 [Min(0)] [SerializeField] private float _surfaceAnchor = 0.01f;


Embora tenhamos decidido que as superfícies em nosso jogo seriam apenas horizontais ou verticais, por precaução, especificaremos o ângulo máximo de inclinação da superfície sobre a qual o personagem pode ficar estável, com um valor inicial de 45º.


 [Range(0, 90)] [SerializeField] private float _maxSlop = 45f;


Através do inspetor, também quero ver a velocidade atual do personagem e seu estado, então adicionarei dois campos com o atributo SerializeField .


 [SerializeField] private Vector2 _velocity; [field: SerializeField] public CharacterState State { get; private set; }


Sim, aqui apresentei uma entidade nova, mas indefinida, CharacterState . Discutiremos isso mais adiante.


Estados de personagem

Para simplificar o desenvolvimento do nosso jogo de plataformas, vamos definir apenas dois estados dos personagens principais.

O primeiro é Grounded , um estado em que o personagem fica seguro na superfície. Neste estado, o personagem pode mover-se livremente pela superfície e pular dela.


O segundo é Airborne , um estado de queda livre, em que o personagem fica no ar. O comportamento do personagem neste estado pode variar dependendo das especificidades do jogo de plataforma. Num caso geral próximo da realidade, o personagem se move sob a influência do impulso inicial e não pode afetar seu comportamento. No entanto, em plataformas, a física é muitas vezes simplificada em favor da conveniência e da dinâmica de jogo: por exemplo, em muitos jogos, mesmo em queda livre, podemos controlar o movimento horizontal do personagem. No nosso caso, isso também é possível, assim como a popular mecânica do salto duplo, que permite um salto adicional no ar.


Vamos representar os estados do nosso personagem no código:


 /// <summary> /// Describes the state of <see cref="CharacterBody"/>. /// </summary> public enum CharacterState { /// <summary> /// The character stays steady on the ground and can move freely along it. /// </summary> Grounded, /// <summary> /// The character is in a state of free fall. /// </summary> Airborne }


É importante notar que pode haver muitos mais estados. Por exemplo, se o jogo incluir superfícies inclinadas, o personagem pode estar em um estado de deslizamento onde não pode se mover livremente para a direita ou para a esquerda, mas pode deslizar pela encosta. Nesse estado, o personagem também pode pular, empurrando-se da encosta, mas apenas na direção da encosta. Outro caso possível é deslizar ao longo de uma parede vertical, onde a influência da gravidade é enfraquecida e o personagem pode empurrar horizontalmente.


Limitação de velocidade de movimento

Já definimos um campo privado _velocity , mas precisamos ser capazes de obter e definir esse valor de fora e, ao mesmo tempo, limitar a velocidade máxima do personagem. Isto requer a comparação do vetor de velocidade fornecido com a velocidade máxima permitida.


Isso pode ser feito calculando o comprimento do vetor velocidade ou, em termos matemáticos, sua magnitude. A estrutura Vector2 já contém uma propriedade magnitude que nos permite fazer isso. Assim, se a magnitude do vetor passado exceder a velocidade máxima permitida, devemos manter a direção do vetor, mas limitar a sua magnitude. Para isso, multiplicamos _maxSpeed pelo vetor de velocidade normalizado (um vetor normalizado é um vetor com a mesma direção, mas com magnitude 1).


Aqui está o que parece no código:


 public Vector2 Velocity { get => _velocity; set => _velocity = value.magnitude > _maxSpeed ? value.normalized * _maxSpeed : value; }


Agora vamos dar uma olhada em como a magnitude de um vetor é calculada. É definido pela fórmula:



Calcular a raiz quadrada é uma operação que consome muitos recursos. Embora na maioria dos casos a velocidade não ultrapasse o máximo, ainda temos que realizar esta comparação pelo menos uma vez por ciclo. No entanto, podemos simplificar significativamente esta operação se compararmos o quadrado da magnitude do vetor com o quadrado da velocidade máxima.


Para isso, introduzimos um campo adicional para armazenar o quadrado da velocidade máxima, e calculamos uma vez no método Awake :


 private float _sqrMaxSpeed; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; }


Agora, a configuração da velocidade pode ser realizada de forma mais otimizada:


 public Vector2 Velocity { get => _velocity; set => _velocity = value.sqrMagnitude > _sqrMaxSpeed ? value.normalized * _maxSpeed : value; }


Assim, evitamos cálculos desnecessários e melhoramos o desempenho do processamento da velocidade de movimento do personagem.


Método de movimento de corpo rígido

Como mencionei anteriormente, o Unity adicionou um novo método Slide() , que simplificará bastante o desenvolvimento do nosso CharacterBody . Porém, antes de utilizar este método, é necessário definir as regras pelas quais o objeto se moverá no espaço. Esse comportamento é definido pela estrutura Rigidbody2D.SlideMovement .


Vamos apresentar um novo campo _slideMovement e definir seus valores.


 private Rigidbody2D.SlideMovement _slideMovement; private void Awake() { _sqrMaxSpeed = _maxSpeed * _maxSpeed; _slideMovement = CreateSlideMovement(); } private Rigidbody2D.SlideMovement CreateSlideMovement() { return new Rigidbody2D.SlideMovement { maxIterations = 3, surfaceSlideAngle = 90, gravitySlipAngle = 90, surfaceUp = Vector2.up, surfaceAnchor = Vector2.down * _surfaceAnchor, gravity = Vector2.zero, layerMask = _solidLayers, useLayerMask = true, }; }



É importante explicar que maxIterations determina quantas vezes um objeto pode mudar de direção como resultado de uma colisão. Por exemplo, se o personagem estiver no ar próximo a uma parede e o jogador tentar movê-lo para a direita enquanto a gravidade atua sobre ele. Assim, para cada chamada ao método Slide() , será definido um vetor de velocidade direcionado para a direita e para baixo. Ao atingir a parede, o vetor de movimento é recalculado e o objeto continuará a se mover para baixo.


Em tal situação, se o valor maxIterations fosse definido como 1, o objeto atingiria a parede, pararia e ficaria efetivamente preso ali.


Os valores para maxIterations e layerMask foram definidos anteriormente. Para informações mais detalhadas sobre os demais campos, consulte a documentação oficial da estrutura .


Finalmente, movendo o personagem

Agora está tudo pronto para colocar o Capitão em movimento. Faremos isso em FixedUpdate — um retorno de chamada no Unity projetado para lidar com a física. Nos últimos anos, a equipe do Unity melhorou significativamente o tratamento da física 2D. Atualmente, o processamento pode ser feito no callback Update ou mesmo chamando o método necessário por conta própria.


No entanto, neste exemplo, usaremos o método tradicional e comprovado FixedUpdate . Antes de prosseguir, vale mencionar algumas palavras sobre o valor de Time.fixedDeltaTime .


Para garantir a previsibilidade na física do jogo, a simulação é realizada em iterações em intervalos de tempo fixos. Isso garante que alterações no FPS ou atrasos não afetem o comportamento do objeto.


No início de cada ciclo, levaremos em conta o efeito da gravidade no objeto. Como a gravidade é dada pelo vetor de aceleração de queda livre, podemos calcular a mudança na velocidade Δv do objeto ao longo do tempo Δt pela fórmula:




onde a é a aceleração constante do objeto. No nosso caso, é a aceleração da gravidade, considerando o coeficiente que introduzimos — Physics2D.gravity * GravityFactor . Portanto, Δv pode ser calculado da seguinte forma:


 Time.fixedDeltaTime * GravityFactor * Physics2D.gravity


O resultado final, onde alteramos a velocidade, fica assim:


 Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity;


Agora podemos realizar o movimento de corpo rígido do personagem:


 var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement);


A variável slideResults é um valor da estrutura SlideResults e armazena os resultados do movimento. Os principais campos deste resultado para nós são slideHit , o resultado da colisão com a superfície durante o movimento, e surfaceHit — o resultado de um lançamento para baixo, que ajudará a determinar se o personagem está em uma superfície estável.


Lidando com Colisões

Ao colidir com superfícies, é crucial limitar a velocidade do personagem direcionada a essa superfície. Um exemplo simples é se o personagem estiver parado no chão, ele não deverá continuar a ganhar velocidade sob a influência da gravidade. Ao final de cada ciclo, sua velocidade deve ser zero. Da mesma forma, ao subir e atingir o teto, o personagem deve perder toda a velocidade vertical e começar a descer.


Os resultados das colisões, slideHit e surfaceHit , são representados por valores da estrutura RaycastHit2D , que inclui a normal da superfície de colisão.


A limitação de velocidade pode ser calculada subtraindo a projeção do vetor velocidade original na normal de colisão do próprio vetor velocidade. Isso é feito usando o produto escalar . Vamos escrever um método que realizará esta operação:


 private static Vector2 ClipVector(Vector2 vector, Vector2 hitNormal) { return vector - Vector2.Dot(vector, hitNormal) * hitNormal; }


Agora vamos integrar esse método ao nosso FixedUpdate . Aqui, para surfaceHit , só limitaremos a velocidade se ela for direcionada para baixo, pois o cast que determina se o objeto está na superfície é sempre realizado para verificar o contato com o solo.


 private void FixedUpdate() { Velocity += Time.fixedDeltaTime * GravityFactor * Physics2D.gravity; var slideResults = _rigidbody.Slide( _velocity, Time.fixedDeltaTime, _slideMovement); if (slideResults.slideHit) { _velocity = ClipVector(_velocity, slideResults.slideHit.normal); } if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; _velocity = ClipVector(_velocity, surfaceHit.normal); } }


Esta implementação permite gerenciar corretamente o movimento do personagem, evitando acelerações indesejadas ao colidir com diversas superfícies e mantendo o movimento dos personagens no jogo previsível e suave.


Determinando o estado do personagem

Ao final de cada ciclo, é necessário determinar se o personagem está em uma superfície sólida (estado Aterrado) ou em queda livre (ou, como definimos, queda controlada – estado Aerotransportado).


Para considerar o personagem como estando no estado Grounded, principalmente, sua velocidade vertical deve ser zero ou negativa, que determinamos pelo valor de _velocity.y .


Outro critério importante é a presença de uma superfície sob os pés da personagem, que identificamos a partir dos resultados do movimento Rigidbody, nomeadamente através da presença de surfaceHit .


O terceiro fator é o ângulo de inclinação da superfície, que analisamos com base na normal desta superfície, ou seja, o valor de surfaceHit.normal . É necessário comparar este ângulo com _maxSlop — o ângulo máximo possível da superfície na qual o personagem pode ficar estável.


Para uma superfície completamente vertical, a normal será estritamente horizontal, ou seja, seu valor vetorial será (1, 0) ou (-1, 0). Para uma superfície horizontal, o valor da normal será (0, 1). Quanto menor o ângulo de inclinação, maior o valor de y . Para o ângulo alpha , este valor pode ser calculado como:



Como nosso ângulo é dado em graus e a função $\cos$ requer radianos, a fórmula é transformada em:



Para isso, vamos introduzir um novo campo e calculá-lo no método Awake .


 private float _minGroundVertical; private void Awake() { _minGroundVertical = Mathf.Cos(_maxSlop * Mathf.PI / 180f); //... }


Agora vamos atualizar nosso código em FixedUpdate , verificando todas as condições acima.


 if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); State = surfaceHit.normal.y >= _minGroundVertical ? CharacterState.Grounded : CharacterState.Airborne; } else { State = CharacterState.Airborne; }


Essa lógica nos permitirá determinar com precisão quando o personagem está no solo e responder corretamente às mudanças em seu estado.


Adicionando CharacterBody ao Capitão

Agora que nosso componente CharacterBody está pronto, a etapa final é adicioná-lo ao nosso Captain. Na cena, selecione o objeto Captain e adicione o componente CharacterBody a ele.

Não se esqueça de configurar o Rigidbody conforme mostrado na ilustração acima. Defina o Fator de Gravidade como 3 e selecione a opção Padrão para Camada Sólida.


Agora você pode iniciar o jogo e experimentar definir diferentes valores de velocidade para observar como nosso personagem se move pela cena.

Concluindo por enquanto

Claro, ainda precisamos adicionar controles de personagens. Porém, este artigo já se tornou bastante extenso, então irei detalhar o controle do personagem usando o novo Sistema de Entrada no próximo artigo: “Criando um Controlador de Personagem 2D no Unity: Parte 2”.


Você pode baixar o projeto completo descrito neste artigo aqui: Caçadores de Tesouro e conferir tudo na prática caso encontre alguma dificuldade. O desenvolvimento de um controlador de personagem é um aspecto fundamental na criação de um jogo de plataformas 2D, pois determina o desenvolvimento futuro do jogo. Afeta a facilidade com que novos recursos serão adicionados ao comportamento do herói principal ou dos inimigos. Portanto, é muito importante entender o básico para poder desenvolver seu próprio jogo de forma independente.