O que vamos salvar (e por quê)
Em um jogo 2D, “salvar” normalmente significa persistir dados entre execuções do jogo. Aqui vamos separar em dois grupos:
- Configurações: preferências do jogador (ex.: volume de música/SFX, tela cheia, idioma). São dados que podem mudar a qualquer momento e devem ser aplicados imediatamente.
- Progresso: estado do jogo (ex.: última fase desbloqueada, melhor pontuação). Geralmente é atualizado em momentos específicos (fim de fase, game over, etc.).
Separar configurações e progresso ajuda a manter o código organizado e evita sobrescrever dados indevidamente.
Onde salvar: user:// e caminhos seguros
Na Godot, o caminho recomendado para salvar dados do usuário é user://. Ele aponta para uma pasta apropriada do sistema operacional (por exemplo, AppData no Windows, Library/Application Support no macOS, etc.). Isso evita problemas de permissão e mantém os arquivos fora do diretório do projeto/exportação.
user://settings.cfgpara configuraçõesuser://progress.jsonpara progresso
Você pode usar um único arquivo para tudo, mas dois arquivos tornam mais fácil resetar apenas configurações ou apenas progresso.
Formatos de salvamento: ConfigFile vs JSON
ConfigFile (bom para configurações)
ConfigFile salva pares chave/valor em seções, com leitura e escrita simples. É ótimo para configurações porque é legível e bem estruturado.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
JSON (bom para progresso estruturado)
JSON é útil quando você quer salvar estruturas como dicionários e listas de forma direta. Também é fácil de versionar e migrar (adicionando campos novos com valores padrão).
Neste capítulo, vamos usar ConfigFile para configurações e JSON para progresso, mas você pode padronizar em um só formato se preferir.
Quando salvar: momentos recomendados
- Ao mudar opções no menu: salvamento imediato (ex.: slider de volume).
- Ao finalizar uma fase: salvar progresso (última fase e melhor pontuação).
- Ao trocar de cena: se a troca representa um checkpoint (ex.: sair da fase para o mapa).
- Ao fechar o jogo: salvar o que estiver “pendente”. Em Godot, você pode usar
NOTIFICATION_WM_CLOSE_REQUESToutree_exitingpara garantir uma última gravação.
Regra prática: salve cedo e com frequência para configurações; para progresso, salve em eventos importantes para evitar escrita excessiva.
Módulo SaveManager (autoload) com API clara
Vamos criar um nó singleton (autoload) chamado SaveManager para centralizar leitura/escrita. Isso evita duplicação e facilita chamar salvamento de qualquer cena (menu, HUD, fase).
1) Criando o script SaveManager.gd
Crie um arquivo res://scripts/SaveManager.gd (ou onde você organiza scripts) com o seguinte conteúdo:
extends Nodeconst SETTINGS_PATH := "user://settings.cfg"const PROGRESS_PATH := "user://progress.json"# Valores padrão (usados quando não existe arquivo ou está corrompido)var settings := { "audio": { "music_volume_db": -6.0, "sfx_volume_db": -6.0 }}var progress := { "last_level": 1, "best_score": 0}func _ready() -> void: # Carrega ao iniciar o jogo (antes de menus/fases usarem) load_settings() load_progress() apply_settings()func save_settings() -> void: var cfg := ConfigFile.new() cfg.set_value("audio", "music_volume_db", settings["audio"]["music_volume_db"]) cfg.set_value("audio", "sfx_volume_db", settings["audio"]["sfx_volume_db"]) var err := cfg.save(SETTINGS_PATH) if err != OK: push_warning("Falha ao salvar settings: %s" % err)func load_settings() -> void: var cfg := ConfigFile.new() var err := cfg.load(SETTINGS_PATH) if err != OK: # Arquivo não existe ou não pôde ser lido: mantém defaults return # Leitura com fallback para defaults (caso falte alguma chave) settings["audio"]["music_volume_db"] = float(cfg.get_value("audio", "music_volume_db", settings["audio"]["music_volume_db"])) settings["audio"]["sfx_volume_db"] = float(cfg.get_value("audio", "sfx_volume_db", settings["audio"]["sfx_volume_db"]))func save_progress() -> void: var data := { "last_level": progress["last_level"], "best_score": progress["best_score"] } var json_text := JSON.stringify(data) var file := FileAccess.open(PROGRESS_PATH, FileAccess.WRITE) if file == null: push_warning("Falha ao abrir arquivo de progresso para escrita.") return file.store_string(json_text) file.flush()func load_progress() -> void: if not FileAccess.file_exists(PROGRESS_PATH): return var file := FileAccess.open(PROGRESS_PATH, FileAccess.READ) if file == null: push_warning("Falha ao abrir arquivo de progresso para leitura.") return var text := file.get_as_text() var parsed := JSON.parse_string(text) if typeof(parsed) != TYPE_DICTIONARY: # Arquivo corrompido ou inválido: faz backup e volta ao default _backup_corrupted_file(PROGRESS_PATH, text) progress = {"last_level": 1, "best_score": 0} return # Migração simples: usa defaults se faltar campo progress["last_level"] = int(parsed.get("last_level", progress["last_level"])) progress["best_score"] = int(parsed.get("best_score", progress["best_score"]))func apply_settings() -> void: # Exemplo de aplicação: ajuste os buses conforme seu projeto var music_bus := AudioServer.get_bus_index("Music") if music_bus != -1: AudioServer.set_bus_volume_db(music_bus, settings["audio"]["music_volume_db"]) var sfx_bus := AudioServer.get_bus_index("SFX") if sfx_bus != -1: AudioServer.set_bus_volume_db(sfx_bus, settings["audio"]["sfx_volume_db"])func set_music_volume_db(value: float) -> void: settings["audio"]["music_volume_db"] = value apply_settings() save_settings()func set_sfx_volume_db(value: float) -> void: settings["audio"]["sfx_volume_db"] = value apply_settings() save_settings()func update_progress_after_level(level_reached: int, score: int) -> void: # last_level: guarda o maior nível alcançado progress["last_level"] = max(progress["last_level"], level_reached) # best_score: guarda a maior pontuação progress["best_score"] = max(progress["best_score"], score) save_progress()func _backup_corrupted_file(path: String, content: String) -> void: var backup_path := path + ".corrupted.bak" var f := FileAccess.open(backup_path, FileAccess.WRITE) if f != null: f.store_string(content) f.flush() push_warning("Arquivo corrompido detectado. Backup criado em: %s" % backup_path)2) Registrando como Autoload
No editor: Project > Project Settings > Autoload. Adicione o script SaveManager.gd com o nome SaveManager. Assim você poderá chamar SaveManager.save_settings() de qualquer lugar.
Integração com o Menu de Opções (configurações)
Suponha que você tenha um menu com dois sliders: MusicSlider e SfxSlider, ambos com valores em dB (por exemplo, de -30 a 0). O fluxo ideal é:
- Ao abrir o menu: preencher sliders com valores carregados.
- Ao mover o slider: aplicar e salvar imediatamente.
Exemplo de script do painel de opções
extends Control@onready var music_slider: HSlider = $MusicSlider@onready var sfx_slider: HSlider = $SfxSliderfunc _ready() -> void: # Inicializa UI com valores atuais music_slider.value = SaveManager.settings["audio"]["music_volume_db"] sfx_slider.value = SaveManager.settings["audio"]["sfx_volume_db"] # Conecta sinais (ou conecte pelo editor) music_slider.value_changed.connect(_on_music_changed) sfx_slider.value_changed.connect(_on_sfx_changed)func _on_music_changed(value: float) -> void: SaveManager.set_music_volume_db(value)func _on_sfx_changed(value: float) -> void: SaveManager.set_sfx_volume_db(value)Como set_music_volume_db e set_sfx_volume_db já chamam apply_settings() e save_settings(), você garante que a mudança é persistida imediatamente.
Integração com HUD (melhor pontuação e feedback)
No HUD, você pode exibir a melhor pontuação carregada e atualizar ao final da fase. Exemplo: um Label chamado BestScoreLabel.
extends CanvasLayer@onready var best_score_label: Label = $BestScoreLabelfunc _ready() -> void: best_score_label.text = "Melhor: %d" % SaveManager.progress["best_score"]func refresh_best_score() -> void: best_score_label.text = "Melhor: %d" % SaveManager.progress["best_score"]Quando a fase terminar e você calcular a pontuação final, atualize o progresso e depois atualize o HUD (se ele continuar visível) ou apenas salve antes de trocar de cena.
Salvando progresso ao finalizar fase (passo a passo)
1) Defina o evento de fim de fase
Você pode ter um nó controlador da fase (por exemplo, LevelController) que detecta vitória/derrota.
2) Atualize e salve
extends Nodevar current_level_number := 1func on_level_completed(final_score: int) -> void: SaveManager.update_progress_after_level(current_level_number, final_score) # Exemplo: ir para próxima cena/menu # get_tree().change_scene_to_file("res://scenes/Menu.tscn")Esse padrão garante que, mesmo que o jogo feche logo após a troca de cena, o progresso já foi gravado.
Carregando progresso para liberar fases no menu
No menu de seleção de fases, você pode habilitar botões até last_level.
extends Control@onready var level_buttons := [ $Levels/Level1Button, $Levels/Level2Button, $Levels/Level3Button]func _ready() -> void: var unlocked := SaveManager.progress["last_level"] for i in level_buttons.size(): var level_number := i + 1 level_buttons[i].disabled = level_number > unlockedSalvando ao fechar o jogo (garantia extra)
Mesmo salvando em eventos importantes, é útil garantir uma última gravação ao fechar. Você pode fazer isso no próprio SaveManager ou em um nó “Game” raiz que sempre existe.
Opção A: no SaveManager com notificação de fechamento
func _notification(what: int) -> void: if what == NOTIFICATION_WM_CLOSE_REQUEST: # Salva o que for necessário save_settings() save_progress() get_tree().quit()Use isso se você quer interceptar o fechamento e garantir gravação. Se seu jogo já chama quit() em algum lugar, você pode centralizar a saída em uma função que salva antes.
Ausência e corrupção de arquivo: estratégias práticas
Arquivo ausente
É o caso mais comum na primeira execução. A estratégia é simples: manter valores padrão e salvar quando o jogador alterar algo (configurações) ou quando houver progresso.
Arquivo corrompido (JSON inválido, conteúdo incompleto)
Para progresso, usamos:
- Validação:
JSON.parse_stringprecisa retornar umDictionary. - Fallback: se inválido, restaurar defaults.
- Backup: salvar uma cópia
.corrupted.bakpara diagnóstico.
Para configurações com ConfigFile, se load() falhar, você também mantém defaults. Se quiser ser mais agressivo, pode renomear o arquivo inválido e recriar um novo ao salvar.
Checklist rápido de integração
| Parte do jogo | O que fazer | Quando |
|---|---|---|
| Inicialização | load_settings(), load_progress(), apply_settings() | No _ready() do autoload |
| Menu de opções | Preencher sliders com SaveManager.settings | Ao abrir o menu |
| Alterar volume | set_music_volume_db/set_sfx_volume_db (aplica e salva) | No value_changed do slider |
| Fim de fase | update_progress_after_level(level, score) | Ao completar a fase |
| Seleção de fases | Habilitar botões até last_level | Ao abrir menu |
| Fechar jogo | save_settings() e save_progress() | Ao receber close request |