Godot do Zero: Salvamento simples de progresso e configurações

Capítulo 15

Tempo estimado de leitura: 10 minutos

+ Exercício

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.cfg para configurações
  • user://progress.json para 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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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_REQUEST ou tree_exiting para 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 Node
const 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 = $SfxSlider
func _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 = $BestScoreLabel
func _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 Node
var current_level_number := 1
func 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 > unlocked

Salvando 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_string precisa retornar um Dictionary.
  • Fallback: se inválido, restaurar defaults.
  • Backup: salvar uma cópia .corrupted.bak para 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 jogoO que fazerQuando
Inicializaçãoload_settings(), load_progress(), apply_settings()No _ready() do autoload
Menu de opçõesPreencher sliders com SaveManager.settingsAo abrir o menu
Alterar volumeset_music_volume_db/set_sfx_volume_db (aplica e salva)No value_changed do slider
Fim de faseupdate_progress_after_level(level, score)Ao completar a fase
Seleção de fasesHabilitar botões até last_levelAo abrir menu
Fechar jogosave_settings() e save_progress()Ao receber close request

Agora responda o exercício sobre o conteúdo:

Qual é a principal vantagem de separar o salvamento de configurações e de progresso em arquivos diferentes (por exemplo, settings.cfg e progress.json)?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Separar configurações e progresso ajuda a organizar o sistema de salvamento, evita sobrescritas indevidas e permite apagar/resetar apenas uma categoria de dados sem afetar a outra.

Próximo capitúlo

Godot do Zero: Organização de projeto e boas práticas iniciais em GDScript

Arrow Right Icon
Capa do Ebook gratuito Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript
88%

Godot do Zero: Criando seu Primeiro Jogo 2D com GDScript

Novo curso

17 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.