Voltar para o Blog
Quest Log

Câmera Seguir o Jogador na Unity por Script (sem Cinemachine)

Câmera 2D acompanhando um personagem com uma seta de suavização e linhas marcando os limites do mapa

Unity camera follow player por script: LateUpdate, SmoothDamp, offset, limites do mapa com Mathf.Clamp e a correção do jitter com Rigidbody, em C#.

Ferramenta:UnityC#

Câmera Seguir o Jogador na Unity por Script (sem Cinemachine)

Para fazer a câmera seguir o jogador na Unity por script, você move o Transform da câmera no LateUpdate em direção à posição do jogador mais um offset, suavizando com Vector3.SmoothDamp. Esse é o núcleo de qualquer unity camera follow player. O resto é acabamento: lookahead, limite do mapa com Mathf.Clamp e a correção do jitter quando o jogador usa Rigidbody.

Aqui você vai montar o script em camadas, entendendo por que cada linha está ali, e terminar com uma câmera 2D que segue, suaviza, olha para frente e não mostra fora da fase. Todo o código foi escrito para o Unity 6.3 LTS e funciona em projeto 2D ou 3D com ajuste mínimo.

Unity camera follow player: o script mínimo

Comece com a versão mais crua possível, para ter certeza de que o básico funciona antes de adicionar suavização.

Arquivo CameraFollowBasic.cs, colocado na Main Camera:

using UnityEngine;

public class CameraFollowBasic : MonoBehaviour
{
    [SerializeField] private Transform target;
    [SerializeField] private Vector3 offset = new Vector3(0f, 1f, -10f);

    private void LateUpdate()
    {
        if (target == null)
        {
            return;
        }

        transform.position = target.position + offset;
    }
}

Arraste o jogador para o campo target. O offset com Z em -10 mantém a câmera na frente dos sprites num jogo 2D (a câmera olha para +Z, e os sprites ficam em Z = 0). Num jogo 3D, troque por algo como (0, 5, -8) e gire a câmera para olhar o personagem.

Essa câmera é colada no jogador. Funciona, mas cada movimento do personagem vira um movimento idêntico da tela, e isso cansa a vista em poucos minutos.

Por que LateUpdate

A Unity chama Update em todos os scripts, em uma ordem que você não controla por padrão, e só depois chama LateUpdate em todos. Se o script do jogador move o personagem no Update e a câmera também segue no Update, em alguns frames a câmera roda antes do jogador e usa a posição do frame anterior. O resultado é um tremor fino e irregular.

Colocando a câmera no LateUpdate, ela sempre lê a posição final do jogador naquele frame. É a regra mais simples e mais esquecida de câmera na Unity.

Existe uma exceção que veremos mais abaixo: quando o jogador é movido pela física. Aí a posição muda no passo de física, não no Update, e o LateUpdate sozinho não basta.

Suavizar com SmoothDamp

O Vector3.SmoothDamp move um valor em direção a um alvo como uma mola amortecida: rápido no começo, desacelerando ao chegar, sem passar do ponto. Os parâmetros, na ordem, são:

Vector3.SmoothDamp(Vector3 current, Vector3 target, ref Vector3 currentVelocity, float smoothTime, float maxSpeed, float deltaTime)

Os dois últimos parâmetros são opcionais. maxSpeed tem padrão infinito e deltaTime usa Time.deltaTime por padrão. O smoothTime é, aproximadamente, o tempo que a câmera leva para alcançar o alvo: 0.15 é firme, 0.3 é solto, acima de 0.5 parece que a câmera está com sono.

O ponto que quebra muita câmera é o ref currentVelocity. O SmoothDamp grava ali a velocidade atual para usar no próximo frame. Se você declarar essa variável dentro do método, ela nasce zerada a cada frame e a suavização vira um tranco. Ela tem que ser um campo da classe.

O SmoothDamp já usa o tempo do frame por dentro, então você não multiplica nada por Time.deltaTime. Se o conceito de tempo de frame ainda não está firme, o post de delta time e movimento por frame explica por que esquecer isso deixa o jogo mais rápido em PC mais forte.

Offset e lookahead simples

O offset fixo posiciona a câmera. O lookahead empurra a câmera para o lado em que o jogador está indo, para ele ver o que vem pela frente. A forma mais simples: ler a velocidade horizontal do Rigidbody2D do jogador e somar um deslocamento na direção dela.

Agora o script completo, com suavização, lookahead e limites. Arquivo CameraFollow2D.cs, na Main Camera:

using UnityEngine;

[RequireComponent(typeof(Camera))]
public class CameraFollow2D : MonoBehaviour
{
    [Header("Alvo")]
    [SerializeField] private Transform target;
    [SerializeField] private Rigidbody2D targetBody;
    [SerializeField] private Vector3 offset = new Vector3(0f, 1f, -10f);

    [Header("Suavização")]
    [SerializeField] private float smoothTime = 0.2f;

