Voltar para o Blog
Quest Log

Checkpoint e Respawn na Unity: Morrer e Voltar

Bandeira de checkpoint acesa numa plataforma, com o personagem reaparecendo ao lado envolto em luz

Checkpoint na Unity com trigger, respawn do jogador com fade, reset de inimigos e o bug do CharacterController que ignora o teleporte, em C# do Unity 6.3.

Ferramenta:UnityC#

Checkpoint e Respawn na Unity: Morrer e Voltar

Um checkpoint na Unity (o unity checkpoint de todo jogo de plataforma e ação) é um trigger que guarda onde o jogador vai reaparecer, e o respawn é o código que usa essa informação quando ele morre: fade para preto, teleporte, vida cheia, inimigos de volta ao lugar, fade de volta. Aqui você monta os dois em C# do Unity 6.3 LTS e resolve o bug que trava muita gente: o personagem com CharacterController que ignora o transform.position e não teleporta.

Como checkpoint e respawn se conectam

São quatro peças com papéis claros:

  • Checkpoint: um trigger na fase. Quando o jogador entra, ele avisa o gerenciador. Não sabe nada de morte.
  • CheckpointManager: guarda o checkpoint ativo e comanda o respawn. É o único que conhece todas as outras peças.
  • Resettable: qualquer objeto que precisa voltar ao estado inicial quando o jogador morre (inimigo, plataforma que cai, porta que fechou).
  • PlayerRespawn: sabe teleportar o jogador do jeito certo para o tipo de corpo que ele usa.

A morte em si vem do sistema de vida. Aqui uso o componente Health do post de sistema de vida e dano na Unity, que dispara o evento Died e tem ResetHealth(). Se o seu jogo usa outro sistema, troque essas duas chamadas.

Antes de escrever, uma decisão de design: recarregar a cena ou resetar na mão? SceneManager.LoadScene reseta tudo de graça, mas também perde o que você queria manter (moedas pegas, porta destrancada, tempo da fase). Resetar na mão dá controle sobre o que volta e o que fica. Este tutorial segue o segundo caminho, que é o que jogo com checkpoint no meio da fase pede.

Checkpoint na Unity com trigger

Crie um GameObject Checkpoint com um BoxCollider marcado como Is Trigger (largo o bastante para o jogador não passar ao lado) e um filho vazio SpawnPoint posicionado um pouco acima do chão, onde o jogador vai aparecer. Crie Checkpoint.cs:

using UnityEngine;

[RequireComponent(typeof(Collider))]
public class Checkpoint : MonoBehaviour
{
    [SerializeField] private int order;
    [SerializeField] private Transform spawnPoint;
    [SerializeField] private CheckpointManager manager;
    [SerializeField] private GameObject activeVisual;

    public int Order => order;
    public Transform SpawnPoint => spawnPoint;

    private void Start()
    {
        SetVisualActive(false);
    }

    private void OnTriggerEnter(Collider other)
    {
        if (!other.CompareTag("Player"))
        {
            return;
        }
        manager.TryActivate(this);
    }

    public void SetVisualActive(bool active)
    {
        if (activeVisual != null)
        {
            activeVisual.SetActive(active);
        }
    }
}

O campo order é o que impede o checkpoint de "ativar de trás": cada checkpoint tem um número crescente na ordem da fase (0, 1, 2...), e o gerenciador só aceita um checkpoint com número maior que o atual. O activeVisual é a bandeira acesa, a fogueira ou o cristal brilhando: o jogador precisa ver que salvou.

Em jogo 2D, a troca é direta: BoxCollider2D com Is Trigger e OnTriggerEnter2D(Collider2D other).

Guardar posição e estado no GameManager

O gerenciador guarda o checkpoint ativo, a posição inicial (para quando o jogador morre antes do primeiro checkpoint) e comanda o respawn. Crie CheckpointManager.cs:

using System.Collections;
using UnityEngine;

