Neste artigo, continuamos desenvolvendo um controlador de personagem para um jogo de plataforma 2D no Unity, examinando detalhadamente cada etapa da configuração e otimização dos controles.
No artigo anterior, “ Como criar um controlador de personagem 2D no Unity: Parte 1 ”, discutimos em detalhes como criar a fundação do personagem, incluindo seu comportamento físico e movimento básico. Agora, é hora de passar para aspectos mais avançados, como manipulação de entrada e acompanhamento dinâmico de câmera.
Neste artigo, vamos nos aprofundar na configuração do novo sistema de entrada do Unity, criando ações ativas para controlar o personagem, permitindo pulos e garantindo respostas adequadas aos comandos do jogador.
Se você quiser implementar todas as mudanças descritas neste artigo você mesmo, você pode baixar o branch do repositório “ Character Body ”, que contém a base para este artigo. Alternativamente, você pode baixar o branch “ Character Controller ” com o resultado final.
Antes de começarmos a escrever código para controlar nosso personagem, precisamos configurar o sistema de entrada no projeto. Para nosso platformer, escolhemos o novo Input System da Unity, introduzido há alguns anos, que continua relevante devido às suas vantagens sobre o sistema tradicional.
O Sistema de Entrada oferece uma abordagem mais modular e flexível ao manuseio de entrada, permitindo que os desenvolvedores configurem facilmente controles para vários dispositivos e ofereçam suporte a cenários de entrada mais complexos sem sobrecarga de implementação adicional.
Primeiro, instale o pacote Input System. Abra o Package Manager no menu principal selecionando Window → Package Manager. Na seção Unity Registry, encontre o pacote "Input System" e clique em "Install".
Em seguida, vá para as configurações do projeto através do menu Edit → Project Settings. Selecione a aba Player, encontre a seção Active Input Handling e defina-a como "Input System Package (New).
Após concluir essas etapas, o Unity solicitará que você reinicie. Uma vez reiniciado, tudo estará pronto para configurar os controles para nosso capitão.
Na pasta Settings , crie Input Actions por meio do menu principal: Assets → Create → Input Actions . Nomeie o arquivo como "Controls".
O Input System da Unity é uma ferramenta de gerenciamento de entrada poderosa e flexível que permite que os desenvolvedores configurem controles para personagens e elementos do jogo. Ele suporta vários dispositivos de entrada. As Ações de Entrada que você cria fornecem gerenciamento de entrada centralizado, simplificando a configuração e tornando a interface mais intuitiva.
Clique duas vezes no arquivo Controles para abri-lo para edição e adicione um Mapa de Ação para controle de personagem chamado "Personagem".
Um Action Map no Unity é uma coleção de ações que podem ser vinculadas a vários controladores e teclas para executar tarefas específicas no jogo. É uma maneira eficiente de organizar controles, permitindo que os desenvolvedores aloquem e ajustem entradas sem reescrever o código. Para mais detalhes, consulte a documentação oficial do Input System .
A primeira ação será chamada de "Move". Essa ação definirá a direção do movimento do personagem. Defina o Action Type como "Value" e o Control Type como "Vector2" para habilitar o movimento em quatro direções.
Atribua vinculações a esta ação selecionando Adicionar composição para cima/baixo/direita/esquerda e atribuindo as teclas WASD conhecidas às suas respectivas direções.
Não esqueça de salvar suas configurações clicando em Save Asset . Essa configuração garante que você possa reatribuir vinculações para a ação "Move", por exemplo, para teclas de seta ou até mesmo um joystick de gamepad.
Em seguida, adicione uma nova ação — "Pular". Mantenha o Tipo de Ação como "Botão", mas adicione uma nova Interação — "Pressionar", e defina o Comportamento do Gatilho como "Pressionar e Soltar", pois precisamos capturar o pressionamento e a liberação do botão.
Isso completa o esquema de controle de caracteres. O próximo passo é escrever um componente para lidar com essas ações.
É hora de vincular as Ações de Entrada que criamos para o controle do personagem ao componente CharacterBody
, permitindo que o personagem se mova ativamente pela cena de acordo com nossos comandos de controle.
Para fazer isso, criaremos um script responsável pelo controle de movimento e o nomearemos CharacterController
para maior clareza. Neste script, primeiro definiremos alguns campos básicos. Adicionaremos uma referência ao componente CharacterBody
, _characterBody
, que será controlado diretamente pelo script.
Também definiremos parâmetros para a velocidade de movimento do personagem ( _speed
) e altura do salto ( _jumpHeight
). Além disso, definiremos a finalidade do campo _stopJumpFactor
.
Você pode ter notado que em muitos jogos de plataforma 2D, a altura do salto pode ser controlada. Quanto mais tempo o botão de salto for pressionado, mais alto o personagem salta. Essencialmente, uma velocidade ascendente inicial é aplicada no início do salto, e essa velocidade é reduzida quando o botão é liberado. O _stopJumpFactor
determina o quanto a velocidade ascendente diminui ao liberar o botão de salto.
Aqui está um exemplo do código que escreveremos:
// CharacterController.cs public class CharacterController : MonoBehaviour { [SerializeField] private CharacterBody _characterBody; [Min(0)] [SerializeField] private float _speed = 5; [Min(0)] [SerializeField] private float _jumpHeight = 2.5f; [Min(1)] [SerializeField] private float _stopJumpFactor = 2.5f; }
Em seguida, implementaremos a habilidade de mover o personagem para a esquerda e para a direita. Ao segurar o botão de movimento, o personagem deve manter a velocidade de movimento especificada, independentemente dos obstáculos. Para conseguir isso, adicionaremos uma variável no script para armazenar a velocidade de movimento atual ao longo da superfície (ou simplesmente horizontalmente quando o personagem estiver no ar):
// CharacterController.cs private float _locomotionVelocity;
No componente CharacterBody
, introduziremos um método para definir essa velocidade:
// CharacterBody.cs public void SetLocomotionVelocity(float locomotionVelocity) { Velocity = new Vector2(locomotionVelocity, _velocity.y); }
Como nosso jogo não apresenta superfícies inclinadas, esse método é bem simples. Em cenários mais complexos, precisaríamos levar em conta o estado do corpo e a inclinação da superfície. Por enquanto, simplesmente preservamos o componente vertical da velocidade enquanto modificamos apenas a coordenada horizontal x
.
Em seguida, definiremos esse valor no método Update
em cada quadro:
// CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); }
Definiremos um método para manipular sinais da ação Move
Input:
// CharacterController.cs public void OnMove(InputAction.CallbackContext context) { var value = context.ReadValue<Vector2>(); _locomotionVelocity = value.x * _speed; }
Como a ação Move
é definida como um Vector2
, o contexto fornecerá um valor de vetor dependendo de quais teclas são pressionadas ou liberadas. Por exemplo, pressionar a tecla D
resultará no método OnMove
recebendo o vetor (1, 0). Pressionar D
e W
simultaneamente resultará em (1, 1). Liberar todas as teclas acionará OnMove
com o valor (0, 0).
Para a tecla A
, o vetor será (-1, 0). No método OnMove
, pegamos o componente horizontal do vetor recebido e o multiplicamos pela velocidade de movimento especificada, _speed
.
Primeiro, precisamos ensinar o componente CharacterBody
a lidar com o pulo. Para fazer isso, adicionaremos um método responsável pelo pulo:
// CharacterBody.cs public void Jump(float jumpSpeed) { Velocity = new Vector2(_velocity.x, jumpSpeed); State = CharacterState.Airborne; }
No nosso caso, esse método é simples: ele define a velocidade vertical e imediatamente muda o estado do personagem para Airborne
.
Em seguida, precisamos determinar a velocidade na qual o personagem deve pular. Já definimos a altura do salto e sabemos que a gravidade atua constantemente no corpo. Com base nisso, a velocidade inicial do salto pode ser calculada usando a fórmula:
Onde h é a altura do salto, e g é a aceleração gravitacional. Também levaremos em conta o multiplicador de gravidade presente no componente CharacterBody
. Adicionaremos um novo campo para definir a velocidade inicial do salto e calculá-la da seguinte forma:
// CharacterController.cs private float _jumpSpeed; private void Awake() { _jumpSpeed = Mathf.Sqrt(2 * Physics2D.gravity.magnitude * _characterBody.GravityFactor * _jumpHeight); }
Precisaremos de outro campo para rastrear se o personagem está saltando no momento, para que possamos limitar a velocidade do salto no momento apropriado.
Além disso, se o jogador segurar o botão de pulo até pousar, devemos redefinir esse sinalizador nós mesmos. Isso será feito no método Update
:
// CharacterController.cs private bool _isJumping; private void Update() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = false; } //... }
Agora, vamos escrever o método para manipular a ação Jump
:
// CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { Jump(); } else if (context.canceled) { StopJumping(); } }
Como a ação Jump
é um botão, podemos determinar a partir do contexto se o pressionamento do botão começou ( context.started
) ou terminou ( context.canceled
). Com base nisso, iniciamos ou paramos o salto.
Aqui está o método para executar o salto:
// CharacterController.cs private void Jump() { if (_characterBody.State == CharacterState.Grounded) { _isJumping = true; _characterBody.Jump(_jumpSpeed); } }
Antes de pular, verificamos se o personagem está no chão. Se sim, definimos o flag _isJumping
e fazemos o corpo pular com o _jumpSpeed
.
Agora, vamos implementar o comportamento de parar de pular:
// CharacterController.cs private void StopJumping() { var velocity = _characterBody.Velocity; if (_isJumping && velocity.y > 0) { _isJumping = false; _characterBody.Velocity = new Vector2( velocity.x, velocity.y / _stopJumpFactor); } }
Paramos o salto somente se o sinalizador _isJumping
estiver ativo. Outra condição importante é que o personagem deve estar se movendo para cima. Isso evita limitar a velocidade de queda se o botão de salto for liberado enquanto se move para baixo. Se todas as condições forem atendidas, reiniciamos o sinalizador _isJumping
e reduzimos a velocidade vertical por um fator de _stopJumpFactor
.
Agora que todos os componentes estão prontos, adicione os componentes PlayerInput
e CharacterController
ao objeto Captain na cena. Certifique-se de selecionar o componente CharacterController
que criamos, não o componente Unity padrão projetado para controlar personagens 3D.
Para o CharacterController
, atribua o componente CharacterBody
existente do personagem. Para o PlayerInput
, defina os Controls criados anteriormente no campo Actions
.
Em seguida, configure o componente PlayerInput para chamar os métodos apropriados do CharacterController. Expanda as seções Events e Character no editor e vincule os métodos correspondentes às ações Move e Jump.
Agora, tudo está pronto para rodar o jogo e testar como todos os componentes configurados funcionam juntos.
Agora, precisamos fazer a câmera seguir o personagem para onde quer que ele vá. A Unity fornece uma ferramenta poderosa para gerenciamento de câmera — Cinemachine .
Cinemachine é uma solução revolucionária para controle de câmera no Unity que oferece aos desenvolvedores uma ampla gama de capacidades para criar sistemas de câmera dinâmicos e bem ajustados que se adaptam às necessidades do jogo. Esta ferramenta facilita a implementação de técnicas complexas de câmera, como acompanhamento de personagem, ajuste automático de foco e muito mais, adicionando vitalidade e riqueza a cada cena.
Primeiro, localize o objeto Câmera Principal na cena e adicione o componente CinemachineBrain
a ele.
Em seguida, crie um novo objeto na cena chamado CaptainCamera . Esta será a câmera que segue o capitão, assim como um cinegrafista profissional. Adicione o componente CinemachineVirtualCamera
a ele. Defina o campo Follow para o capitão, escolha Framing Transposer para o campo Body e defina o parâmetro Lens Ortho Size para 4.
Além disso, precisaremos de outro componente para definir o deslocamento da câmera em relação ao personagem — CinemachineCameraOffset
. Defina o valor Y como 1,5 e o valor Z como -15.
Agora, vamos testar como a câmera segue nosso personagem.
Acho que ficou bem legal. Notei que a câmera ocasionalmente gagueja um pouco. Para consertar isso, configurei o campo Blend Update Method do objeto Main Camera para FixedUpdate.
Vamos testar a mecânica atualizada. Tente correr e pular continuamente. Jogadores experientes podem notar que os pulos nem sempre são registrados. Na maioria dos jogos, isso não é um problema.
Acontece que é difícil prever o momento exato do pouso para pressionar o botão de pulo novamente. Precisamos tornar o jogo mais tolerante, permitindo que os jogadores pressionem o pulo levemente antes de pousar e que o personagem pule imediatamente após o pouso. Esse comportamento se alinha com o que os jogadores estão acostumados.
Para implementar isso, introduziremos uma nova variável, _jumpActionTime
, que representa a janela de tempo durante a qual um salto ainda pode ser acionado se surgir a oportunidade.
// CharacterController.cs [Min(0)] [SerializeField] private float _jumpActionTime = 0.1f;
Adicionei um campo _jumpActionEndTime
, que marca o fim da janela de ação de pulo. Em outras palavras, até que _jumpActionEndTime
seja atingido, o personagem pulará se a oportunidade surgir. Vamos também atualizar o manipulador de ação Jump
.
// CharacterController.cs private float _jumpActionEndTime; public void OnJump(InputAction.CallbackContext context) { if (context.started) { if (_characterBody.State == CharacterState.Grounded) { Jump(); } else { _jumpActionEndTime = Time.unscaledTime + _jumpActionTime; } } else if (context.canceled) { StopJumping(); } }
Quando o botão de pulo é pressionado, se o personagem estiver no chão, ele pula imediatamente. Caso contrário, armazenamos a janela de tempo durante a qual o pulo ainda pode ser executado.
Vamos remover a verificação de estado Grounded
do próprio método Jump
.
// CharacterController.cs private void Jump() { _isJumping = true; _characterBody.Jump(_jumpSpeed); }
Também adaptaremos o método stop-jumping. Se o botão foi liberado antes do pouso, nenhum salto deve ocorrer, então redefinimos _jumpActionEndTime
.
// CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; //... }
Quando devemos verificar se o personagem pousou e disparar um pulo? O estado CharacterBody
é processado em FixedUpdate
, enquanto o processamento da ação ocorre depois. Independentemente de ser Update
ou FixedUpdate
, pode ocorrer um atraso de um quadro entre o pouso e o pulo, o que é perceptível.
Adicionaremos um evento StateChanged
ao CharacterBody
para responder instantaneamente ao pouso. O primeiro argumento será o estado anterior, e o segundo será o estado atual.
// CharacterBody.cs public event Action<CharacterState, CharacterState> StateChanged;
Ajustaremos o gerenciamento de estado para acionar o evento de mudança de estado e reescreveremos FixedUpdate
.
// CharacterBody.cs [field: SerializeField] private CharacterState _state; public CharacterState State { get => _state; private set { if (_state != value) { var previousState = _state; _state = value; StateChanged?.Invoke(previousState, value); } } }
Também refinei como surfaceHit
é manipulado em FixedUpdate
.
// CharacterBody.cs private void FixedUpdate() { //... if (_velocity.y <= 0 && slideResults.surfaceHit) { var surfaceHit = slideResults.surfaceHit; Velocity = ClipVector(_velocity, surfaceHit.normal); if (surfaceHit.normal.y >= _minGroundVertical) { State = CharacterState.Grounded; return; } } State = CharacterState.Airborne; }
Em CharacterController
, assinaremos o evento StateChanged
e adicionaremos um manipulador.
// CharacterController.cs private void OnEnable() { _characterBody.StateChanged += OnStateChanged; } private void OnDisable() { _characterBody.StateChanged -= OnStateChanged; } private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } }
Removeremos a verificação de estado Grounded
de Update
e a moveremos para OnGrounded
.
// CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); } private void OnGrounded() { _isJumping = false; }
Agora, adicione o código para verificar se um salto deve ser acionado.
// CharacterController.cs private void OnGrounded() { _isJumping = false; if (_jumpActionEndTime > Time.unscaledTime) { _jumpActionEndTime = 0; Jump(); } }
Se _jumpActionEndTime
for maior que o tempo atual, significa que o botão de salto foi pressionado recentemente, então redefinimos _jumpActionEndTime
e executamos o salto.
Agora, tente pular continuamente com o personagem. Você notará que o botão de pulo parece mais responsivo, e controlar o personagem se torna mais suave. No entanto, observei que em certas situações, como o canto mostrado na ilustração abaixo, o estado Grounded
sofre um pequeno atraso, interrompendo a cadeia de pulos.
Para resolver isso, configurei o campo Surface Anchor
no componente CharacterBody
para 0,05 em vez de 0,01. Esse valor representa a distância mínima para uma superfície para o corpo entrar no estado Grounded
.
Você pode ter notado que tentar pular enquanto corre em superfícies verticais nem sempre funciona. Às vezes, pode parecer que o botão de pular não responde.
Esta é uma das sutilezas do desenvolvimento de um Controlador de Personagem para jogos de plataforma 2D. Os jogadores precisam da habilidade de pular mesmo quando estão um pouco atrasados para pressionar o botão de pulo. Embora este conceito possa parecer estranho, é como a maioria dos jogos de plataforma funciona. O resultado é um personagem parecendo se impulsionar do ar, como demonstrado na animação abaixo.
Vamos implementar essa mecânica. Vamos introduzir um novo campo para armazenar a janela de tempo (em segundos) durante a qual o personagem ainda pode pular após perder o estado Grounded
.
// CharacterController.cs [Min(0)] [SerializeField] private float _rememberGroundTime = 0.1f;
Também adicionaremos outro campo para armazenar o registro de data e hora após o qual o estado Grounded
é "esquecido".
// CharacterController.cs private float _lostGroundTime;
Este estado será rastreado usando o evento CharacterBody
. Ajustaremos o manipulador OnStateChanged
para esse propósito.
// CharacterController.cs private void OnStateChanged(CharacterState previousState, CharacterState state) { if (state == CharacterState.Grounded) { OnGrounded(); } else if (previousState == CharacterState.Grounded) { _lostGroundTime = Time.unscaledTime + _rememberGroundTime; } }
É importante distinguir se o personagem perdeu o estado Grounded
devido a um pulo intencional ou por outro motivo. Já temos o flag _isJumping
, que é desabilitado toda vez que StopJumping
é chamado para evitar ações redundantes.
Decidi não introduzir outra flag, já que o cancelamento redundante de pulo não afeta a jogabilidade. Sinta-se à vontade para experimentar. A flag _isJumping
agora só será limpa quando o personagem pousar após pular. Vamos atualizar o código adequadamente.
// CharacterController.cs private void StopJumping() { _jumpActionEndTime = 0; var velocity = _characterBody.Velocity; if (_isJumping && velocity.y > 0) { _characterBody.Velocity = new Vector2( velocity.x, velocity.y / _stopJumpFactor); } }
Por fim, revisaremos o método OnJump
.
// CharacterController.cs public void OnJump(InputAction.CallbackContext context) { if (context.started) { if (_characterBody.State == CharacterState.Grounded || (!_isJumping && _lostGroundTime > Time.unscaledTime)) { Jump(); } else { _jumpActionEndTime = Time.unscaledTime + _jumpActionTime; } } else if (context.canceled) { StopJumping(); } }
Agora, pular de superfícies verticais não interrompe mais o ritmo do jogo e parece muito mais natural, apesar do seu aparente absurdo. O personagem pode literalmente empurrar o ar, indo mais longe do que parece lógico. Mas é exatamente isso que é necessário para o nosso jogo de plataforma.
O toque final é fazer o personagem ficar de frente para a direção do movimento. Implementaremos isso da maneira mais simples — alterando a escala do personagem ao longo do eixo x. Definir um valor negativo fará com que nosso capitão fique de frente para a direção oposta.
Primeiro, vamos armazenar a escala original caso ela seja diferente de 1.
// CharacterController.cs public class CharacterController : MonoBehaviour { //... private Vector3 _originalScale; private void Awake() { //... _originalScale = transform.localScale; } }
Agora, ao mover para a esquerda ou direita, aplicaremos uma escala positiva ou negativa.
// CharacterController.cs public class CharacterController : MonoBehaviour { public void OnMove(InputAction.CallbackContext context) { //... // Change character's direction. if (value.x != 0) { var scale = _originalScale; scale.x = value.x > 0 ? _originalScale.x : -_originalScale.x; transform.localScale = scale; } } }
Vamos testar o resultado.
Este artigo acabou sendo bem detalhado, mas conseguimos cobrir todos os aspectos essenciais do controle de personagem em um jogo de plataforma 2D. Como lembrete, você pode conferir o resultado final no branch “ Character Controller ” do repositório.
Se você gostou ou achou este e o artigo anterior úteis, eu apreciaria curtidas e estrelas no GitHub. Não hesite em entrar em contato se encontrar algum problema ou erro. Obrigado pela sua atenção!