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:
- GameObject > UI > Image. Renomeie para
Fade. - Em Rect Transform, use o preset de âncora que estica nos dois eixos (segure Alt ao clicar) e zere as margens.
- Cor preta, Alpha 255.
- Adicione Canvas Group e desmarque Blocks Raycasts, para o fade invisível não bloquear cliques nos botões da UI.
- Arraste o Canvas Group para o campo
Fade Groupdo 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.
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:
- 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.
Physics.SyncTransforms()logo depois de mudar a posição, forçando a física a ler os transforms antes do próximoMove().- 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.