public class CheckpointManager : MonoBehaviour
{
    [SerializeField] private PlayerRespawn player;
    [SerializeField] private Health playerHealth;
    [SerializeField] private CanvasGroup fadeGroup;
    [SerializeField] private float fadeDuration = 0.35f;
    [SerializeField] private float holdBlackTime = 0.2f;

    private Checkpoint current;
    private Vector3 startPosition;
    private Quaternion startRotation;
    private bool isRespawning;

    private void Awake()
    {
        startPosition = player.transform.position;
        startRotation = player.transform.rotation;
        fadeGroup.alpha = 0f;
    }

    private void OnEnable()
    {
        playerHealth.Died += HandlePlayerDied;
    }

    private void OnDisable()
    {
        playerHealth.Died -= HandlePlayerDied;
    }

    public void TryActivate(Checkpoint checkpoint)
    {
        if (current != null && checkpoint.Order <= current.Order)
        {
            return;
        }

        if (current != null)
        {
            current.SetVisualActive(false);
        }

        current = checkpoint;
        current.SetVisualActive(true);
    }

    private void HandlePlayerDied()
    {
        if (!isRespawning)
        {
            StartCoroutine(RespawnRoutine());
        }
    }

    private IEnumerator RespawnRoutine()
    {
        isRespawning = true;
        player.SetControlEnabled(false);

        yield return Fade(0f, 1f);

        Vector3 position = current != null ? current.SpawnPoint.position : startPosition;
        Quaternion rotation = current != null ? current.SpawnPoint.rotation : startRotation;
        player.TeleportTo(position, rotation);

        ResettableRegistry.ResetAll();
        playerHealth.ResetHealth();

        yield return new WaitForSecondsRealtime(holdBlackTime);
        yield return Fade(1f, 0f);

        player.SetControlEnabled(true);
        isRespawning = false;
    }

    private IEnumerator Fade(float from, float to)
    {
        float elapsed = 0f;
        while (elapsed < fadeDuration)
        {
            elapsed += Time.unscaledDeltaTime;
            fadeGroup.alpha = Mathf.Lerp(from, to, elapsed / fadeDuration);
            yield return null;
        }
        fadeGroup.alpha = to;
    }
}

O gerenciador recebe o jogador e o fade por referência no Inspector, sem Find. A flag isRespawning evita o respawn duplo quando o jogador "morre de novo" durante o fade (cair num espinho enquanto a tela escurece, por exemplo).

Respawn do jogador com fade

O fade é uma Image preta ocupando a tela inteira dentro de um Canvas, com um componente CanvasGroup:

  1. GameObject > UI > Image. Renomeie para Fade.
  2. Em Rect Transform, use o preset de âncora que estica nos dois eixos (segure Alt ao clicar) e zere as margens.
  3. Cor preta, Alpha 255.
  4. Adicione Canvas Group e desmarque Blocks Raycasts, para o fade invisível não bloquear cliques nos botões da UI.
  5. Arraste o Canvas Group para o campo Fade Group do gerenciador.

O fade usa Time.unscaledDeltaTime e WaitForSecondsRealtime de propósito. Se em algum momento você pausar o jogo na morte com Time.timeScale = 0 (comum para uma tela de "Você morreu"), um fade com Time.deltaTime nunca avançaria e a tela ficaria preta para sempre.

A ordem dentro da coroutine importa: o teleporte e os resets acontecem com a tela preta. O jogador nunca vê os inimigos voltando ao lugar nem o personagem sumindo e aparecendo. É isso que faz o respawn parecer polido em vez de remendado.

Resetar inimigos e objetos

Cada objeto que precisa voltar ao estado inicial implementa uma interface e se registra num registro central. Crie IResettable.cs e ResettableRegistry.cs:

public interface IResettable
{
    void ResetState();
}
using System.Collections.Generic;
using UnityEngine;

public static class ResettableRegistry
{
    private static readonly List<IResettable> items = new List<IResettable>();

    public static void Register(IResettable item)
    {
        if (!items.Contains(item))
        {
            items.Add(item);
        }
    }

