This episode covers game state management: autoload singletons as global state storage, saving and loading data with JSON, ConfigFile, and custom formats, plus scene transitions, state machines, pause menus, and settings that persist across sessions.

In episode 8, you learned to design clean, responsive UI. But one question always follows: where do the score, level, and settings go when the game closes? Or when switching scenes, do all the variables just blow up? Episode 9 answers both: Game State & Data Persistence. You'll learn to store global state that survives across scenes, write data to disk, and manage the scene flow with discipline.
Episode 9's roadmap: we start with the autoload singleton as the home for global state, then save and load data using JSON and ConfigFile, touch on custom formats, then design scene transitions and state machines, and finish with a pause menu and settings that are truly persistent.
Godot scenes don't share variables — the moment a scene changes, all its nodes are destroyed and their variables vanish. The official solution is autoload. An autoload is a node loaded once at game start and living above all scenes. Because it always exists, every other scene can access it at any time.
Here's how: create a scene with a script (e.g. game_state.gd), then register it in Project Settings > Autoload. Godot names it automatically according to the file name, and that node is immediately available everywhere as a singleton — for example GameState.
extends Node
var player_name := "Player"
var score := 0
var unlocked_levels: Array[String] = ["level_1"]
func reset() -> void:
score = 0
player_name = "Player"
unlocked_levels = ["level_1"]From any scene, just write GameState.score += 10 or GameState.player_name = "Budi". The value changes globally right away and survives scene switches. This is the foundation of state management in Godot — where health, score, inventory, and settings live.
Info
Autoload is available everywhere because it becomes a child of the scene tree root, which is never unloaded when scenes change. Watch out for one pitfall: since it always exists, it never runs through _ready() again — so initialize state in _ready() once, or create a reset() function called manually when starting a new game.
State in memory only lasts while the application is running. To persist between sessions, you write to disk. Godot provides the user:// path — a safe, writable location on all platforms, hidden from the project directory. The most universal format for storing data is JSON: lightweight, human-readable, and easy to process.
Godot has built-in helpers: JSON.stringify(){gdscript} to turn a dictionary into a string, and JSON.parse_string(){gdscript} for the reverse. A simple save example:
extends Node
const SAVE_PATH := "user://savegame.json"
func save_game() -> void:
var data := {
"player_name": GameState.player_name,
"score": GameState.score,
"unlocked_levels": GameState.unlocked_levels,
}
var file := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
file.store_string(JSON.stringify(data))
file.close()
func load_game() -> void:
if not FileAccess.file_exists(SAVE_PATH):
return
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
var data: Dictionary = JSON.parse_string(file.get_as_text())
file.close()
GameState.player_name = data["player_name"]
GameState.score = data["score"]
GameState.unlocked_levels = data["unlocked_levels"]Notice the flow: open the file with FileAccess.open, write or read with store_string and get_as_text, then close. Always check file_exists before reading, and always verify that the JSON.parse_string result isn't null — corrupted JSON can return null and cause a crash if accessed directly.
If your data is simple key-value pairs like settings, there's a more convenient format: ConfigFile. Its structure resembles INI — made up of sections and keys. ConfigFile has a direct API without manual stringify:
extends Node
const SETTINGS_PATH := "user://settings.cfg"
func save_settings() -> void:
var config := ConfigFile.new()
config.set_value("video", "fullscreen", DisplayServer.window_get_mode())
config.set_value("audio", "music_volume", 0.8)
config.set_value("audio", "sfx_volume", 1.0)
config.save(SETTINGS_PATH)
func load_settings() -> void:
var config := ConfigFile.new()
var error := config.load(SETTINGS_PATH)
if error != OK:
return
var music: float = config.get_value("audio", "music_volume", 1.0)
AudioServer.set_bus_volume_db(
AudioServer.get_bus_index("Music"),
linear_to_db(music),
)get_value(section, key, default){gdscript} accepts a default value — so if a key doesn't exist, you still get a safe value instead of an error. Compare with JSON: ConfigFile fits flat, easily readable settings; JSON fits complex data like structured save games; and both can be combined in one project.
Sometimes the two formats above aren't enough: you need binary encoding, encryption, or checksums so data can't be easily tampered with. For that, Godot has FileAccess.open(path, FileAccess.WRITE), which can write binary data with store_32, store_float, and similar. A custom format gives full control, but it must be documented because you're the one managing compatibility across game versions.
One thing often forgotten: a save game is secret code that's visible. Players can open a JSON file and change their score. If that matters, add a simple checksum — for example store a sum value of all fields and validate it on load. For cloud saves, consider a third-party service; for local, user:// is enough.
Real games are never just one scene. Switching scenes should feel smooth, not dump the user into a sudden black screen. The simplest way is get_tree().change_scene_to_file(path){gdscript}:
extends Node
func go_to_level(path: String) -> void:
get_tree().paused = false
get_tree().change_scene_to_file(path)But there's a smoother pattern: wrap the switch with a fade animation. Because the autoload always exists, it's perfect for managing transitions — show a fullscreen ColorRect, fade in, switch the scene, then fade out. All without fearing the autoload being unloaded along with it. The result is a premium-feeling scene transition with code that stays simple.
Game state can get complicated fast: main menu, loading, gameplay, pause, game over, victory. If all of it is maintained with nested if-else, the code will be a mess. The solution is a state machine — modeling the game as a collection of states and the rules for transitioning between them.
The simplest GDScript implementation uses an enum and match:
extends Node
enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }
var state := GameState.MENU
func set_state(next: GameState) -> void:
var prev := state
state = next
print("state: %s -> %s" % [prev, next])
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("pause") and state == GameState.PLAYING:
set_state(GameState.PAUSED)
get_tree().paused = trueA state machine provides a single source of truth for game conditions: logic may only run in the right state, and transitions are managed in one place. From here you can evolve to bigger systems, but this enum + match pattern already handles the majority of games.
Let's put it all together: a pause menu with Control, state discipline, and persistent settings. When the player presses pause, we set get_tree().paused = true, but the pause menu node must keep running. The secret is process_mode:
extends CanvasLayer
func _ready() -> void:
process_mode = Node.PROCESS_MODE_ALWAYS
visible = false
func _unhandled_input(event: InputEvent) -> void:
if event.is_action_pressed("pause"):
visible = not visible
get_tree().paused = visibleWith PROCESS_MODE_ALWAYS, the node keeps processing input even while the tree is paused — that's why the "Resume", "Settings", and "Quit" buttons in the pause menu keep working. When Settings is pressed, save the changes to a ConfigFile (the pattern above) so the settings stay intact the next time the game opens. The autoload stores in-game state, ConfigFile stores preferences, and the two work together without collision.
Episode 9 settled the state business: the autoload singleton as the global state warehouse, save and load with JSON and ConfigFile plus custom formats and checksums, managed scene transitions, a state machine for disciplined game flow, and a pause menu with process_mode and persistent settings.
The key takeaways:
JSON.stringify and JSON.parse_string are the most universal save-load pair.ConfigFile is more convenient for key-value settings, and always give a default value when reading.user:// is the safe path for saving data on all platforms.match keeps the game flow structured.process_mode = PROCESS_MODE_ALWAYS to stay responsive while the game is paused.In episode 10, you'll bring the player's senses to life: Audio & Sound Design. We'll get to know AudioStreamPlayer and its 2D/3D versions, manage background music and sound effects, understand audio buses, volume, pitch shifting, and audio effects, and audio integration strategies that don't drain performance. See you in the world of sound!