    [Header("Lookahead")]
    [SerializeField] private float lookaheadDistance = 2f;
    [SerializeField] private float lookaheadSpeedThreshold = 0.5f;
    [SerializeField] private float lookaheadSmoothTime = 0.4f;

    [Header("Limites do mapa (unidades do mundo)")]
    [SerializeField] private bool useBounds = true;
    [SerializeField] private Vector2 minBounds = new Vector2(-20f, -5f);
    [SerializeField] private Vector2 maxBounds = new Vector2(60f, 15f);

    private Camera cam;
    private Vector3 velocity = Vector3.zero;
    private float currentLookahead = 0f;
    private float lookaheadVelocity = 0f;

    private void Awake()
    {
        cam = GetComponent<Camera>();
    }

    private void LateUpdate()
    {
        if (target == null)
        {
            return;
        }

        UpdateLookahead();

        Vector3 desired = target.position + offset;
        desired.x += currentLookahead;

        Vector3 smoothed = Vector3.SmoothDamp(transform.position, desired, ref velocity, smoothTime);

        if (useBounds)
        {
            smoothed = ClampToBounds(smoothed);
        }

        transform.position = smoothed;
    }

    private void UpdateLookahead()
    {
        float targetLookahead = currentLookahead;

        if (targetBody != null)
        {
            float speedX = targetBody.linearVelocity.x;
            if (Mathf.Abs(speedX) > lookaheadSpeedThreshold)
            {
                targetLookahead = Mathf.Sign(speedX) * lookaheadDistance;
            }
        }

        currentLookahead = Mathf.SmoothDamp(currentLookahead, targetLookahead, ref lookaheadVelocity, lookaheadSmoothTime);
    }

    private Vector3 ClampToBounds(Vector3 position)
    {
        float halfHeight = cam.orthographicSize;
        float halfWidth = halfHeight * cam.aspect;

        float minX = minBounds.x + halfWidth;
        float maxX = maxBounds.x - halfWidth;
        float minY = minBounds.y + halfHeight;
        float maxY = maxBounds.y - halfHeight;

        position.x = minX <= maxX ? Mathf.Clamp(position.x, minX, maxX) : (minBounds.x + maxBounds.x) * 0.5f;
        position.y = minY <= maxY ? Mathf.Clamp(position.y, minY, maxY) : (minBounds.y + maxBounds.y) * 0.5f;

        return position;
    }

    private void OnDrawGizmosSelected()
    {
        if (!useBounds)
        {
            return;
        }

        Gizmos.color = Color.yellow;
        Vector3 center = new Vector3((minBounds.x + maxBounds.x) * 0.5f, (minBounds.y + maxBounds.y) * 0.5f, 0f);
        Vector3 size = new Vector3(maxBounds.x - minBounds.x, maxBounds.y - minBounds.y, 0f);
        Gizmos.DrawWireCube(center, size);
    }
}

Algumas decisões que valem explicar:

  • O lookahead só troca de lado quando a velocidade passa de um limiar. Sem isso, um toque leve no direcional faz a câmera atravessar a tela.
  • Quando o jogador para, o targetLookahead mantém o último valor. A câmera continua olhando para onde ele estava indo, que é o comportamento esperado em plataforma. Se preferir que ela volte ao centro, troque a linha inicial por float targetLookahead = 0f;.
  • O lookahead tem o próprio SmoothDamp, mais lento que o da posição, para não dar tranco na troca de direção.
  • linearVelocity é o nome atual no Unity 6. Tutoriais antigos usam velocity, que está marcado como obsoleto no Rigidbody2D da Unity 6.

Limitar a câmera ao mapa

O ClampToBounds é onde entra a conta que confunde. Numa câmera ortográfica, o Orthographic Size é metade da altura visível, em unidades do mundo. A metade da largura é essa altura vezes cam.aspect (largura dividida pela altura da tela). Com Orthographic Size 5 numa tela 16:9, a câmera mostra 10 unidades de altura e cerca de 17,8 de largura.

Para a borda da tela nunca passar da borda do mapa, o centro da câmera precisa ficar a meia largura de distância da borda esquerda e da direita, e a meia altura de distância do chão e do teto. Daí os minX, maxX, minY e maxY.

O teste minX <= maxX cobre uma sala menor que a tela. Sem ele, o Clamp recebe mínimo maior que máximo e a câmera pula. Com ele, a câmera centraliza na sala.

O Gizmo amarelo desenha os limites na Scene view quando a câmera está selecionada. Ajuste os valores até o retângulo contornar a fase.

Se você usa Tilemap, dá para tirar os limites direto dele: tilemap.localBounds ou tilemap.cellBounds convertidos para mundo. Para começar, digitar os números e conferir pelo Gizmo é mais rápido.