    public static void Unregister(IResettable item)
    {
        items.Remove(item);
    }

    public static void ResetAll()
    {
        for (int i = items.Count - 1; i >= 0; i--)
        {
            items[i].ResetState();
        }
    }

    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
    private static void Clear()
    {
        items.Clear();
    }
}

E um inimigo resetável, em ResettableEnemy.cs:

using UnityEngine;

public class ResettableEnemy : MonoBehaviour, IResettable
{
    [SerializeField] private Health health;

    private Vector3 startPosition;
    private Quaternion startRotation;

    private void Awake()
    {
        startPosition = transform.position;
        startRotation = transform.rotation;
        ResettableRegistry.Register(this);
    }

    private void OnDestroy()
    {
        ResettableRegistry.Unregister(this);
    }

    public void ResetState()
    {
        gameObject.SetActive(true);
        transform.SetPositionAndRotation(startPosition, startRotation);
        health.ResetHealth();
    }
}

O registro é feito em Awake e desfeito em OnDestroy, não em OnEnable e OnDisable. Inimigo morto costuma ser desativado, e se ele saísse do registro ao ser desativado, o reset nunca o traria de volta. Para isso funcionar, o inimigo morto deve ser desativado, não destruído. O Clear com RuntimeInitializeOnLoadMethod limpa a lista estática ao entrar em Play Mode, para não sobrar referência da execução anterior quando o recarregamento de domínio está desligado.

Note quantos sistemas um respawn toca: vida, input, câmera, inimigos, UI, física. Cada um funciona sozinho, e o problema é a ordem e a dependência entre eles. Esse é o tipo de integração que trava projeto solo, e um curso de Unity com jogo completo treina justamente porque monta todas as peças dentro do mesmo projeto, na ordem em que elas se conectam.

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

O que mais o checkpoint deve guardar

Posição é o mínimo. Em quase todo jogo, o checkpoint também decide o que acontece com o progresso feito entre ele e a morte, e essa é uma decisão de design que você precisa tomar de propósito:

  • Moedas e coletáveis: o jogador pegou 12 moedas depois do checkpoint e morreu. Ele perde as 12 (o coletável volta para a fase) ou mantém? Para perder, guarde a pontuação no momento em que o checkpoint ativa e restaure esse número no respawn, e faça as moedas implementarem IResettable.
  • Chaves e portas: quase sempre ficam. Fazer o jogador buscar a chave de novo depois de cada morte é punição, não desafio.
  • Chefes e arenas: o chefe volta com vida cheia, mas a porta da arena reabre para o jogador poder respirar antes de tentar de novo.

A forma prática: no TryActivate, além de guardar o checkpoint, tire uma "foto" dos números que devem voltar (pontuação, munição) e restaure essa foto no RespawnRoutine, junto com o ResetAll.

CharacterController e teleporte

Aqui está o bug. Você escreve transform.position = checkpoint; no jogador com CharacterController, e ele aparece no checkpoint por um frame e volta para onde morreu. Ou nem sai do lugar.

O motivo: o CharacterController mantém a própria representação de posição dentro da física. Por padrão, a Unity não sincroniza mudanças de transform com a física a cada alteração (a opção Auto Sync Transforms em Project Settings > Physics vem desligada). No próximo Move(), o controller usa a posição que ele conhecia e escreve de volta no transform, desfazendo o seu teleporte.

As correções que funcionam:

  1. Desativar, mover, reativar o componente. Ao ser reativado, o controller lê a posição atual do transform. É a mais usada e a mais previsível.
  2. Physics.SyncTransforms() logo depois de mudar a posição, forçando a física a ler os transforms antes do próximo Move().
  3. Ligar Auto Sync Transforms no projeto todo. Funciona, mas custa desempenho em toda mudança de transform da cena, só para resolver um teleporte. Não recomendo.

O PlayerRespawn.cs usa a primeira e cobre também o caso de Rigidbody:

using UnityEngine;

