Script de Movimento 3D na Unity: Andar, Correr e Girar
O script de movimento Unity 3D que a maioria dos jogos em terceira pessoa precisa faz três coisas: anda na direção relativa à câmera (W leva para a frente da tela, não para o norte do mundo), corre enquanto o Shift está apertado e gira o corpo suavemente para onde está indo. Aqui você monta o PlayerMovement3D.cs com CharacterController, Input System e código tipado para o Unity 6.3 LTS.
No fim você terá um personagem que responde a teclado e controle, não corre mais rápido na diagonal, não dá giro brusco de 180 graus em um frame e cai quando sai de uma plataforma. É a base em cima da qual pulo, câmera orbital e animação entram depois.
Script movimento unity 3d: o que vamos montar
O personagem tem esta hierarquia:
Player (CharacterController + PlayerMovement3D)
└── Model (a malha do personagem, ou uma cápsula para testar)
Main Camera
Por que CharacterController e não Rigidbody? O CharacterController é um componente de movimento cinemático: você diz quanto ele anda e ele resolve colisão, degraus e rampas sem empurrões de física. Isso dá controle exato, que é o que um personagem de jogador costuma pedir. O preço é que gravidade, empurrar caixas e impulsos ficam por sua conta. A comparação completa está no post sobre Character Controller vs Rigidbody desta mesma série; aqui o foco é o movimento.
Configure o CharacterController no Inspector para um personagem de 2 metros:
- Height:
2e Center:(0, 1, 0), para a base da cápsula ficar no pivô do objeto. - Radius:
0.4a0.5. - Skin Width: cerca de 10% do raio (
0.05para raio 0.5). Valor baixo demais faz o personagem travar em quinas. - Step Offset:
0.3. É a altura de degrau que ele sobe sem pular. - Slope Limit:
45. Rampas mais íngremes viram parede.
Não coloque um Rigidbody no mesmo objeto. Os dois tentam mandar na posição e o resultado é tremedeira e teleporte.
Ler input com o Input System
Projetos novos do Unity 6 trazem o asset InputSystem_Actions configurado como project-wide actions, já habilitado. O mapa Player tem as ações Move (WASD, setas e analógico esquerdo) e Sprint (Shift esquerdo no teclado), que é tudo o que este script usa. Você pega as duas pelo nome com InputSystem.actions.FindAction.
A ação Move usa um composite 2D Vector para o teclado. O modo padrão desse composite é Digital Normalized: cada tecla vale 0 ou 1 e o vetor final é normalizado, então W+D já chega com comprimento 1. No analógico o valor é contínuo, com comprimento entre 0 e 1. Guardar esse comprimento é importante, porque ele permite andar devagar empurrando o analógico só um pouco.
Movimento relativo à câmera
O input chega como Vector2: x é esquerda e direita, y é frente e trás. No mundo 3D, "frente" depende de onde a câmera está. A conversão:
Vector3 forward = cameraTransform.forward;
Vector3 right = cameraTransform.right;
forward.y = 0f;
right.y = 0f;
forward.Normalize();
right.Normalize();
Vector3 direction = forward * input.y + right * input.x;
Zerar o y é o passo que todo mundo esquece. A câmera de terceira pessoa olha levemente para baixo, então o forward dela aponta para o chão. Sem achatar o vetor, parte do movimento vai para baixo, o CharacterController bate no piso e o personagem anda mais devagar para a frente do que para os lados. Depois de zerar o y, o Normalize devolve o comprimento 1. Como forward e right achatados são perpendiculares, a direção final tem o mesmo comprimento do input: 1 no teclado, proporcional no analógico.
Um detalhe: a câmera fica no cache. Camera.main procura a câmera com a tag MainCamera; a documentação do Unity 6.3 diz que o custo é pequeno, comparável a um GetComponent, e recomenda guardar a referência onde a performance importa. Pegue uma vez no Awake ou arraste a câmera no Inspector.
Se a matemática de somar vetores multiplicados ainda parece mágica, o post de matemática para jogos: vetores cobre exatamente essa operação com desenhos.
O script completo: andar, correr, girar e cair
Crie PlayerMovement3D.cs:
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(CharacterController))]
public class PlayerMovement3D : MonoBehaviour
{
[Header("Velocidade")]
[SerializeField] private float walkSpeed = 4f;
[SerializeField] private float runSpeed = 7.5f;
[SerializeField] private float speedChangeRate = 20f;
[Header("Rotação")]
[SerializeField] private float turnSpeed = 720f;
[Header("Gravidade")]
[SerializeField] private float gravity = -20f;
[SerializeField] private float groundedVerticalVelocity = -2f;
[Header("Referências")]
[SerializeField] private CharacterController controller;
[SerializeField] private Transform cameraTransform;
private InputAction moveAction;
private InputAction sprintAction;
private Vector3 lastMoveDirection = Vector3.forward;
private float currentSpeed;
private float verticalVelocity;
public float CurrentSpeed => currentSpeed;
private void Awake()
{
if (controller == null)
{
controller = GetComponent<CharacterController>();
}
if (cameraTransform == null && Camera.main != null)
{
cameraTransform = Camera.main.transform;
}
moveAction = InputSystem.actions.FindAction("Move");
sprintAction = InputSystem.actions.FindAction("Sprint");
}
private void Update()
{
Vector2 input = Vector2.ClampMagnitude(moveAction.ReadValue<Vector2>(), 1f);
Vector3 moveDirection = GetCameraRelativeDirection(input);
float targetSpeed = (IsRunning() ? runSpeed : walkSpeed) * input.magnitude;
currentSpeed = Mathf.MoveTowards(currentSpeed, targetSpeed, speedChangeRate * Time.deltaTime);
if (moveDirection.sqrMagnitude > 0.0001f)
{
lastMoveDirection = moveDirection.normalized;
RotateTowards(lastMoveDirection);
}
ApplyGravity();
Vector3 horizontal = lastMoveDirection * currentSpeed;
Vector3 velocity = new Vector3(horizontal.x, verticalVelocity, horizontal.z);
controller.Move(velocity * Time.deltaTime);
}
private bool IsRunning()
{
if (sprintAction != null)
{
return sprintAction.IsPressed();
}
return Keyboard.current != null && Keyboard.current.leftShiftKey.isPressed;
}
private Vector3 GetCameraRelativeDirection(Vector2 input)
{
if (cameraTransform == null)
{
return new Vector3(input.x, 0f, input.y);
}
Vector3 forward = cameraTransform.forward;
Vector3 right = cameraTransform.right;
forward.y = 0f;
right.y = 0f;
forward.Normalize();
right.Normalize();
return forward * input.y + right * input.x;
}
private void RotateTowards(Vector3 direction)
{
Quaternion targetRotation = Quaternion.LookRotation(direction, Vector3.up);
transform.rotation = Quaternion.RotateTowards(
transform.rotation,
targetRotation,
turnSpeed * Time.deltaTime);
}
private void ApplyGravity()
{
if (controller.isGrounded && verticalVelocity < 0f)
{
verticalVelocity = groundedVerticalVelocity;
}
else
{
verticalVelocity += gravity * Time.deltaTime;
}
}
}
Uma única chamada de controller.Move por frame, com horizontal e vertical juntos. Isso não é estilo: o isGrounded do CharacterController reflete se ele tocou o chão durante o último Move. Duas chamadas por frame (uma para andar, outra para cair) fazem o isGrounded piscar, porque a chamada horizontal não encosta no piso.
Andar e correr
targetSpeed combina três coisas: andar ou correr, o quanto o analógico está empurrado (input.magnitude) e zero quando não há input. O MoveTowards leva a velocidade atual até o alvo a speedChangeRate metros por segundo a cada segundo. Com 20, o personagem sai do zero até a corrida em menos de meio segundo, e a troca andar para correr fica perceptível sem parecer lenta.
Repare no lastMoveDirection. Quando o jogador solta o direcional, moveDirection vira zero na hora, mas a velocidade ainda está freando. Se o script multiplicasse a velocidade pela direção atual, o personagem pararia seco. Guardando a última direção válida, ele desacelera na direção em que vinha, que é o que o olho espera. A mesma variável segura a rotação: sem input, o personagem continua olhando para onde parou.
currentSpeed está exposto pela propriedade CurrentSpeed para o Animator. No script de animação você faz animator.SetFloat("Speed", movement.CurrentSpeed) e monta um Blend Tree 1D de parado, andar e correr usando os mesmos valores de walkSpeed e runSpeed como limiares.
Juntar esse movimento com câmera orbital, animação, pulo, combate e um level que alguém consiga terminar é o ponto em que a maior parte dos projetos em 3D desanda: cada sistema funciona sozinho e quebra quando encontra os outros. Aprender a costurar essas peças dentro de um projeto completo, com alguém mostrando a ordem certa, é o que um curso de Unity estruturado entrega e um tutorial isolado não consegue.
Girar na direção do movimento
A rotação usa Quaternion.LookRotation(direction, Vector3.up), que cria a rotação de "olhar para essa direção com o topo para cima", e Quaternion.RotateTowards, que gira da rotação atual até a alvo sem passar de turnSpeed * Time.deltaTime graus no frame.
Com turnSpeed = 720, uma meia-volta de 180 graus leva 0,25 segundo. Isso é rápido o bastante para não atrapalhar quem está desviando e lento o bastante para o jogador ver o personagem virar. Valores comuns ficam entre 540 e 900.
Por que não Slerp? O padrão que circula em tutoriais é este:
transform.rotation = Quaternion.Slerp(transform.rotation, targetRotation, 10f * Time.deltaTime);
Ele funciona, mas o comportamento é outro. Cada frame cobre uma fração da distância restante, então o giro começa rápido e desacelera, sem nunca chegar exatamente no alvo. Para uma câmera isso é agradável. Para um personagem, a meia-volta fica com um "arrasto" no final e a velocidade de giro muda conforme o ângulo. RotateTowards é linear e previsível: o mesmo número de graus por segundo sempre.
Gravidade básica
O CharacterController não usa a gravidade da física. O script acumula verticalVelocity com gravity * Time.deltaTime a cada frame no ar e manda junto no Move.
Dois detalhes:
gravity = -20e não-9.81. O padrão da física da Unity emProject Settings > Physicsé Y = -9,81, que é a gravidade real. Em jogo, gravidade real deixa quedas e pulos com cara de flutuação. Valores entre -15 e -30 são comuns em jogos de ação.- Velocidade de -2 no chão. Quando está no chão, o script não zera a velocidade vertical: mantém um valor negativo pequeno. Isso empurra a cápsula contra o piso a todo frame, o que mantém o
isGroundedestável ao descer rampas e degraus. Com zero, o personagem "descola" do chão na descida e oisGroundedalterna entre verdadeiro e falso.
O pulo entra exatamente nessa variável: basta definir verticalVelocity com um valor positivo quando o botão é apertado e o personagem está no chão. O cálculo da força pela altura desejada, o isGrounded instável e o coyote time em 3D estão no post sobre pulo e gravidade com Character Controller.
Erros comuns: diagonal rápida e giro brusco
Diagonal mais rápida. Se você trocar a ação Move por leitura direta de teclas (Keyboard.current.wKey), o vetor (1, 1) tem comprimento cerca de 1,41 e a diagonal fica 41% mais rápida. O Vector2.ClampMagnitude(input, 1f) no começo do Update resolve sem estragar o analógico. Evite input.normalized: ele transforma um toque leve no analógico em velocidade máxima.
Giro brusco. transform.forward = direction ou transform.rotation = Quaternion.LookRotation(direction) sem interpolação. O personagem estala na nova direção em um frame. Use o RotateTowards do script.
Personagem anda devagar para a frente e normal para os lados. Esqueceu de zerar o y do forward da câmera antes de normalizar.
Look rotation viewing vector is zero no Console. LookRotation recebeu um vetor zero. Sempre teste sqrMagnitude antes, como no script.
Personagem gira junto com a câmera e anda de lado. O script de câmera está como filho do Player. A câmera precisa ser um objeto separado (ou uma câmera do Cinemachine que segue o player), senão girar o corpo gira a câmera, que muda a direção, que gira o corpo.
Movimento depende do FPS. Algum valor está entrando no Move sem Time.deltaTime. O Move recebe deslocamento do frame, não velocidade. Por isso o script monta a velocidade em metros por segundo e multiplica pelo delta só na chamada. O post sobre delta time e movimento por frame mostra o efeito com números.
Próximo passo
Com andar, correr e girar resolvidos, o próximo degrau é a câmera. Você pode escrever a sua câmera orbital ou usar o pacote pronto da Unity; o post de third person controller na Unity compara os dois caminhos e mostra quando cada um vale a pena.
Exercício para fixar: troque o RotateTowards por Slerp, grave dez segundos de jogo com os dois e compare a meia-volta. Depois faça o mesmo com a gravidade em -9,81 e em -25. Ver a diferença em movimento ensina mais sobre sensação de controle do que qualquer explicação.
Perguntas frequentes
Como fazer o personagem andar para onde a câmera está olhando na Unity?
Pegue camera.forward e camera.right, zere o componente Y dos dois, normalize e combine com o input: forward vezes input.y mais right vezes input.x. Assim o W sempre leva para a frente da tela, qualquer que seja o ângulo da câmera.
Quaternion.Slerp ou Quaternion.RotateTowards para girar o personagem?
RotateTowards gira com velocidade angular constante, em graus por segundo, e é previsível. Slerp com um t fixo por frame faz uma aproximação exponencial: rápido no começo e lento no fim. Para personagem controlado pelo jogador, RotateTowards com 540 a 900 graus por segundo costuma dar a resposta mais limpa.
Por que meu personagem anda mais rápido na diagonal na Unity?
Porque somar dois eixos de valor 1 gera um vetor de comprimento cerca de 1,41. A ação Move padrão do Input System já normaliza o teclado, mas código que monta o vetor na mão precisa de Vector2.ClampMagnitude(input, 1f) antes de multiplicar pela velocidade.
CharacterController tem gravidade?
Não. O CharacterController não é um Rigidbody e não sofre a gravidade da física. Você acumula uma velocidade vertical no script e passa junto no Move, como mostra a seção de gravidade deste tutorial.



