Pulo na Unity 2D: Ground Check, Altura Variável e Coyote
Um pulo na Unity 2D que o jogador sente como justo precisa de cinco peças: um ground check confiável, velocidade inicial calculada pela altura que você quer, altura variável quando o botão é solto cedo, coyote time e jump buffer, e uma gravidade maior na queda. Este tutorial monta as cinco num único PlayerJump2D.cs em C# para o Unity 6.3 LTS, com Rigidbody2D.linearVelocity e o Input System.
O script foi feito para trabalhar junto com o script de movimento 2D na Unity: o movimento só escreve a velocidade X e o pulo só escreve a Y. Se você já tem outro script de movimento horizontal, ele encaixa do mesmo jeito, desde que não zere a velocidade vertical.
Pulo unity 2d: o mínimo que funciona
A versão mais curta possível de um pulo é esta:
if (jumpAction.WasPressedThisFrame() && isGrounded)
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpVelocity);
}
Ela já tem duas decisões certas. Primeiro, define a velocidade vertical em vez de somar força: o pulo sai igual se o personagem estava parado ou descendo uma rampa. Com AddForce, uma velocidade de queda residual come parte do impulso e o pulo sai baixo de vez em quando. Segundo, só pula no chão.
O que falta é tudo o que faz o pulo parecer bom: o jogador aperta o botão um pouco antes de tocar o chão e nada acontece, sai da borda e aperta um frame atrasado e nada acontece, solta o botão cedo e o personagem sobe a altura inteira mesmo assim. O resto do post resolve cada um desses.
Prepare a cena assim: o Player com Rigidbody2D (Dynamic, Freeze Rotation Z, Interpolate, Gravity Scale 3), um CapsuleCollider2D, e um filho vazio chamado GroundCheck posicionado na base da cápsula. Todo o cenário pisável fica numa layer Ground. O Player não fica na layer Ground.
Ground check: OverlapCircle, OverlapBox ou raycast
Os três métodos aparecem em tutoriais e cada um tem um defeito típico:
Physics2D.Raycastpara baixo a partir do centro. Um raio só, no meio do corpo. Quando o personagem está com metade dos pés para fora da borda, o raio passa no vazio e o jogo diz que ele está no ar. Funciona com dois ou três raios (um em cada pé), mas aí você está reinventando uma caixa.Physics2D.OverlapCirclenos pés. Cobre largura, mas um círculo largo o bastante para os pés todos sobe pelos lados e encosta na parede. Resultado: o personagem "está no chão" grudado no muro e pula infinitamente escalando a parede.Physics2D.OverlapBoxnos pés. Uma caixa fina, um pouco mais estreita que o collider e um pouco abaixo dele. Cobre os pés inteiros e não alcança paredes. É a escolha padrão deste post.
Na documentação do Unity 6.3, o overload mais simples é OverlapBox(Vector2 point, Vector2 size, float angle, int layerMask), que devolve o primeiro Collider2D encontrado ou null. Os outros overloads recebem um ContactFilter2D e preenchem um array ou uma List<Collider2D>, úteis quando você quer saber tudo o que está sob o personagem. Para o ground check basta o primeiro:
bool grounded = Physics2D.OverlapBox(groundCheck.position, groundCheckSize, 0f, groundLayer) != null;
Tamanho de referência para um collider de 1 unidade de largura: groundCheckSize = (0.8, 0.1), com o GroundCheck a 0,05 unidade abaixo da base da cápsula. Desenhe a caixa com OnDrawGizmosSelected para enxergar o que ela pega; o script completo já faz isso.
Força do pulo e linearVelocity
Em vez de chutar um número de força e testar até acertar, calcule a velocidade a partir da altura. Da física básica, a velocidade inicial para subir h com gravidade g é:
v = raiz(2 * g * h)
No 2D da Unity, a gravidade que age no personagem é Physics2D.gravity.y (o padrão do projeto é -9,81, em Project Settings > Physics 2D) multiplicada pelo gravityScale do Rigidbody2D. Em código:
float g = Mathf.Abs(Physics2D.gravity.y * baseGravityScale);
float jumpVelocity = Mathf.Sqrt(2f * g * jumpHeight);
Com Gravity Scale 3 e jumpHeight = 3, a velocidade sai perto de 13,3 unidades por segundo e o personagem sobe 3 unidades, ou três tiles de 1 unidade. Agora o level designer fala em tiles ("esse buraco precisa de pulo de 3 de altura") e o código entende a mesma língua. Mudou a gravidade para deixar o jogo mais pesado? A altura continua 3, só fica mais rápido.
O script completo
Crie PlayerJump2D.cs:
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class PlayerJump2D : MonoBehaviour
{
[Header("Pulo")]
[SerializeField] private float jumpHeight = 3f;
[SerializeField, Range(0f, 1f)] private float jumpCutMultiplier = 0.5f;
[Header("Tolerâncias")]
[SerializeField] private float coyoteTime = 0.1f;
[SerializeField] private float jumpBufferTime = 0.12f;
[SerializeField] private float groundLockAfterJump = 0.1f;
[Header("Gravidade")]
[SerializeField] private float fallGravityMultiplier = 1.8f;
[SerializeField] private float maxFallSpeed = 20f;
[Header("Ground check")]
[SerializeField] private Transform groundCheck;
[SerializeField] private Vector2 groundCheckSize = new Vector2(0.8f, 0.1f);
[SerializeField] private LayerMask groundLayer;
[Header("Referências")]
[SerializeField] private Rigidbody2D rb;
private InputAction jumpAction;
private float baseGravityScale;
private float coyoteCounter;
private float jumpBufferCounter;
private float groundLockCounter;
private bool jumpCutRequested;
private bool isGrounded;
public bool IsGrounded => isGrounded;
private void Awake()
{
if (rb == null)
{
rb = GetComponent<Rigidbody2D>();
}
baseGravityScale = rb.gravityScale;
jumpAction = InputSystem.actions.FindAction("Jump");
}
private void Update()
{
groundLockCounter -= Time.deltaTime;
isGrounded = CheckGround();
if (isGrounded)
{
coyoteCounter = coyoteTime;
}
else
{
coyoteCounter -= Time.deltaTime;
}
if (jumpAction.WasPressedThisFrame())
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
if (jumpAction.WasReleasedThisFrame())
{
jumpCutRequested = true;
}
}
private void FixedUpdate()
{
if (jumpBufferCounter > 0f && coyoteCounter > 0f)
{
Jump();
}
if (jumpCutRequested)
{
if (rb.linearVelocity.y > 0f)
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, rb.linearVelocity.y * jumpCutMultiplier);
}
jumpCutRequested = false;
}
ApplyFallGravity();
}
private void Jump()
{
float gravity = Mathf.Abs(Physics2D.gravity.y * baseGravityScale);
float jumpVelocity = Mathf.Sqrt(2f * gravity * jumpHeight);
rb.linearVelocity = new Vector2(rb.linearVelocity.x, jumpVelocity);
jumpBufferCounter = 0f;
coyoteCounter = 0f;
groundLockCounter = groundLockAfterJump;
jumpCutRequested = false;
}
private void ApplyFallGravity()
{
if (rb.linearVelocity.y < 0f)
{
rb.gravityScale = baseGravityScale * fallGravityMultiplier;
}
else
{
rb.gravityScale = baseGravityScale;
}
if (rb.linearVelocity.y < -maxFallSpeed)
{
rb.linearVelocity = new Vector2(rb.linearVelocity.x, -maxFallSpeed);
}
}
private bool CheckGround()
{
if (groundLockCounter > 0f)
{
return false;
}
return Physics2D.OverlapBox(groundCheck.position, groundCheckSize, 0f, groundLayer) != null;
}
private void OnDrawGizmosSelected()
{
if (groundCheck == null)
{
return;
}
Gizmos.color = Color.green;
Gizmos.DrawWireCube(groundCheck.position, groundCheckSize);
}
}
O CheckGround devolve false por 0,1 segundo logo depois do pulo (groundLockAfterJump). Parece detalhe, mas evita um bug chato: no frame seguinte ao pulo, a caixa dos pés ainda encosta no chão, o coyote recarrega e um segundo toque rápido no botão dá um pulo duplo acidental. A trava é por tempo, e não por "velocidade Y positiva", porque subindo uma rampa a física também dá velocidade Y positiva ao personagem, e ele perderia o pulo no meio da subida.
Pulo e movimento funcionando juntos já dão um personagem jogável. O desafio de verdade começa quando ele precisa morrer, renascer no checkpoint, tomar dano, tocar animação e som certos em cada estado, e tudo isso num jogo com começo, meio e fim. Organizar essa quantidade de pequenos sistemas sem virar bagunça é a parte que mais trava quem aprende sozinho, e é onde um curso de Unity com projeto completo, do primeiro script ao jogo publicado, encurta o caminho.
Altura variável ao soltar o botão
InputAction.WasReleasedThisFrame() devolve verdadeiro no frame em que o botão passa do limiar de soltura depois de ter sido apertado. O script marca o pedido no Update e, no FixedUpdate, se o personagem ainda está subindo, multiplica a velocidade vertical por jumpCutMultiplier.
Com 0.5, soltar logo no começo do pulo corta a subida pela metade da velocidade, e como a altura cresce com o quadrado da velocidade, o pulo curto fica com cerca de um quarto da altura máxima. Segurar até o topo dá os 3 tiles completos. Esse controle fino é o que permite ao jogador fazer um pulinho para desviar de um projétil baixo sem bater a cabeça no teto.
Valores menores (0,3) deixam o corte mais radical. Valores perto de 1 praticamente desligam o recurso.
Coyote time e jump buffer
As duas técnicas resolvem o mesmo problema por lados opostos: a diferença entre o que o jogador acha que fez e o frame em que o jogo registrou.
Coyote time (coyoteTime = 0.1): enquanto está no chão, o contador fica cheio. Ao sair da borda, ele desce. Se o pulo chega antes de zerar, vale. O jogador nunca percebe que pulou "no ar"; ele só percebe quando o recurso não existe e o jogo "comeu" o pulo.
Jump buffer (jumpBufferTime = 0.12): o toque no botão fica guardado por 0,12 segundo. Se o personagem toca o chão nesse intervalo, o pulo sai na hora. Isso é o que deixa sequências de pulos encadeados fluírem.
O FixedUpdate só pula quando os dois contadores estão positivos, e o Jump() zera os dois. Zerar o coyote impede pulo duplo; zerar o buffer impede que o mesmo toque dispare dois pulos. A teoria das duas técnicas, com os números usados em jogos conhecidos, está no post sobre sistema de pulo com coyote time.
Queda mais rápida
Pulo com a mesma gravidade na subida e na descida tem cara de bola de vôlei: sobe e desce num arco simétrico e lento. Quase todo jogo de plataforma com boa sensação cai mais rápido do que sobe.
O ApplyFallGravity troca o gravityScale para base * 1.8 quando a velocidade Y é negativa. A subida mantém a altura calculada e a descida fica mais seca. O maxFallSpeed limita a velocidade terminal, para que quedas longas não fiquem tão rápidas que o jogador perca o controle e para que o personagem não atravesse plataformas finas.
Ajuste com o jogo rodando: fallGravityMultiplier entre 1,5 e 2,5 cobre a maioria dos estilos. O cálculo do jumpVelocity usa baseGravityScale, então mexer no multiplicador de queda não muda a altura do pulo.
Unity jump not working: causas
Quando o pulo simplesmente não sai, a causa quase sempre está nesta lista, na ordem de frequência:
- O ground check acerta o próprio player. O Player está na layer
Groundou oGround Layerestá comoEverything. A caixa encosta no collider do próprio personagem, o resultado fica sempre verdadeiro e o personagem pula no ar quantas vezes quiser. Coloque o Player numa layer própria. Ground Layervazio. O LayerMask está comoNothinge o personagem nunca está no chão. Selecione o Player e confira o campo.- Input lido no
FixedUpdate.WasPressedThisFrameé sobre o frame de renderização. Dentro doFixedUpdate, que pode rodar zero vezes num frame, o toque se perde. Leia noUpdatee use o buffer, como no script. - Outro script zera a velocidade Y. Um movimento horizontal que faz
rb.linearVelocity = new Vector2(x, 0f)mata o pulo no passo seguinte. Escreva sempre preservando o Y. - Nome da ação errado. O
FindActionnão diferencia maiúsculas, mas um nome diferente do asset ("Pular"quando a ação se chamaJump) devolvenulle oUpdateestoura comNullReferenceException. Ponha umDebug.Log(jumpAction.WasPressedThisFrame())para separar problema de input de problema de física. AddForcesemForceMode2D.Impulse. O modo padrão é força contínua, pensado para aplicar a cada passo. Num único frame o empurrão é minúsculo e parece que nada aconteceu. ComlinearVelocity, como no script, esse problema não existe.- Rigidbody2D em
Kinematic. Corpo kinematic não sofre gravidade nem é barrado pelo cenário: o pulo sobe e não volta, ou o personagem atravessa o chão. O player de plataforma éDynamic.
Próximo passo
Com pulo e movimento prontos, monte uma sala de teste: um degrau de 1 tile, um de 3, um de 4 (que não deve dar para subir), uma borda para testar coyote e um corredor de teto baixo para testar o pulo curto. Ajuste os números até os cinco obstáculos se comportarem como você planejou. Esse exercício ensina mais do que qualquer valor pronto.
Se quiser ver as mesmas técnicas implementadas em outra engine para separar conceito de API, o tutorial de movimento de personagem de plataforma 2D faz aceleração e fricção no Godot, com a mesma lógica de tolerâncias.
Perguntas frequentes
Qual o melhor ground check na Unity 2D?
Para plataforma, Physics2D.OverlapBox um pouco mais estreito que o collider do personagem e posicionado nos pés é o mais confiável. Ele cobre a largura dos pés inteira, ao contrário de um raycast único no centro, e não detecta parede quando é mais estreito que o corpo.
Por que meu pulo na Unity não funciona?
As causas mais comuns são: o ground check acerta o próprio collider do player, a layer do chão não está marcada no LayerMask, o input do pulo é lido dentro do FixedUpdate e perde o toque, ou outro script sobrescreve a velocidade Y com zero. A seção de causas deste post tem o passo a passo para cada uma.
Como calcular a força do pulo pela altura na Unity?
A velocidade inicial é a raiz quadrada de 2 vezes a gravidade vezes a altura. Na Unity 2D a gravidade efetiva é Physics2D.gravity.y vezes o gravityScale do Rigidbody2D, em módulo. Assim você ajusta o pulo em unidades de altura, não em números mágicos de força.
O que é coyote time?
É uma janela curta, em geral de 0,08 a 0,15 segundo, em que o jogador ainda pode pular depois de sair da borda de uma plataforma. Ela compensa a diferença entre o que o jogador vê e o frame exato em que o personagem deixou o chão.



