Every game needs some global state โ player progress, run seed, settings, audio volumes, save files. Godot's mechanism for this is the autoload, which is a singleton that lives at the top of the scene tree for the lifetime of the game. Used well, it's a clean dependency boundary. Used badly, it's the God object.
An autoload is a scene or script that Godot instantiates automatically when the game starts and makes available via a global name.
scripts/global.gd:extends Node
var run_seed: int = 0
var player_deck: Array[CardData] = []
var gold: int = 0
func reset_run() -> void:
run_seed = randi()
gold = 0
seed(run_seed)
print("[Global] new run, seed=", run_seed)
res://scripts/global.gd. Name: Global. Add.Global.gold += 10, Global.reset_run(), etc.That's it. The autoload node is called Global (or whatever you named it) and lives at /root/Global.
Yes:
No:
class_name work better)Sometimes two unrelated systems need to know about an event. Coupling them via direct references is ugly. An event bus autoload fixes this:
# scripts/events.gd โ register as autoload "Events"
extends Node
signal card_played(card: Card, word: String)
signal duel_won(reward: int)
signal run_ended(score: int)
signal setting_changed(key: String, value: Variant)
Anywhere in the game:
# emit
Events.card_played.emit(card, "LEXICON")
# listen
func _ready() -> void:
Events.duel_won.connect(_on_duel_won)
This gives you pub/sub decoupling across the whole game. Use it for broadcast-style events. For point-to-point communication (Hand tells DuelController about a play), just wire signals directly โ don't route through the bus.
For a turn-based word/card game you have two kinds of save data:
Godot ships with two save mechanisms. Know both.
func save_game() -> void:
var data := {
"gold": Global.gold,
"unlocks": Global.unlocks,
"version": 1,
}
var f := FileAccess.open("user://save.json", FileAccess.WRITE)
f.store_string(JSON.stringify(data, " "))
f.close()
func load_game() -> void:
if not FileAccess.file_exists("user://save.json"):
return
var f := FileAccess.open("user://save.json", FileAccess.READ)
var text := f.get_as_text()
f.close()
var parsed = JSON.parse_string(text)
if parsed is Dictionary:
Global.gold = parsed.get("gold", 0)
Global.unlocks = parsed.get("unlocks", [])
Pros: readable, portable, easy to version. Cons: you hand-write the serialization. Best for simple metadata.
user:// is the per-app writable location โ on Android it's the app's private data directory, on desktop it's ~/.local/share/godot/app_userdata/ProjectName/. You never need to know the physical path; just use the user:// prefix.
# scripts/save_data.gd
class_name SaveData
extends Resource
@export var gold: int = 0
@export var unlocks: Array[String] = []
@export var current_run: RunData # nested Resource
@export var version: int = 1
# Saving
var save := SaveData.new()
save.gold = Global.gold
save.unlocks = Global.unlocks
ResourceSaver.save(save, "user://save.tres")
# Loading
var save: SaveData = load("user://save.tres")
if save:
Global.gold = save.gold
Global.unlocks = save.unlocks
Pros: typed, uses @export, handles nested Resources automatically. Cons: Godot-specific format. Slightly harder to debug-edit by hand.
For Lexicon Duel, I'd use ResourceSaver for the current run (it's complex, has many typed Resources nested) and JSON for meta-progression (small, human-editable for debugging).
You will change the save schema mid-development. Always include a version field:
func load_game() -> void:
var data = _read_save()
if not data: return
var v = data.get("version", 0)
if v < 1:
_migrate_v0_to_v1(data)
if v < 2:
_migrate_v1_to_v2(data)
# apply to Global
On the first production bug, you'll be grateful. The alternative โ "please delete save.json and start over" in a patch note โ is what ships indie games to Steam with 40% refund rates.
user:// on Android is wiped if the user clears app data but survives updates. That's usually what you want. Settings and unlocks go here. If you want true cloud sync (Google Play Games Services, iCloud), that's a Module 7 conversation โ we'll point you at the plugins.
scripts/global.gd with a gold property. Register as autoload.scripts/events.gd with a duel_won(reward: int) signal. Register as autoload "Events".Events.duel_won.emit(25) when you set state to VICTORY (just for testing).global.gd, connect to Events.duel_won in _ready and add to gold.