Voltar para o Blog
Quest Log

Singleton na Unity: GameManager do Jeito Certo

Uma peça central brilhante no meio de várias engrenagens menores de um jogo, todas ligadas a ela por linhas finas

Unity singleton em C# para um GameManager sem duplicata nem NullReference: Instance protegido, ordem Awake e Start, versão genérica e quando trocar por eventos.

Ferramenta:UnityC#

Singleton na Unity: GameManager do Jeito Certo

Um unity singleton é uma classe com uma única instância acessível de qualquer lugar por uma propriedade estática, normalmente GameManager.Instance. Na Unity, o jeito seguro de fazer em C# é a própria instância se registrar no Awake, destruir qualquer cópia que apareça depois e expor Instance com set privado, para nenhum outro script sobrescrever. Esses três cuidados resolvem os dois bugs que todo mundo encontra: GameManager duplicado e NullReferenceException porque o Instance ainda estava vazio.

Aqui você monta um GameManager singleton no Unity 6.3 LTS, entende por que a ordem entre Awake e Start causa o null, cria uma versão genérica reutilizável e vê quando o padrão começa a atrapalhar, com as alternativas.

O que é Singleton na Unity

Singleton é um padrão de projeto: garantir que uma classe tenha uma instância só e dar um ponto de acesso global a ela. O post sobre padrões de projeto em jogos coloca o Singleton ao lado de Observer, State e outros; aqui o foco é a implementação na Unity.

Na Unity, o Singleton ganha uma complicação que não existe em C# puro: o objeto é um MonoBehaviour, criado pela cena, não por você. Você não controla quando ele nasce (a cena cria), quantas vezes (a cada vez que a cena carrega) nem em que ordem em relação aos outros scripts. Por isso o Singleton clássico de C#, com construtor privado, não serve. O padrão adaptado se apoia no Awake.

Usos típicos: GameManager (estado da partida, pontuação, vitória e derrota), gerenciador de áudio, de save e de cenas. Se você vem do Godot, é o papel que o autoload cumpre, como explica o post de autoload e singleton no Godot.

GameManager singleton passo a passo

Arquivo GameManager.cs:

using System;
using UnityEngine;

public enum EstadoDoJogo
{
    Jogando,
    Pausado,
    Vitoria,
    Derrota
}

[DefaultExecutionOrder(-100)]
public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    public event Action<int> PontuacaoMudou;
    public event Action<EstadoDoJogo> EstadoMudou;

    public int Pontuacao { get; private set; }
    public EstadoDoJogo Estado { get; private set; } = EstadoDoJogo.Jogando;

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

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

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

    public void AdicionarPontos(int quantidade)
    {
        Pontuacao += quantidade;
        PontuacaoMudou?.Invoke(Pontuacao);
    }

    public void MudarEstado(EstadoDoJogo novoEstado)
    {
        if (Estado == novoEstado)
        {
            return;
        }

        Estado = novoEstado;
        EstadoMudou?.Invoke(Estado);
    }

    public void NovaPartida()
    {
        Pontuacao = 0;
        MudarEstado(EstadoDoJogo.Jogando);
        PontuacaoMudou?.Invoke(Pontuacao);
    }
}

Coloque num objeto vazio GameManager na primeira cena. Qualquer script acessa assim:

GameManager.Instance.AdicionarPontos(10);

O que cada parte protege:

  • { get; private set; }: todo mundo lê, só a própria classe escreve. Com um campo public static GameManager Instance;, qualquer script pode fazer GameManager.Instance = null por engano.
  • Destroy + return no Awake: se a cena com o GameManager carregar de novo, a cópia nova se destrói e para ali. O return impede que o resto do Awake rode na cópia que está morrendo, já que Destroy só remove o objeto no fim do frame.
  • OnDestroy limpando a referência: se o GameManager for destruído de propósito, Instance não fica apontando para um objeto morto.
  • Eventos em vez de o GameManager chamar a UI diretamente: a UI se inscreve em PontuacaoMudou; o GameManager não sabe que a UI existe.

O DontDestroyOnLoad está aqui porque um GameManager costuma atravessar fases. Se o seu vive só dentro de uma cena, remova a linha. Os cuidados de vida do objeto entre cenas, incluindo a cópia que reaparece ao voltar ao menu, estão detalhados no tutorial de DontDestroyOnLoad na Unity, que trata só desse ciclo de vida.

