Voltar para o Blog
Quest Log

DontDestroyOnLoad na Unity: Dados entre Cenas sem Bug

Um cubo brilhante flutuando sobre várias cenas de jogo empilhadas como cartas, ligado a todas por fios de luz

Unity DontDestroyOnLoad sem objeto duplicado: manter algo vivo entre cenas, checar instância, música de fundo com crossfade, cena bootstrap e quando não usar.

Ferramenta:UnityC#

DontDestroyOnLoad na Unity: Dados entre Cenas sem Bug

O unity dontdestroyonload faz um GameObject sobreviver à troca de cena: você chama DontDestroyOnLoad(gameObject) no Awake e, quando o SceneManager.LoadScene destruir tudo da cena atual, aquele objeto fica. É o jeito mais direto de manter música tocando, pontuação ou um gerenciador de áudio entre fases. O problema aparece na primeira vez que o jogador volta ao menu: a cena do menu cria o objeto de novo, o antigo continua vivo e agora existem dois. Na terceira volta, três.

Aqui você vê o básico, entende por que a duplicata acontece, corrige com checagem de instância, monta um gerenciador de música de fundo que não reinicia nem duplica, e conhece as alternativas: cena bootstrap, ScriptableObject e classe estática. No fim, quando não usar.

O que DontDestroyOnLoad faz na Unity

Ao carregar uma cena no modo padrão, a Unity destrói todos os objetos da cena atual. Object.DontDestroyOnLoad marca um objeto como exceção. Na prática, a Unity move o objeto para uma cena especial, que aparece na Hierarchy durante o Play Mode com o nome DontDestroyOnLoad. Ela não é descarregada por troca de cena.

Três regras da documentação:

  • Funciona em GameObjects raiz ou componentes de GameObjects raiz. Objeto filho não é preservado.
  • Os filhos do objeto vão junto. Preservou o pai, preservou a árvore.
  • Chamar no componente (DontDestroyOnLoad(this)) preserva o GameObject inteiro dele, igual.

Exemplo básico entre duas cenas

Arquivo Persistente.cs:

using UnityEngine;

public class Persistente : MonoBehaviour
{
    private void Awake()
    {
        if (transform.parent != null)
        {
            transform.SetParent(null);
        }

        DontDestroyOnLoad(gameObject);
    }
}

Coloque num objeto da cena Fase01, troque para Fase02 e veja na Hierarchy: o objeto está na cena DontDestroyOnLoad. O SetParent(null) protege contra a regra da raiz: se alguém organizar a cena e colocar o objeto dentro de uma pasta vazia, ele continua funcionando.

Se você tem experiência com Godot, o conceito equivalente é o autoload, que o post de autoload e singleton no Godot explica. A diferença importante é que, na Unity, quem cria o objeto persistente é a própria cena, e é daí que vem o bug a seguir.

O bug do objeto duplicado ao voltar ao menu

Cenário clássico: você coloca um GerenciadorDeAudio na cena MenuPrincipal com DontDestroyOnLoad.

  1. O jogo abre no menu. A cena cria o gerenciador A, que vira persistente.
  2. O jogador vai para Fase01. O menu é destruído, A sobrevive.
  3. O jogador volta para o menu. A cena MenuPrincipal é carregada de novo e cria o gerenciador B, porque o objeto está salvo no arquivo da cena. B também chama DontDestroyOnLoad.
  4. Agora A e B estão vivos. Duas músicas tocando por cima, dois sons por clique.

O DontDestroyOnLoad não sabe que A e B são "o mesmo". Para ele, são dois objetos diferentes que pediram para sobreviver.

Corrigindo com checagem de instância

A correção é o objeto novo perguntar, no Awake, se já existe um igual. Se existir, ele se destrói.

Arquivo GerenciadorPersistente.cs:

using UnityEngine;

public class GerenciadorPersistente : MonoBehaviour
{
    private static GerenciadorPersistente instancia;

    private void Awake()
    {
        if (instancia != null && instancia != this)
        {
            Destroy(gameObject);
            return;
        }

        instancia = this;
        transform.SetParent(null);
        DontDestroyOnLoad(gameObject);
    }

    private void OnDestroy()
    {
        if (instancia == this)
        {
            instancia = null;
        }
    }
}

O return depois do Destroy é essencial. O Destroy não remove o objeto na hora, só no fim do frame. Sem o return, o resto do Awake roda na cópia que está morrendo, e ela pode se registrar em evento, tocar música ou sobrescrever dados antes de sumir. Outro detalhe: a cópia nova é a que morre, não a antiga. A antiga já tem estado (a música no meio, a pontuação acumulada) e é ela que interessa.

Esse padrão (uma instância só, acessível por um campo estático) é a base do Singleton. O post de Singleton na Unity trata do lado do acesso: como outros scripts acham o gerenciador sem NullReferenceException e quando o padrão vira problema. Aqui o foco é a vida do objeto entre as cenas.

Música de fundo entre cenas com crossfade

