Voltar para o Blog
Quest Log

Autoload e Singletons no Godot: Guia Completo de Estado Global

Ilustração de um node central conectado a várias cenas representando um singleton autoload no Godot

Aprenda autoload no Godot 4: crie singletons, gerencie estado global com o padrão GameManager, monte um event bus e saiba quando não usar globals.

Autoload e Singletons no Godot: Guia Completo de Estado Global

Toda pessoa que faz jogo no Godot esbarra na mesma pergunta cedo: onde eu guardo o score quando troco de cena? A cena morre, o node morre, a variável morre junto. A resposta da engine pra isso é o autoload do Godot, um node que carrega junto com o jogo e sobrevive a qualquer troca de cena. É a versão do Godot pro padrão singleton, e é uma das ferramentas mais úteis e mais mal usadas da engine ao mesmo tempo.

Neste tutorial eu mostro como criar um autoload, como montar um GameManager de verdade (score, vidas, troca de cena, save), como usar um event bus pra desacoplar suas cenas, e a parte que quase ninguém ensina: quando NÃO usar, porque global demais vira espaguete rápido.

O que é um autoload no Godot

Quando você registra um script ou cena como autoload, o Godot instancia ele como filho direto da raiz da árvore (/root) assim que o jogo abre, antes da sua cena principal. A partir daí:

  • Ele existe durante a vida inteira do jogo
  • Trocar de cena com change_scene_to_file() não destrói ele
  • Qualquer script acessa ele direto pelo nome registrado, sem get_node(), sem caminho

É por isso que o pessoal chama de singleton: existe uma instância só, global, acessível de qualquer lugar. Tecnicamente não é o singleton clássico de livro de design pattern (não tem nada impedindo você de instanciar o script de novo na mão), mas na prática cumpre o mesmo papel.

Registrando um autoload

Crie um script novo, por exemplo game_manager.gd, herdando de Node:

extends Node

var score: int = 0

Depois registre em Projeto > Configurações do Projeto > Autoload (nas versões mais recentes do Godot 4 a aba fica dentro de Globals). Aponte pro arquivo, dê o nome GameManager e confirme. Pronto. De qualquer script do projeto:

func _on_coin_collected():
    GameManager.score += 1

Sem preload, sem referência, sem nada. O nome que você registrou virou um identificador global.

Um detalhe que muda bastante coisa: o autoload pode ser uma cena (.tscn), não só um script. Isso é ótimo pra música, por exemplo. Uma cena com um node raiz e um AudioStreamPlayer filho, registrada como autoload Music, toca a trilha sem interrupção enquanto o jogador navega entre menu, fase e game over. A música não engasga na troca de cena porque o player nunca foi destruído.

O padrão GameManager

O uso mais comum de autoload é centralizar o estado da partida. Aqui está um GameManager enxuto e funcional, do tipo que eu uso como ponto de partida em projeto pequeno e médio:

extends Node

const SAVE_PATH = "user://save.json"

var score: int = 0
var high_score: int = 0
var lives: int = 3

func add_score(points: int) -> void:
    score += points
    if score > high_score:
        high_score = score
        save_game()

func lose_life() -> void:
    lives -= 1
    if lives <= 0:
        game_over()

func game_over() -> void:
    get_tree().change_scene_to_file("res://ui/game_over.tscn")

func new_game() -> void:
    score = 0
    lives = 3
    get_tree().change_scene_to_file("res://levels/level_01.tscn")

func save_game() -> void:
    var data = {"high_score": high_score}
    var file = FileAccess.open(SAVE_PATH, FileAccess.WRITE)
    file.store_string(JSON.stringify(data))

func load_game() -> void:
    if not FileAccess.file_exists(SAVE_PATH):
        return
    var file = FileAccess.open(SAVE_PATH, FileAccess.READ)
    var data = JSON.parse_string(file.get_as_text())
    if data is Dictionary and data.has("high_score"):
        high_score = data["high_score"]

func _ready() -> void:
    load_game()

Repare em três escolhas aqui:

A troca de cena mora no manager. Se a lógica de "morreu, vai pra tela de game over" estivesse dentro do player, o player precisaria conhecer o caminho da cena de game over. No manager, o player só chama GameManager.lose_life() e não sabe (nem deve saber) o que acontece depois.

O save usa user://. Esse caminho aponta pra pasta de dados do usuário no sistema operacional, que é onde arquivos graváveis devem ficar. Nunca tente salvar em res://: em jogo exportado, o pacote de recursos é somente leitura.

O _ready() do autoload roda antes de tudo. Autoloads entram na árvore antes da cena principal, então carregar o save aqui garante que o high score já existe quando o menu desenhar.

Acessando o autoload com segurança

Como o autoload é um node de verdade dentro da árvore, ele tem acesso a get_tree(), pode usar await, criar timers, tocar som. É um script normal, só que imortal. E como ele é registrado por nome, o editor te dá autocomplete em GameManager. como se fosse uma classe.

Event bus: sinais globais pra desacoplar cenas

O segundo padrão clássico de autoload no Godot é o event bus (ou signal bus): um autoload que não guarda quase nada, só declara sinais. Ele resolve um problema chato da árvore de cenas: como o inimigo lá no fundo da fase avisa a HUD, que está em outro galho completamente diferente, que o player ganhou pontos?