Evitando duplicata e instância nula

A duplicata já está resolvida pelo Awake. O null tem três origens diferentes:

  1. Ordem de execução: outro script acessa Instance antes do Awake do GameManager rodar. É o caso mais comum e tem seção própria abaixo.
  2. GameManager não existe na cena: você apertou Play direto na Fase02, mas o GameManager mora no MenuPrincipal. Coloque-o num prefab criado por RuntimeInitializeOnLoadMethod ou teste sempre a partir da cena inicial.
  3. Acesso durante o fechamento da cena: no OnDestroy de outros objetos, o GameManager pode já ter sido destruído. Cheque antes: if (GameManager.Instance != null).

Uma "solução" que aparece em tutorial antigo é buscar na cena quando Instance estiver nulo, com FindObjectOfType. No Unity 6 esse método está marcado como obsoleto; os substitutos são FindFirstObjectByType e FindAnyObjectByType (este mais rápido, quando qualquer instância serve). Mas buscar na cena esconde o problema real, que é a ordem. Prefira resolver a ordem.

Outra variação comum é o singleton "preguiçoso", que cria o próprio GameObject quando Instance é acessado e ainda não existe (new GameObject("GameManager").AddComponent<GameManager>()). Parece prático, mas tem dois efeitos colaterais. O primeiro: um objeto criado assim não tem nada configurado no Inspector, então campos como clipes de áudio e prefabs ficam vazios. O segundo: se algum script acessa Instance no próprio OnDestroy enquanto o jogo fecha ou o Play Mode termina, o singleton nasce de novo no meio da limpeza, e a Unity reclama de objetos que não foram limpos ao fechar a cena. Para um GameManager com dados configurados, o registro no Awake é mais previsível.

Existe ainda uma pegadinha do editor. Se você desligou o Domain Reload em Edit > Project Settings > Editor > Enter Play Mode Settings para o Play abrir mais rápido, as variáveis estáticas não são zeradas entre uma execução e outra. O Instance pode começar o Play apontando para um objeto destruído da execução anterior. A correção é zerar explicitamente, adicionando ao GameManager:

[RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)]
private static void ZerarEstatico()
{
    Instance = null;
}

Um GameManager funcionando é o começo. O difícil vem quando ele precisa coordenar pausa, troca de fase, save, áudio e UI num jogo que você termina e publica, sem virar uma classe de mil linhas que todo script chama. Organizar essa arquitetura sozinho, sem alguém para revisar o projeto, é onde muita gente trava e recomeça do zero.

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

Ordem de execução: Awake vs Start

A Unity garante esta ordem para os objetos de uma cena que carrega:

  1. Awake de todos os objetos ativos (e o OnEnable de cada um logo depois do seu Awake).
  2. Start de todos, antes do primeiro Update.

O que a Unity não garante é a ordem entre os Awake de objetos diferentes. Se o script da UI faz GameManager.Instance.PontuacaoMudou += ... no Awake ou no OnEnable, ele pode rodar antes do Awake do GameManager e encontrar null. Funciona no seu PC hoje e quebra amanhã, depois que você reorganiza a Hierarchy.

A regra prática: registre-se no Awake, consuma no Start. O singleton define Instance no próprio Awake; quem usa o singleton acessa no Start ou depois.

Quando o consumidor precisa mesmo usar no OnEnable (inscrição em evento, o caso mais comum), force o GameManager a rodar antes. Duas formas:

  • Atributo no código: [DefaultExecutionOrder(-100)] acima da classe, como no exemplo. Valores menores rodam antes. Fica junto do código e vai para o controle de versão sem esforço.
  • Project Settings: Edit > Project Settings > Script Execution Order, clique em +, escolha o GameManager e arraste para cima de Default Time.

Singleton genérico reutilizável

Com três ou quatro gerenciadores, repetir o mesmo Awake cansa. Uma classe base genérica resolve:

Arquivo Singleton.cs:

using UnityEngine;

