Voltar para o Blog
Quest Log

Script de Movimento 3D na Unity: Andar, Correr e Girar

Personagem 3D estilizado andando num piso quadriculado, com uma seta curva mostrando o giro na direção do movimento e a câmera atrás

Script de movimento Unity 3D com CharacterController: andar relativo à câmera, correr com Shift, girar suave na direção e gravidade básica, em C# do Unity 6.

Ferramenta:UnityC#

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: 2 e Center: (0, 1, 0), para a base da cápsula ficar no pivô do objeto.
  • Radius: 0.4 a 0.5.
  • Skin Width: cerca de 10% do raio (0.05 para 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.

Curso de Unity
Quer fazer seu primeiro jogo na Unity?

No curso de Unity 6 da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Unity
Suporte no WhatsAppAcesso vitalícioGarantia de 7 dias

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 = -20 e não -9.81. O padrão da física da Unity em Project 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 isGrounded estável ao descer rampas e degraus. Com zero, o personagem "descola" do chão na descida e o isGrounded alterna 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.