Pulo e Gravidade com Character Controller na Unity 3D
Para fazer o unity jump character controller funcionar você precisa de três coisas que o componente não dá: uma velocidade vertical acumulada no script, a gravidade somada a ela a cada frame e uma velocidade de pulo calculada pela altura que você quer. O CharacterController só se move quando você chama Move, e só cai se você mandar ele cair. Aqui você monta gravidade, pulo, altura pela fórmula, coyote time e jump buffer em C# para o Unity 6.3 LTS.
O post também explica o isGrounded instável, que é a reclamação número um de quem usa CharacterController, e o truque de manter -2 de velocidade no chão que aparece em tanto código sem explicação.
Por que o Character Controller não tem gravidade
O CharacterController é um controlador cinemático: uma cápsula com resolução de colisão, degraus (Step Offset) e rampas (Slope Limit), sem massa, sem forças e sem gravidade. A gravidade de Project Settings > Physics, que no padrão é (0, -9.81, 0), só age em objetos com Rigidbody.
Isso é uma vantagem. Num personagem com Rigidbody, a gravidade, o atrito e os impulsos de colisão disputam o controle com o seu script. No CharacterController, cada centímetro de movimento sai do seu código, e o personagem para exatamente onde você quer, sobe degraus sem quicar e não escorrega numa rampa leve. O preço é escrever a gravidade. A escolha entre os dois modelos tem post próprio: Character Controller vs Rigidbody na Unity.
Gravidade manual passo a passo
A lógica tem três linhas, e a ordem importa:
- Some a gravidade na velocidade vertical:
verticalVelocity += gravity * Time.deltaTime. - Monte o deslocamento do frame: horizontal e vertical juntos, multiplicados por
Time.deltaTime. - Chame
controller.Move(deslocamento)uma única vez.
O Time.deltaTime aparece duas vezes, e as duas estão certas. A primeira transforma aceleração (metros por segundo ao quadrado) em ganho de velocidade no frame. A segunda transforma velocidade em deslocamento no frame, que é o que o Move recebe. Esquecer qualquer uma deixa a queda dependente do FPS: em 144 quadros por segundo o personagem cai num ritmo, em 30 cai em outro. O conceito está detalhado em delta time e movimento por frame.
Sobre o valor: a gravidade real (-9,81) deixa o pulo com cara de câmera lenta, porque personagens de jogo pulam muito mais alto em proporção ao corpo do que pessoas. Comece com -20 e ajuste.
Unity jump character controller: o script
Crie PlayerJumpController3D.cs. Ele tem um movimento horizontal simples, relativo ao próprio personagem, só para dar para testar; se você já tem o seu, copie a parte vertical.
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public class PlayerJumpController3D : MonoBehaviour
{
[Header("Movimento")]
[SerializeField] private float moveSpeed = 5f;
[Header("Pulo")]
[SerializeField] private float jumpHeight = 1.5f;
[SerializeField, Range(0f, 1f)] private float jumpCutMultiplier = 0.5f;
[Header("Gravidade")]
[SerializeField] private float gravity = -20f;
[SerializeField] private float fallGravityMultiplier = 1.6f;
[SerializeField] private float groundedVerticalVelocity = -2f;
[SerializeField] private float maxFallSpeed = 40f;
[Header("Tolerâncias")]
[SerializeField] private float coyoteTime = 0.12f;
[SerializeField] private float jumpBufferTime = 0.12f;
[Header("Referências")]
[SerializeField] private CharacterController controller;
private InputAction moveAction;
private InputAction jumpAction;
private float verticalVelocity;
private float coyoteCounter;
private float jumpBufferCounter;
public float VerticalVelocity => verticalVelocity;
private void Awake()
{
if (controller == null)
{
controller = GetComponent<CharacterController>();
}
moveAction = InputSystem.actions.FindAction("Move");
jumpAction = InputSystem.actions.FindAction("Jump");
}
private void Update()
{
bool grounded = controller.isGrounded;
if (grounded && verticalVelocity < 0f)
{
verticalVelocity = groundedVerticalVelocity;
}
UpdateTimers(grounded);
TryJump();
ApplyGravity();
MoveCharacter();
}
private void UpdateTimers(bool grounded)
{
if (grounded)
{
coyoteCounter = coyoteTime;
}
else
{
coyoteCounter -= Time.deltaTime;
}
if (jumpAction.WasPressedThisFrame())
{
jumpBufferCounter = jumpBufferTime;
}
else
{
jumpBufferCounter -= Time.deltaTime;
}
}
private void TryJump()
{
if (jumpBufferCounter > 0f && coyoteCounter > 0f)
{
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
jumpBufferCounter = 0f;
coyoteCounter = 0f;
}
if (jumpAction.WasReleasedThisFrame() && verticalVelocity > 0f)
{
verticalVelocity *= jumpCutMultiplier;
}
}
private void ApplyGravity()
{
float currentGravity = verticalVelocity < 0f ? gravity * fallGravityMultiplier : gravity;
verticalVelocity += currentGravity * Time.deltaTime;
verticalVelocity = Mathf.Max(verticalVelocity, -maxFallSpeed);
}
private void MoveCharacter()
{
Vector2 input = Vector2.ClampMagnitude(moveAction.ReadValue<Vector2>(), 1f);
Vector3 horizontal = (transform.right * input.x + transform.forward * input.y) * moveSpeed;
Vector3 motion = new Vector3(horizontal.x, verticalVelocity, horizontal.z) * Time.deltaTime;
CollisionFlags flags = controller.Move(motion);
if ((flags & CollisionFlags.Above) != 0 && verticalVelocity > 0f)
{
verticalVelocity = 0f;
}
}
}
A última verificação usa o retorno do Move. CollisionFlags.Above indica que a cápsula bateu em algo em cima. Sem zerar a velocidade nesse caso, o personagem que bate a cabeça num teto baixo fica "grudado" nele até a gravidade consumir toda a velocidade de subida, o que parece um bug de colisão.
Altura do pulo pela fórmula
A linha do pulo é:
verticalVelocity = Mathf.Sqrt(jumpHeight * -2f * gravity);
Ela vem da equação de Torricelli: um objeto lançado para cima com velocidade v sob gravidade g sobe até h = v² / (2g). Isolando v, fica v = raiz(2 * g * h). O -2f está ali porque a gravidade do script é negativa; multiplicar por -2 deixa o produto positivo antes da raiz.
Com jumpHeight = 1.5 e gravity = -20, a velocidade inicial é cerca de 7,75 m/s e o pico fica em 1,5 metro. A grande vantagem: você ajusta o pulo em metros, que é a unidade em que o level design pensa ("esse muro tem 1,4 de altura, tem que dar para subir"). Se depois mudar a gravidade para -30 para deixar o jogo mais seco, a altura continua 1,5; o pulo só fica mais rápido.
A fórmula vale para a subida. O multiplicador de queda (fallGravityMultiplier) só entra quando a velocidade fica negativa, então não altera a altura máxima, só a sensação de peso na descida.
Gravidade, pulo, colisão com teto e tolerâncias de input já são muitos detalhes para um único botão. Some câmera, animação de pulo com início, ar e aterrissagem, e um level que precisa ser testado com esses números, e o projeto 3D começa a cobrar organização que tutorial solto não ensina. É aqui que muita gente para, e é o tipo de costura entre sistemas que um curso de Unity com projeto completo trabalha do começo ao fim.
isGrounded instável: como resolver
A documentação do Unity 6.3 descreve o CharacterController.isGrounded como: a cápsula estava tocando o chão durante o último Move? Ele é atualizado a cada chamada de Move, com base na colisão com o que está embaixo. Três consequências práticas:
Sem movimento para baixo, sem chão. Se no frame você chamar Move com Y igual a zero, a cápsula não encosta no piso e isGrounded vira false. É por isso que o script não zera a velocidade vertical quando aterrissa: ele mantém -2. Esse valor empurra a cápsula contra o chão a todo frame, o contato acontece e o isGrounded fica estável. Por que -2 e não -0,1? Porque descendo uma rampa ou um degrau o chão "foge" mais rápido do que 0,1 m/s, e a cápsula descola. Algo entre -1 e -5 funciona; -2 é o valor que virou costume.
Duas chamadas de Move, dois resultados. Se você chama Move para o horizontal e outra vez para o vertical, o isGrounded reflete a última. Quem lê no meio do frame pega o valor errado. Uma chamada por frame, com tudo junto.
Min Move Distance. No Inspector do CharacterController, se esse campo for maior que zero, deslocamentos menores que ele são ignorados e a cápsula nem se mexe. Com o personagem parado e a velocidade vertical pequena, isso pode deixar o isGrounded falso. O manual recomenda deixar em 0 na maioria dos casos.
Se mesmo assim você quer uma checagem independente do Move, faça como o Starter Assets da Unity: uma esfera nos pés com Physics.CheckSphere.
[SerializeField] private float groundCheckRadius = 0.28f;
[SerializeField] private float groundCheckOffset = -0.14f;
[SerializeField] private LayerMask groundLayers;
private bool CheckGroundSphere()
{
Vector3 spherePosition = transform.position + Vector3.up * groundCheckOffset;
return Physics.CheckSphere(spherePosition, groundCheckRadius, groundLayers, QueryTriggerInteraction.Ignore);
}
Com o pivô do personagem na base da cápsula, essa esfera fica levemente abaixo dos pés. Troque controller.isGrounded por CheckGroundSphere() no Update e o pulo deixa de depender do resultado do frame anterior.
Coyote time em 3D
O coyote time funciona igual ao 2D: enquanto o personagem está no chão, o contador fica em coyoteTime; quando sai, ele desce, e o pulo vale enquanto for positivo. Em 3D ele importa até mais, porque o jogador julga bordas por perspectiva e erra com mais frequência a distância da beirada. Valores entre 0,1 e 0,15 segundo costumam passar despercebidos pelo jogador e evitam a sensação de "o jogo comeu meu pulo".
O jump buffer cobre o outro lado: o toque no botão fica guardado por 0,12 segundo, e se o personagem aterrissar nesse intervalo o pulo sai. As duas técnicas, com a explicação de por que funcionam e exemplos de jogos que usam, estão no post de sistema de pulo com coyote time e jump buffer. O TryJump zera os dois contadores quando pula, o que impede pulo duplo por acidente.
Repare que o WasReleasedThisFrame faz a altura variável: soltar o botão durante a subida multiplica a velocidade por jumpCutMultiplier. Com 0,5, um toque rápido dá um pulo de cerca de um quarto da altura máxima.
Erros comuns
Personagem cai rápido demais ou devagar demais dependendo do PC. Falta Time.deltaTime em uma das duas multiplicações. Confira as duas.
Pulo infinito no ar. O ground check (seja isGrounded ou esfera) está encostando em algo que não é chão: o próprio collider do personagem, um trigger, ou a parede. Na esfera, use um LayerMask que não inclua a layer do player e QueryTriggerInteraction.Ignore.
Pulo às vezes não sai. isGrounded instável. Aplique o -2 no chão, uma chamada de Move por frame e, se persistir, troque pela esfera.
Personagem quica descendo rampa. A velocidade horizontal tira o personagem do chão mais rápido do que os -2 trazem de volta. Aumente para -5 ou faça um Move extra para baixo só quando estava no chão no frame anterior e não pulou (o chamado "snap to ground").
Personagem fica preso no teto. Falta checar CollisionFlags.Above depois do Move.
Pulo com altura diferente da configurada. O multiplicador de queda está sendo aplicado na subida (o pulo fica mais baixo), ou a gravidade usada na fórmula é diferente da usada no ApplyGravity. As duas têm que ler o mesmo campo.
Rigidbody e CharacterController no mesmo objeto. Remova o Rigidbody. Os dois brigam pela posição.
Próximo passo
Monte um percurso de teste com blocos de 0,5, 1, 1,4 e 2 metros de altura e um vão de 3 metros. Com jumpHeight = 1.5, os três primeiros devem dar para subir e o de 2 não. Depois mude a gravidade para -30 e confirme que as alturas continuam iguais. Quando a fórmula estiver na sua mão, o próximo passo é decidir se o seu personagem continua com CharacterController ou se o jogo pede física de verdade, e o post Character Controller vs Rigidbody ajuda nessa escolha.
Perguntas frequentes
Por que o CharacterController não cai sozinho na Unity?
Porque ele não é um Rigidbody e não participa da simulação de física. Ele só se move quando você chama Move, com o deslocamento que você calcular. A gravidade precisa ser somada à mão numa velocidade vertical dentro do script.
Por que o isGrounded do CharacterController fica alternando entre true e false?
O isGrounded diz se a cápsula tocou algo embaixo durante a última chamada de Move. Se nessa chamada não houve movimento para baixo, ele vira false. Manter uma velocidade vertical pequena e negativa no chão, como -2, e chamar Move uma vez só por frame resolve na maioria dos casos.
Como calcular a força do pulo pela altura na Unity 3D?
Use velocidade = Mathf.Sqrt(alturaDoPulo * -2f * gravidade), com a gravidade negativa. Com altura 1,5 e gravidade -20, a velocidade inicial é cerca de 7,75 metros por segundo, e o personagem sobe exatamente 1,5 metro.
Qual o valor padrão da gravidade na Unity?
O padrão em Project Settings > Physics é (0, -9.81, 0). Ele só afeta Rigidbodies. Para CharacterController o valor é o que você escrever no script, e muitos jogos usam algo entre -15 e -30 para o pulo não parecer flutuante.