public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    public static T Instance { get; private set; }

    [SerializeField] private bool persistirEntreCenas = true;

    protected virtual void Awake()
    {
        if (Instance != null && Instance != this)
        {
            Destroy(gameObject);
            return;
        }

        Instance = this as T;

        if (persistirEntreCenas)
        {
            transform.SetParent(null);
            DontDestroyOnLoad(gameObject);
        }
    }

    protected virtual void OnDestroy()
    {
        if (Instance == this)
        {
            Instance = null;
        }
    }
}

Uso:

Arquivo GerenciadorDeAudio.cs:

using UnityEngine;

public class GerenciadorDeAudio : Singleton<GerenciadorDeAudio>
{
    [SerializeField] private AudioSource fonteDeEfeitos;

    public void TocarEfeito(AudioClip clip)
    {
        fonteDeEfeitos.PlayOneShot(clip);
    }
}

Cada Singleton<T> com um T diferente tem o seu próprio Instance estático, então GerenciadorDeAudio.Instance e GameManager.Instance não se misturam. Se a classe filha precisar de Awake, use protected override void Awake() e chame base.Awake() primeiro, ou a checagem de duplicata não roda.

Quando o Singleton vira problema

O padrão é cômodo, e é aí que mora o risco. Sinais de que passou do ponto:

  • Todo script acessa três ou quatro singletons. Cada Instance é uma dependência escondida: olhando o Inspector do inimigo, você não vê que ele depende do GameManager, do Áudio e do Save.
  • O GameManager sabe de tudo. Ele chama a UI, toca som, troca cena, salva e controla inimigos. Vira o arquivo que ninguém quer abrir.
  • Testar uma fase isolada é impossível sem carregar todos os gerenciadores antes.
  • Bugs de ordem aparecem e somem conforme você mexe na cena.

Alternativas: eventos, ScriptableObject e referência no Inspector

Eventos. Em vez de o inimigo chamar GameManager.Instance.AdicionarPontos, ele dispara um evento "morri valendo 10 pontos", e quem se importa (o GameManager, a UI, as conquistas) escuta. O inimigo não conhece ninguém. O post de eventos na Unity cobre UnityEvent, Action e event em C# com essa ideia.

ScriptableObject como dado compartilhado. Um asset PontuacaoAtual referenciado pelo inimigo e pela UI. Os dois falam com o asset, não um com o outro, e não há ordem de Awake para dar errado, porque o asset existe antes de qualquer cena.

Referência pelo Inspector. Para coisas da mesma cena, arrastar a referência num campo [SerializeField] private é o mais explícito: a dependência aparece no Inspector e quebra de forma visível se faltar.

Um equilíbrio que funciona bem em jogos pequenos e médios: singleton para dois ou três sistemas realmente globais (música, save, cenas), eventos para o resto da comunicação, Inspector para tudo que é da mesma cena.

Próximo passo

Pegue o GameManager deste post e conte quantos scripts do seu projeto chamam GameManager.Instance. Para cada um, pergunte se ele precisa mandar uma ordem (aí o acesso direto faz sentido) ou só avisar que algo aconteceu (aí um evento é melhor). Troque dois desses por eventos e veja quanto o GameManager encolhe.

Perguntas frequentes

Por que o Instance do meu singleton está null na Unity?

Normalmente porque outro script acessou o Instance no Awake ou no OnEnable antes do Awake do GameManager rodar. Acesse no Start, ou faça o GameManager rodar antes com o atributo DefaultExecutionOrder com valor negativo.

Como fazer um singleton na Unity em C#?

Crie uma propriedade estática Instance com set privado. No Awake, se já existe uma instância diferente, destrua o objeto novo e retorne; senão, atribua Instance = this. Se o objeto precisa sobreviver entre cenas, chame DontDestroyOnLoad.

FindObjectOfType ainda funciona no Unity 6?

Ele está marcado como obsoleto. Use FindFirstObjectByType quando precisa de uma instância específica ou FindAnyObjectByType quando qualquer uma serve, que é mais rápido. Em singleton, prefira o registro no Awake e evite busca na cena.

Singleton é má prática na Unity?

Não para um punhado de sistemas realmente únicos, como música e save. Vira problema quando tudo é singleton e cada script acessa vários deles, criando dependências escondidas. Eventos e referências pelo Inspector resolvem boa parte desses casos.