Sem event bus, você acaba fazendo correntes de get_parent().get_parent() ou caminhos absolutos frágeis que quebram quando você reorganiza a cena. Com event bus, ninguém precisa conhecer ninguém.

Crie events.gd e registre como autoload Events:

extends Node

signal coin_collected(value: int)
signal player_died
signal level_completed(level_name: String)

Quem causa o evento, emite:

# Na moeda, quando o player encosta:
func _on_body_entered(body):
    if body.is_in_group("player"):
        Events.coin_collected.emit(10)
        queue_free()

Quem se importa com o evento, escuta:

# Na HUD:
func _ready():
    Events.coin_collected.connect(_on_coin_collected)

func _on_coin_collected(value: int) -> void:
    GameManager.add_score(value)
    score_label.text = str(GameManager.score)

A moeda não conhece a HUD. A HUD não conhece a moeda. Você pode deletar a HUD inteira e a moeda continua funcionando sem erro. Esse desacoplamento é o que deixa o projeto reorganizável depois que ele cresce.

A regra de bolso que eu sigo: sinal sobe, chamada desce. Quando um node filho precisa avisar algo pra cima ou pra longe, ele emite sinal. Quando um node pai precisa mandar um filho fazer algo, ele chama o método direto. O event bus é a versão "pra longe" do sinal.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Pause global: um caso real de autoload

Um exemplo onde o autoload brilha é o sistema de pause. Quando você faz get_tree().paused = true, todos os nodes da árvore param de processar, inclusive o script que despausaria o jogo. A saída é colocar a lógica de pause num autoload com process_mode configurado pra continuar rodando:

extends Node

func _ready() -> void:
    process_mode = Node.PROCESS_MODE_ALWAYS

func _unhandled_input(event: InputEvent) -> void:
    if event.is_action_pressed("pause"):
        get_tree().paused = not get_tree().paused

PROCESS_MODE_ALWAYS significa "eu processo mesmo com a árvore pausada". O resto do jogo congela, o autoload continua escutando o input, e o mesmo botão pausa e despausa. O menu de pause em si também precisa de PROCESS_MODE_ALWAYS (ou PROCESS_MODE_WHEN_PAUSED) pra responder a cliques.

Quando NÃO usar autoload

Aqui está a parte que separa projeto organizado de projeto que vira pântano. Autoload é viciante: tudo acessível de qualquer lugar parece produtividade. Até o dia em que você não sabe mais quem mexe em quê.

Os sintomas de abuso:

Tudo virou global. Se o seu projeto tem PlayerData, EnemyManager, WeaponManager, UIManager, LevelManager e mais oito autoloads, qualquer script pode alterar qualquer estado de qualquer lugar. Quando um bug aparece ("por que o score zerou?"), você tem o projeto inteiro como suspeito.

Dependência escondida. Uma cena que usa GameManager por dentro não funciona sozinha. Você abre ela isolada pra testar (F6 no editor) e ela quebra, porque o autoload até existe, mas o estado que ela espera não foi montado. Quanto mais globals uma cena consome, menos testável ela é.

Estado de fase no lugar de estado de jogo. Score total, high score, configurações, progresso: isso é estado de jogo, sobrevive entre cenas, faz sentido no autoload. Quantos inimigos restam na fase atual, posição do player, timer da fase: isso é estado da fase, morre com ela, e deve viver na própria cena da fase. Misturar os dois no autoload te obriga a lembrar de resetar tudo na mão a cada fase, e esquecer um reset é bug garantido.

As alternativas pra cada caso:

  • Constantes e helpers puros que não precisam da árvore: use class_name com static var e static func (disponível no GDScript do Godot 4). Config.music_volume funciona globalmente sem ocupar um autoload e sem ser um node.
  • Dados que descrevem coisas (status de arma, definição de inimigo, tabela de loot): use Resource customizado e arraste no Inspector. Resource é compartilhável, serializável e editável sem código.
  • Comunicação local entre nodes da mesma cena: sinal direto e referência via @export var, sem passar pelo bus global.

Meu critério prático: o autoload ganha a vaga quando o dado precisa sobreviver à troca de cena ou quando o serviço precisa estar acessível do jogo inteiro (áudio, save, pause, transição de cena, event bus). Se não passa nesse teste, vai pra cena ou pra um Resource.

Resumo do que importa

Autoload é o mecanismo do Godot pra singleton: um node imortal, filho de /root, acessível por nome de qualquer script. Os três usos que pagam a conta são o GameManager (estado de jogo e troca de cena), o event bus (sinais globais pra desacoplar cenas) e serviços de jogo inteiro como música, pause e save.

A disciplina que mantém isso saudável cabe numa frase: globalize o que sobrevive entre cenas, deixe na cena o que morre com ela. Dois ou três autoloads bem definidos carregam um projeto comercial inteiro. Doze autoloads carregam um problema.

Se quiser fixar, pega um projeto seu que perde o score na troca de cena e refatora: GameManager com score e troca de cena, Events com dois sinais, HUD escutando em vez de ser chamada por caminho. É uma tarde de trabalho e o projeto sai do outro lado muito mais fácil de crescer.