A busca por unity background music quase sempre é este problema: a música reinicia toda vez que a cena muda, ou duplica ao voltar ao menu. O gerenciador abaixo resolve os dois e ainda troca de faixa com crossfade quando a fase pede outra música.

Crie um objeto GerenciadorDeMusica com duas AudioSources (ambas com Loop marcado e Play On Awake desmarcado) e este script.

Arquivo GerenciadorDeMusica.cs:

using System.Collections;
using UnityEngine;

public class GerenciadorDeMusica : MonoBehaviour
{
    public static GerenciadorDeMusica Instancia { get; private set; }

    [SerializeField] private AudioSource fonteA;
    [SerializeField] private AudioSource fonteB;
    [SerializeField] private float duracaoDoCrossfade = 1.5f;
    [SerializeField] private float volumeMaximo = 0.8f;

    private AudioSource fonteAtiva;
    private Coroutine rotinaDeCrossfade;

    private void Awake()
    {
        if (Instancia != null && Instancia != this)
        {
            Destroy(gameObject);
            return;
        }

        Instancia = this;
        transform.SetParent(null);
        DontDestroyOnLoad(gameObject);
        fonteAtiva = fonteA;
    }

    public void Tocar(AudioClip musica)
    {
        if (musica == null || fonteAtiva.clip == musica)
        {
            return;
        }

        AudioSource fonteNova = fonteAtiva == fonteA ? fonteB : fonteA;
        fonteNova.clip = musica;
        fonteNova.volume = 0f;
        fonteNova.Play();

        if (rotinaDeCrossfade != null)
        {
            StopCoroutine(rotinaDeCrossfade);
        }

        rotinaDeCrossfade = StartCoroutine(Crossfade(fonteAtiva, fonteNova));
        fonteAtiva = fonteNova;
    }

    private IEnumerator Crossfade(AudioSource saindo, AudioSource entrando)
    {
        float tempo = 0f;
        float volumeInicialSaindo = saindo.volume;

        while (tempo < duracaoDoCrossfade)
        {
            tempo += Time.unscaledDeltaTime;
            float t = tempo / duracaoDoCrossfade;
            saindo.volume = Mathf.Lerp(volumeInicialSaindo, 0f, t);
            entrando.volume = Mathf.Lerp(0f, volumeMaximo, t);
            yield return null;
        }

        saindo.Stop();
        saindo.clip = null;
        entrando.volume = volumeMaximo;
        rotinaDeCrossfade = null;
    }
}

Cada cena diz qual música quer com um componente simples, colocado em qualquer objeto da cena:

Arquivo MusicaDaCena.cs:

using UnityEngine;

public class MusicaDaCena : MonoBehaviour
{
    [SerializeField] private AudioClip musica;

    private void Start()
    {
        if (GerenciadorDeMusica.Instancia != null)
        {
            GerenciadorDeMusica.Instancia.Tocar(musica);
        }
    }
}

O comportamento que sai disso:

  • Fase com a mesma música da anterior: Tocar vê que o clip é o mesmo e não faz nada. A música segue sem cortar.
  • Fase com música diferente: a nova entra em uma fonte enquanto a outra sai, em 1.5 s.
  • Voltar ao menu: a cópia do gerenciador que o menu cria se destrói no Awake, e o MusicaDaCena do menu pede a música do menu ao gerenciador original.

O crossfade usa Time.unscaledDeltaTime para funcionar mesmo se a troca acontecer com o jogo pausado. A chamada fica no Start do MusicaDaCena porque, no Start, o Awake de todos os objetos da cena já rodou, incluindo o do gerenciador quando ele está na mesma cena.

Funciona no teste com duas cenas. No jogo inteiro, o gerenciador de música convive com o menu de opções que muda o volume, com a pausa que silencia o áudio, com o save que lembra a fase, e cada um desses sistemas quer sobreviver à troca de cena também. Organizar quem vive onde, sem virar uma pilha de objetos persistentes que ninguém entende, é a parte que mais trava quem estuda sozinho.

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

Cena bootstrap: alternativa mais limpa

A raiz do bug da duplicata é o objeto persistente morar numa cena que é carregada mais de uma vez. Se ele morar numa cena que carrega uma vez só, o problema some.

Cena bootstrap. Crie uma cena Bootstrap no índice 0 da Scene List, com todos os sistemas persistentes (música, áudio, save). O script dela marca tudo com DontDestroyOnLoad e carrega o menu logo em seguida. Como o jogo nunca volta para Bootstrap, nada duplica. A desvantagem: ao apertar Play numa fase qualquer no editor, os sistemas não existem, porque a bootstrap não rodou.

Prefab criado antes da primeira cena. Resolve essa desvantagem. Um método estático com RuntimeInitializeOnLoadMethod roda uma vez quando o jogo inicia, em qualquer cena:

Arquivo InicializadorDeSistemas.cs:

using UnityEngine;

