Basic · The GameState autoload
30 minYou are building: a singleton that knows every available character and remembers what the local player picked. Any script in the game can reach it by name.
1 · What an autoload is
An autoload is a script Godot loads once, before your main scene, and keeps for the whole run. It is reachable from anywhere by its name — no node path, no get_parent().get_parent().
Use one for things that belong to the game, not to a scene. Two qualify here: GameState (who am I playing?) and, in lesson 16, NetworkManager.
2 · Write the script
Create res://scripts/autoload/game_state.gd:
extends Node
const CHARACTER_DIR := "res://resources/characters"
var characters: Array[CharacterData] = []
var player_name: String = "Player"
var selected_character_id: String = ""
func _ready() -> void:
reload_characters()
func reload_characters() -> void:
characters.clear()
var dir := DirAccess.open(CHARACTER_DIR)
if dir == null:
push_warning("No character folder at %s" % CHARACTER_DIR)
return
var files := dir.get_files()
files.sort()
for file in files:
# Exported builds rename resources to *.remap.
var file_name := file.trim_suffix(".remap")
if not (file_name.ends_with(".tres") or file_name.ends_with(".res")):
continue
var res: Resource = load(CHARACTER_DIR.path_join(file_name))
if res is CharacterData:
characters.append(res)
func get_character(character_id: String) -> CharacterData:
for c in characters:
if c.id == character_id:
return c
return null
func get_character_or_default(character_id: String) -> CharacterData:
var found := get_character(character_id)
if found != null:
return found
return characters[0] if not characters.is_empty() else null3 · Register it
- Project → Project Settings → Globals → Autoload.
- Choose
res://scripts/autoload/game_state.gd. - Set the Node Name to GameState — spelled exactly, because that is how every script will name it.
- Click Add.
Order will matter shortly
In lesson 16 you add NetworkManager, and it reads the chosen character out of GameState when hosting. So GameState must sit above it in the autoload list. Put it in first and the order takes care of itself.
4 · Three details worth copying into your own projects
files.sort()makes the order the same every run. That is exactly why the files are named01_,02_,03_.- The
.remapline matters once you export the game. Godot renames resources during export, and a folder scan that ignores this works perfectly in the editor and finds nothing in the shipped build. res is CharacterDatameans a stray file in the folder is skipped rather than crashing the game.
5 · Test it
Add this temporarily to the end of _ready(), run the game, then remove it:
for c in characters:
print("%s (%s) run %.1f" % [c.display_name, c.id, c.run_speed])Done when: F5 prints one line per character, in filename order, with no warnings.
Challenge 1 · No code to add a character 🟢 Easy
8 minProve the promise from lesson 4.
- Run the game and note how many characters print.
- Stop it. Make a new
CharacterDatafile called05_healer.tres. Fill in an id and a name only. - Run again without touching a single script.
- Now rename it to
00_healer.tresand run once more. Where does it appear?
Done when: the new character prints without any code change, and you can explain why renaming moved it.
Challenge 2 · Make the lookups safe 🟡 Medium
12 minget_character() returns null when nothing matches. That is on purpose — but every caller must cope with it.
- In your test script, call
GameState.get_character("dragon")and print the result. - Then print
GameState.get_character("dragon").display_nameand read the error carefully. Write down the exact wording. - Replace it with
get_character_or_default("dragon")and show it returns the first character instead. - Add a
has_character(id: String) -> boolfunction to GameState and use it to guard the unsafe call.
Answer in one sentence: when should a function return null, and when should it hand back a default?
Done when: you have caused the error on purpose, then made the same line safe two different ways.
Challenge 3 · Remember the choice 🟠 Harder
15 minGameState already holds player_name and selected_character_id, but they reset every launch. Make them stick.
- Add save and load functions to GameState:
const SETTINGS_PATH := "user://settings.cfg"
func save_settings() -> void:
var cfg := ConfigFile.new()
cfg.set_value("player", "name", player_name)
cfg.set_value("player", "character", selected_character_id)
cfg.save(SETTINGS_PATH)
func load_settings() -> void:
var cfg := ConfigFile.new()
if cfg.load(SETTINGS_PATH) != OK:
return # First run: keep the defaults.
player_name = cfg.get_value("player", "name", player_name)
selected_character_id = cfg.get_value(
"player", "character", selected_character_id)- Call
load_settings()at the end of_ready(), afterreload_characters(). - Set a name in code, call
save_settings(), quit, and run again. The name should survive. - Find the file on your machine. In Godot, use Project → Open User Data Folder.
Validate what you load
A saved selected_character_id may name a character you have since deleted. After loading, check it with get_character() and clear it if it is gone.
Done when: the name survives a restart, you have found settings.cfg on disk, and a deleted character in the save does not break startup.
Extra · Go further with autoloads 🔴 Open-ended
no limit- Add a
characters_reloadedsignal to GameState and emit it at the end ofreload_characters(). In lesson 14 the select screen can listen to it. - Read the Godot docs on DirAccess. What happens to a
res://folder that contains only files Godot does not import? - Write one paragraph for your design document: why is scanning a folder better than a list in code — and what does it cost?
- Try the same idea on something else of yours: a folder of sound effects, or of map ideas.