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, “ ”, 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. Como criar um controlador de personagem 2D no Unity: Parte 1 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 “ ”, que contém a base para este artigo. Alternativamente, você pode baixar o branch “ ” com o resultado final. Character Body Character Controller Configurando o sistema de entrada 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. Criando ações de entrada Na pasta , crie Input Actions por meio do menu principal: . Nomeie o arquivo como "Controls". Settings Assets → Create → Input Actions 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 para abri-lo para edição e adicione um para controle de personagem chamado "Personagem". Controles Mapa de Ação 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 como "Value" e o como "Vector2" para habilitar o movimento em quatro direções. Action Type Control Type 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 . 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. Save Asset Em seguida, adicione uma nova ação — "Pular". Mantenha o como "Botão", mas adicione uma nova — "Pressionar", e defina como "Pressionar e Soltar", pois precisamos capturar o pressionamento e a liberação do botão. Tipo de Ação Interação o Comportamento do Gatilho Isso completa o esquema de controle de caracteres. O próximo passo é escrever um componente para lidar com essas ações. Movendo o personagem para a esquerda e para a direita É hora de vincular as Ações de Entrada que criamos para o controle do personagem ao componente , permitindo que o personagem se mova ativamente pela cena de acordo com nossos comandos de controle. CharacterBody Para fazer isso, criaremos um script responsável pelo controle de movimento e o nomearemos para maior clareza. Neste script, primeiro definiremos alguns campos básicos. Adicionaremos uma referência ao componente , , que será controlado diretamente pelo script. CharacterController CharacterBody _characterBody Também definiremos parâmetros para a velocidade de movimento do personagem ( ) e altura do salto ( ). Além disso, definiremos a finalidade do campo . _speed _jumpHeight _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 determina o quanto a velocidade ascendente diminui ao liberar o botão de salto. _stopJumpFactor 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 , introduziremos um método para definir essa velocidade: CharacterBody // 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 em cada quadro: Update // CharacterController.cs private void Update() { _characterBody.SetLocomotionVelocity(_locomotionVelocity); } Definiremos um método para manipular sinais da ação Input: Move // CharacterController.cs public void OnMove(InputAction.CallbackContext context) { var value = context.ReadValue<Vector2>(); _locomotionVelocity = value.x * _speed; } Como a ação é definida como um , o contexto fornecerá um valor de vetor dependendo de quais teclas são pressionadas ou liberadas. Por exemplo, pressionar a tecla resultará no método recebendo o vetor (1, 0). Pressionar e simultaneamente resultará em (1, 1). Liberar todas as teclas acionará com o valor (0, 0). Move Vector2 D OnMove D W OnMove Para a tecla , o vetor será (-1, 0). No método , pegamos o componente horizontal do vetor recebido e o multiplicamos pela velocidade de movimento especificada, . A OnMove _speed Ensinando o personagem a pular Primeiro, precisamos ensinar o componente a lidar com o pulo. Para fazer isso, adicionaremos um método responsável pelo pulo: CharacterBody // 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 é a altura do salto, e é a aceleração gravitacional. Também levaremos em conta o multiplicador de gravidade presente no componente . Adicionaremos um novo campo para definir a velocidade inicial do salto e calculá-la da seguinte forma: h g CharacterBody // 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 é um botão, podemos determinar a partir do contexto se o pressionamento do botão começou ( ) ou terminou ( ). Com base nisso, iniciamos ou paramos o salto. Jump context.started context.canceled 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 e fazemos o corpo pular com o . _isJumping _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 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 e reduzimos a velocidade vertical por um fator de . _isJumping _isJumping _stopJumpFactor Configurando o personagem Agora que todos os componentes estão prontos, adicione os componentes e ao objeto na cena. Certifique-se de selecionar o componente que criamos, não o componente Unity padrão projetado para controlar personagens 3D. PlayerInput CharacterController Captain CharacterController Para o , atribua o componente existente do personagem. Para o , defina os criados anteriormente no campo . CharacterController CharacterBody PlayerInput Controls 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. Movimento da câmera 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 na cena e adicione o componente a ele. Câmera Principal CinemachineBrain Em seguida, crie um novo objeto na cena chamado . Esta será a câmera que segue o capitão, assim como um cinegrafista profissional. Adicione o componente a ele. Defina o campo para o capitão, escolha para o campo e defina o parâmetro para 4. CaptainCamera CinemachineVirtualCamera Follow Framing Transposer Body Lens Ortho Size Além disso, precisaremos de outro componente para definir o deslocamento da câmera em relação ao personagem — . Defina o valor como 1,5 e o valor como -15. CinemachineCameraOffset Y Z 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. Melhorando os saltos 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, , que representa a janela de tempo durante a qual um salto ainda pode ser acionado se surgir a oportunidade. _jumpActionTime // CharacterController.cs [Min(0)] [SerializeField] private float _jumpActionTime = 0.1f; Adicionei um campo , que marca o fim da janela de ação de pulo. Em outras palavras, até que seja atingido, o personagem pulará se a oportunidade surgir. Vamos também atualizar o manipulador de ação . _jumpActionEndTime _jumpActionEndTime 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 do próprio método . Grounded 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 é processado em , enquanto o processamento da ação ocorre depois. Independentemente de ser ou , pode ocorrer um atraso de um quadro entre o pouso e o pulo, o que é perceptível. CharacterBody FixedUpdate Update FixedUpdate Adicionaremos um evento ao para responder instantaneamente ao pouso. O primeiro argumento será o estado anterior, e o segundo será o estado atual. StateChanged CharacterBody // 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 é manipulado em . surfaceHit 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 , assinaremos o evento e adicionaremos um manipulador. CharacterController StateChanged // 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 de e a moveremos para . Grounded Update 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 for maior que o tempo atual, significa que o botão de salto foi pressionado recentemente, então redefinimos e executamos o salto. _jumpActionEndTime _jumpActionEndTime 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 sofre um pequeno atraso, interrompendo a cadeia de pulos. Grounded Para resolver isso, configurei o campo no componente 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 . Surface Anchor CharacterBody Grounded Salto de penhasco 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 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. Controlador de Personagem 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 é "esquecido". Grounded // CharacterController.cs private float _lostGroundTime; Este estado será rastreado usando o evento . Ajustaremos o manipulador para esse propósito. CharacterBody OnStateChanged // 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 devido a um pulo intencional ou por outro motivo. Já temos o flag , que é desabilitado toda vez que é chamado para evitar ações redundantes. Grounded _isJumping StopJumping 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 agora só será limpa quando o personagem pousar após pular. Vamos atualizar o código adequadamente. _isJumping // 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. Inversão de personagem 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. Encerrando 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 “ ” do repositório. Character Controller 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!