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 foi adicionado ao componente , o que simplifica muito a escrita de um controlador de personagem por permitindo o uso do no modo Kinematic de forma mais eficaz. Anteriormente, toda esta funcionalidade tinha que ser implementada manualmente. Slide Rigidbody2D Rigidbody2D 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 , onde já estão incluídos os assets necessários e um nível pronto para testar seu personagem. Treasure Hunters 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 e ao objeto. Defina o tipo 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. Rigidbody2D BoxCollider2D Rigidbody2D 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 . 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. Rigidbody2D 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 foi adicionado ao componente , permitindo o controle flexível do objeto físico ao mesmo tempo que fornece todas as informações necessárias sobre o movimento realizado. Slide Rigidbody2D Vamos começar criando uma classe , que conterá a lógica básica para mover personagens no mundo físico. Esta classe usará no modo , que já adicionamos ao nosso personagem. Então, a primeira coisa é adicionar uma referência a este componente. CharacterBody Rigidbody2D Kinematic 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, . Discutiremos isso mais adiante. CharacterState Estados de personagem Para simplificar o desenvolvimento do nosso jogo de plataformas, vamos definir apenas dois estados dos personagens principais. O primeiro é , 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. Grounded O segundo é , 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. Airborne 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 , 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. _velocity Isso pode ser feito calculando o comprimento do vetor velocidade ou, em termos matemáticos, sua magnitude. A estrutura já contém uma propriedade 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 pelo vetor de velocidade normalizado (um vetor normalizado é um vetor com a mesma direção, mas com magnitude 1). Vector2 magnitude _maxSpeed 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 , que simplificará bastante o desenvolvimento do nosso . 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 . Slide() CharacterBody Rigidbody2D.SlideMovement Vamos apresentar um novo campo e definir seus valores. _slideMovement 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 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 , 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. maxIterations Slide() Em tal situação, se o valor fosse definido como 1, o objeto atingiria a parede, pararia e ficaria efetivamente preso ali. maxIterations Os valores para e foram definidos anteriormente. Para informações mais detalhadas sobre os demais campos, consulte . maxIterations layerMask a documentação oficial da estrutura Finalmente, movendo o personagem Agora está tudo pronto para colocar o Capitão em movimento. Faremos isso em — 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 ou mesmo chamando o método necessário por conta própria. FixedUpdate Update No entanto, neste exemplo, usaremos o método tradicional e comprovado . Antes de prosseguir, vale mencionar algumas palavras sobre o valor de . FixedUpdate 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 do objeto ao longo do tempo pela fórmula: Δv Δt onde é a aceleração constante do objeto. No nosso caso, é a aceleração da gravidade, considerando o coeficiente que introduzimos — . Portanto, pode ser calculado da seguinte forma: a Physics2D.gravity * GravityFactor Δv 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 é um valor da estrutura e armazena os resultados do movimento. Os principais campos deste resultado para nós são , o resultado da colisão com a superfície durante o movimento, e — o resultado de um lançamento para baixo, que ajudará a determinar se o personagem está em uma superfície estável. slideResults SlideResults slideHit surfaceHit 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, e , são representados por valores da estrutura , que inclui a normal da superfície de colisão. slideHit surfaceHit RaycastHit2D 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 . Vamos escrever um método que realizará esta operação: produto escalar private static Vector2 ClipVector(Vector2 vector, Vector2 hitNormal) { return vector - Vector2.Dot(vector, hitNormal) * hitNormal; } Agora vamos integrar esse método ao nosso . Aqui, para , 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. FixedUpdate surfaceHit 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 . É necessário comparar este ângulo com — o ângulo máximo possível da superfície na qual o personagem pode ficar estável. surfaceHit.normal _maxSlop 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 . Para o ângulo , este valor pode ser calculado como: y alpha 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 , verificando todas as condições acima. FixedUpdate 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 a ele. CharacterBody 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: 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. Caçadores de Tesouro