public class PlayerRespawn : MonoBehaviour
{
    [SerializeField] private CharacterController characterController;
    [SerializeField] private Rigidbody body;
    [SerializeField] private MonoBehaviour[] controlScripts;

    public void TeleportTo(Vector3 position, Quaternion rotation)
    {
        if (characterController != null)
        {
            characterController.enabled = false;
            transform.SetPositionAndRotation(position, rotation);
            characterController.enabled = true;
        }
        else if (body != null)
        {
            body.linearVelocity = Vector3.zero;
            body.angularVelocity = Vector3.zero;
            body.position = position;
            body.rotation = rotation;
            transform.SetPositionAndRotation(position, rotation);
        }
        else
        {
            transform.SetPositionAndRotation(position, rotation);
        }
    }

    public void SetControlEnabled(bool isEnabled)
    {
        foreach (MonoBehaviour script in controlScripts)
        {
            script.enabled = isEnabled;
        }
    }
}

Com Rigidbody, a diferença entre Rigidbody.position e transform.position: escrever em body.position move o corpo direto na física, sem passar pela interpolação, que é o que um teleporte quer. Zerar linearVelocity (o nome no Unity 6 para a antiga velocity) evita que o jogador reapareça ainda caindo com a velocidade da queda que o matou. No seu script de movimento, zere também a velocidade vertical acumulada quando o controle for reativado.

Erros comuns: respawn no lugar errado, checkpoint ativando de trás

Reaparece no início da fase. O checkpoint nunca ativou: o jogador não tem a tag Player, o collider do checkpoint não é trigger, ou nenhum dos dois tem Rigidbody ou CharacterController (o CharacterController conta como corpo para disparar trigger).

Reaparece dentro do chão. O SpawnPoint está na altura dos pés, mas o pivô do jogador é o centro do corpo. Suba o SpawnPoint metade da altura do personagem.

O checkpoint ativa de trás. Faltou o order ou os números estão fora da sequência da fase.

Inimigos não voltam. Foram destruídos em vez de desativados, ou não têm o ResettableEnemy.

O jogador morre de novo ao reaparecer. Ele reaparece dentro de uma zona de dano, ou o fade termina com um inimigo ainda em cima do spawn. Dê 1 segundo de invencibilidade após o respawn.

Próximo passo: salvar entre sessões

O checkpoint de hoje vive na memória e some ao fechar o jogo. O próximo degrau é salvar o order do checkpoint ativo (e o que mais importar) em arquivo JSON em Application.persistentDataPath, e ler ao abrir. Para comparar a mesma arquitetura em outra engine, o post de checkpoint e respawn no Godot resolve o mesmo problema com nós e sinais, e o de sistema de vida e dano no Godot mostra o lado da morte.

Perguntas frequentes

Como fazer um checkpoint na Unity?

Coloque um collider marcado como Is Trigger no ponto do checkpoint e, quando o jogador entrar, guarde a posição de spawn num gerenciador. Ao morrer, o gerenciador teleporta o jogador para essa posição e reseta o que precisa ser resetado.

Por que o CharacterController não teleporta na Unity?

O CharacterController guarda a própria posição interna, e quando a sincronização automática de transforms está desligada (o padrão), ele pode sobrescrever o transform.position que você acabou de definir. Desative o componente, mude a posição e ative de novo, ou chame Physics.SyncTransforms depois de mudar a posição.

Como fazer o respawn do jogador na Unity sem recarregar a cena?

Mantenha um gerenciador que sabe o último checkpoint e uma lista de objetos resetáveis. Ao morrer, faça um fade para preto, teleporte o jogador, restaure a vida, chame o reset de cada inimigo e objeto registrado e faça o fade de volta. Recarregar a cena é mais simples, mas perde o estado que você quer manter.

Como impedir que o checkpoint ative quando o jogador volta para trás?

Dê um número de ordem a cada checkpoint e só aceite a ativação se o número for maior que o do checkpoint atual. Assim, passar de novo por um checkpoint antigo não move o ponto de respawn para trás.