Repare que o clamp acontece depois do SmoothDamp. Se você limitar o alvo antes de suavizar, a câmera ainda pode passar da borda por um instante quando o jogador muda de direção perto do limite, porque o SmoothDamp tem inércia. Limitando o resultado final, a borda é absoluta.

Se o jogo tem zoom (o orthographicSize muda durante a partida), a conta continua certa, porque o script lê o tamanho a cada frame. Só tome cuidado com zoom-out numa sala pequena: quando a área visível fica maior que a sala, entra o caso de centralizar, e a câmera para de seguir o jogador naquele eixo. Isso é esperado e é o que a maioria dos jogos faz.

Até aqui, a câmera resolve um caso. Quando o projeto cresce e a câmera precisa conversar com troca de sala, cutscene, menu e checkpoint, a falta de um caminho organizado pesa. É nesse ponto, de juntar sistemas num jogo completo, que um curso de Unity com projeto do começo ao fim encurta a estrada.

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

Jitter com Rigidbody: a correção

Este é o problema que faz gente desistir de câmera por script: o personagem treme na tela, mas só ele, o cenário fica liso. Ou o contrário, o cenário treme e o personagem fica firme.

A causa: o Rigidbody2D se move no passo de física, que roda em intervalo fixo (por padrão, 50 vezes por segundo). A tela atualiza no ritmo do monitor, 60, 144 ou mais vezes por segundo. Em alguns frames a física não andou e o corpo está parado; em outros andou duas vezes. A câmera, suavizada no LateUpdate, anda de forma contínua. A diferença entre os dois ritmos aparece como tremor.

A correção é ligar a interpolação no Rigidbody2D do jogador:

  • No Inspector: Rigidbody 2D > Interpolate > Interpolate.
  • Por código: targetBody.interpolation = RigidbodyInterpolation2D.Interpolate;

Com interpolação, a Unity calcula uma posição intermediária do Transform a cada frame renderizado, entre o passo de física anterior e o atual. A câmera passa a ler uma posição que anda de forma suave, e o tremor some. No 3D é o mesmo campo, no Rigidbody, com RigidbodyInterpolation.Interpolate.

Duas coisas que pioram o jitter e você deve evitar:

  • Mover a câmera no FixedUpdate. Ela passa a andar no ritmo da física e todo o cenário treme.
  • Mover o jogador com transform.position dentro do FixedUpdate enquanto ele tem Rigidbody. Use rb.MovePosition ou velocidade.

Quando usar Cinemachine em vez de script

O script acima tem cerca de 100 linhas, você entende cada número e não depende de pacote. Para muitos jogos pequenos, isso basta.

O Cinemachine passa a valer a pena quando aparecem:

  • Dead zone e hard limits com ajuste visual na tela.
  • Várias câmeras com transição suave (uma por sala, câmera de diálogo, câmera de chefe).
  • Mapa com borda irregular, que pede um PolygonCollider2D como limite em vez de um retângulo.
  • Camera shake por impulso, cutscene com Timeline e integração com Pixel Perfect.

Reescrever tudo isso à mão é possível, mas vira um projeto paralelo. O guia de Cinemachine 2D mostra o mesmo resultado deste post, mais o confiner e a troca de sala, sem script de seguimento.

Próximo passo

Coloque o CameraFollow2D numa cena de teste com um corredor longo e uma sala pequena. Teste três valores de smoothTime (0.1, 0.2 e 0.4) e escolha pelo que parece melhor jogando, não pelo número. Depois desligue o Interpolate do Rigidbody2D por um minuto, só para reconhecer o tremor quando ele aparecer de novo. Se você também usa Godot, compare com a câmera 2D da Godot: a lógica de suavização e limite é a mesma.

Perguntas frequentes

Por que a câmera deve seguir o jogador no LateUpdate e não no Update?

O LateUpdate roda depois de todos os Update do frame. Se o jogador se move no Update e a câmera também, a ordem entre os dois scripts não é garantida, e em alguns frames a câmera usa a posição antiga. No LateUpdate a câmera sempre lê a posição final do jogador naquele frame.

O que é o parâmetro ref velocity do Vector3.SmoothDamp?

É uma variável que o próprio SmoothDamp usa para guardar a velocidade atual da câmera entre um frame e outro. Ela precisa ser um campo da classe, não uma variável local, senão a suavização zera a cada frame e a câmera anda aos trancos.

Como calcular o tamanho visível de uma câmera ortográfica na Unity?

A metade da altura visível, em unidades do mundo, é o Orthographic Size. A metade da largura é o Orthographic Size multiplicado por camera.aspect. Com esses dois valores você calcula quanto a câmera precisa ficar longe da borda do mapa.

Vale mais a pena usar Cinemachine ou script próprio para câmera?

Para câmera que só segue com suavização e limite, um script de 40 linhas resolve e você entende cada número. Quando entram dead zone, várias câmeras com transição, shake por impulso, cutscene ou confiner com forma irregular, o Cinemachine economiza muito trabalho.