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

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

por Denis Kondratev17m2024/12/08
Read on Terminal Reader

Muito longo; Para ler

Este artigo mostra como aprimorar um controlador de personagem Unity 2D, abordando uma nova configuração de sistema de entrada, mecânica de salto aprimorada e acompanhamento de câmera perfeito.
featured image - Como criar um controlador de personagem 2D no Unity: Parte 2
Denis Kondratev HackerNoon profile picture

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.

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 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.

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 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 .

Ensinando o personagem a pular

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 .

Configurando o personagem

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.


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 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.

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, _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 .

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 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.

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 “ 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!