public static class InicializadorDeSistemas
{
    [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)]
    private static void Inicializar()
    {
        GameObject prefab = Resources.Load<GameObject>("Sistemas");
        if (prefab == null)
        {
            Debug.LogWarning("Prefab Sistemas não encontrado em Assets/Resources");
            return;
        }

        GameObject sistemas = Object.Instantiate(prefab);
        sistemas.name = "Sistemas";
        Object.DontDestroyOnLoad(sistemas);
    }
}

Coloque o prefab Sistemas (com o GerenciadorDeMusica e o que mais precisar) em Assets/Resources. Ele nasce uma vez, antes de qualquer cena, inclusive quando você testa uma fase direto no editor. Como nenhuma cena tem uma cópia salva, não há duplicata para tratar.

Dados entre cenas com ScriptableObject ou classe estática

Nem tudo que precisa sobreviver é um objeto com comportamento. Pontuação, fase atual e moedas são só dados, e para dados não é preciso GameObject.

Classe estática. A mais simples. Variáveis estáticas não pertencem a nenhuma cena:

public static class DadosDaPartida
{
    public static int Moedas { get; set; }
    public static int FaseAtual { get; set; }

    public static void Zerar()
    {
        Moedas = 0;
        FaseAtual = 0;
    }
}

Lembre de chamar Zerar ao começar uma partida nova, ou o jogador começa o segundo jogo com as moedas do primeiro.

ScriptableObject. Um asset com os dados, referenciado pelos scripts que precisam dele. Funciona entre cenas porque o asset não é de cena nenhuma. Cuidado com uma pegadinha: no editor, mudanças feitas em tempo de execução num ScriptableObject continuam lá depois que você sai do Play Mode, porque você está editando o arquivo. Na build, os valores voltam ao original a cada execução. Zere os valores no começo da partida para o comportamento ser igual nos dois.

Quando NÃO usar DontDestroyOnLoad

  • Para coisas da fase. Inimigos, plataformas e o HUD de uma fase específica devem morrer com a fase.
  • Para salvar progresso. Objeto persistente vive enquanto o jogo está aberto. Fechou, perdeu. Progresso vai para arquivo.
  • Quando só existem dados. Use classe estática ou ScriptableObject, sem GameObject.
  • Como atalho para não passar referência. Se tudo vira persistente "para facilitar", o jogo fica cheio de objetos que se conhecem e ninguém sabe a ordem de inicialização. O post sobre padrões de projeto em jogos mostra alternativas, como eventos, que reduzem essa dependência.

Erros comuns com objetos persistentes

Aviso no Console dizendo que DontDestroyOnLoad só funciona em objetos raiz. O objeto está dentro de outro na Hierarchy. O SetParent(null) antes da chamada resolve.

Referência perdida depois de trocar de cena. O gerenciador persistente guardou no Inspector uma referência a um objeto da fase (a câmera, o jogador). Quando a fase troca, aquele objeto é destruído e a referência vira nula. Objetos persistentes não devem apontar para objetos de cena pelo Inspector; a cena nova é que se apresenta ao gerenciador, como o MusicaDaCena faz.

Evento chamando objeto destruído. Um objeto da fase se inscreveu num evento do gerenciador e não se desinscreveu ao ser destruído. Na fase seguinte, o evento tenta chamar um objeto que não existe e gera erro. Inscreva em OnEnable e desinscreva em OnDisable, sempre em par.

Dois gerenciadores ao testar no editor. Você tem o prefab criado por RuntimeInitializeOnLoadMethod e também uma cópia esquecida numa cena. A checagem de instância segura o problema, mas apague a cópia da cena para não confundir.

Próximo passo

Pratique com um caso completo: um prefab Sistemas criado por RuntimeInitializeOnLoadMethod com o gerenciador de música, três fases com duas músicas diferentes e um contador de moedas estático que zera ao voltar ao menu. Se a música não corta entre as fases iguais, faz crossfade nas diferentes e nada duplica depois de dez voltas ao menu, o padrão está dominado.

Perguntas frequentes

Por que o DontDestroyOnLoad duplica o objeto quando volto ao menu?

Porque a cena do menu tem o objeto original salvo nela. Ao voltar, a cena cria uma cópia nova, e a antiga continua viva. Corrija checando no Awake se já existe uma instância e destruindo a cópia nova.

DontDestroyOnLoad funciona em objeto filho?

Não. Segundo a documentação, ele só funciona em GameObjects raiz ou em componentes de GameObjects raiz. Tire o objeto do pai com transform.SetParent(null) antes de chamar, ou coloque-o na raiz da cena.

Como manter a música tocando ao trocar de cena na Unity?

Coloque a AudioSource num objeto com DontDestroyOnLoad e a checagem de instância. Cada fase só pede a música que quer; se for a mesma que já está tocando, nada muda, e se for outra, o gerenciador faz um crossfade.

Qual a alternativa ao DontDestroyOnLoad na Unity?

Uma cena bootstrap carregada primeiro com os sistemas do jogo, um prefab criado por RuntimeInitializeOnLoadMethod antes da primeira cena, ou dados guardados em ScriptableObject ou classe estática, quando o que precisa sobreviver é só informação.