| name | godot-autoload-architecture |
| description | Structures Godot AutoLoad singletons: GameManager, SceneTransitioner, signal_bus, autoload_order, and Engine.register_singleton. Use when the user wants state that survives change_scene_to_file or a cross-scene manager. Trigger: project.godot autoload, PROCESS_MODE_ALWAYS. Not for JSON FileAccess save schemas (godot-save-load-systems). Do not use an Autoload for scene-local popups. Never stash pure static data that belongs in a class_name static var. |
| version | 1.0.1 |
Overview
AutoLoads are Godot's singleton pattern, allowing scripts to be globally accessible throughout the project lifecycle. This skill guides implementing robust, maintainable singleton architectures in Godot 4.7+.
Godot 4.7 Baseline
- Expert patterns in this skill target Godot 4.7+ (stable, 2026-06-18).
- Consult the Godot 4.7 migration guide when upgrading projects from 4.6.
- NEVER assume 4.6 defaults (stretch mode, audio area_mask, RichTextLabel percent flags) without checking 4.7 migration notes.
Available Scripts
Load these reference scripts from scripts/ when the agent needs the corresponding pattern:
| Script | When to Load |
|---|
scripts/static_state_manager.gd | High-performance global state via static var that doesn't need SceneTree presence |
scripts/safe_scene_switcher.gd | Robust scene transitioning with deferred freeing and root-level management |
scripts/autoload_init_order_diag.gd | Verifying/debugging the initialization sequence of Singletons |
scripts/global_event_bus.gd | Centralized signal router for decoupling systems (Achievements, Stats, Game Events) |
scripts/persistent_data_holder.gd | Data that must survive change_scene_to_file() (Inventory, Settings) |
scripts/lazy_loaded_singleton.gd | Memory-efficient singleton that instantiates on-demand rather than at boot |
scripts/debug_console_autoload.gd | CanvasLayer-based debug overlay accessible from any game context |
scripts/cross_autoload_comms.gd | Expert rules and safety checks for communication between multiple Singletons |
scripts/thread_safe_global_access.gd | Using Mutex and call_deferred to safely access global data from background threads |
scripts/autoload_reference_checker.gd | Validation utility to ensure Autoloads are correctly registered before attempting access |
When to Use
Good Use Cases:
- Game Managers: PlayerManager, GameManager, LevelManager
- Global State: Score, inventory, player stats
- Scene Transitions: SceneTransitioner for loading/unloading scenes
- Audio Management: Global music/SFX controllers
- Save/Load Systems: Persistent data management
Avoid AutoLoads For:
- Scene-specific logic (use scene trees instead)
- Temporary state (use signals or direct references)
- Over-architecting simple projects
Prerequisites
- Godot 4.7+ project open in the editor
- Windows host (PowerShell) is the primary environment
- Scripts organized under
res://autoloads/
Procedure
Step 1: Create the Singleton Script
# res://autoloads/game_manager.gd
extends Node
# Signals for global events
signal game_started
signal game_paused(is_paused: bool)
signal player_died
# Global state
var score: int = 0
var current_level: int = 1
var is_paused: bool = false
func _ready() -> void:
# Initialize autoload state
print("GameManager initialized")
func start_game() -> void:
score = 0
current_level = 1
game_started.emit()
func pause_game(paused: bool) -> void:
is_paused = paused
get_tree().paused = paused
game_paused.emit(paused)
func add_score(points: int) -> void:
score += points
Step 2: Register as AutoLoad
Project โ Project Settings โ AutoLoad
- Click the folder icon, select
game_manager.gd
- Set Node Name:
GameManager (PascalCase convention)
- Enable if needed globally
- Click "Add"
Verify in project.godot:
[autoload]
GameManager="*res://autoloads/game_manager.gd"
The * prefix makes it active immediately on startup.
Step 3: Access from Any Script
extends Node2D
func _ready() -> void:
# Access the singleton
GameManager.connect("game_paused", _on_game_paused)
GameManager.start_game()
func _on_button_pressed() -> void:
GameManager.add_score(100)
func _on_game_paused(is_paused: bool) -> void:
print("Game paused: ", is_paused)
Step 4: Organize AutoLoads by Feature
res://autoloads/
game_manager.gd
audio_manager.gd
scene_transitioner.gd
save_manager.gd
Step 5: Scene Transitioning Pattern
# res://autoloads/scene_transitioner.gd
extends Node
signal scene_changed(scene_path: String)
func change_scene(scene_path: String) -> void:
# Fade out effect (optional)
await get_tree().create_timer(0.3).timeout
get_tree().change_scene_to_file(scene_path)
scene_changed.emit(scene_path)
Step 6: Game State Machine
enum GameState { MENU, PLAYING, PAUSED, GAME_OVER }
var current_state: GameState = GameState.MENU
func change_state(new_state: GameState) -> void:
current_state = new_state
match current_state:
GameState.MENU:
# Load menu
pass
GameState.PLAYING:
get_tree().paused = false
GameState.PAUSED:
get_tree().paused = true
GameState.GAME_OVER:
# Show game over screen
pass
Step 7: Resource Preloading
# Preload heavy resources once
const PLAYER_SCENE := preload("res://scenes/player.tscn")
const EXPLOSION_EFFECT := preload("res://effects/explosion.tscn")
func spawn_player(position: Vector2) -> Node2D:
var player := PLAYER_SCENE.instantiate()
player.global_position = position
return player
Step 8: Lazy Initialization for Heavy Setup
Since AutoLoads are always loaded, avoid heavy initialization in _ready(). Use lazy initialization or explicit init functions:
var _initialized: bool = false
func initialize() -> void:
if _initialized:
return
_initialized = true
# Heavy setup here
Expert Architecture Patterns
Pattern 1: Service-Locator (Dynamic Registration)
Lightweight alternative to hardcoded Autoloads for dependency management.
- Why: Standard Autoloads must be
Node types, which incur memory and SceneTree overhead. For pure data systems, use Engine.register_singleton().
- The Script: Create a
ServiceLocator autoload at the top of the list.
- Registration: Register lightweight
RefCounted objects globally into the engine's scope.
# ServiceLocator.gd (Autoload)
func register_service(name: StringName, service: Object) -> void:
if not Engine.has_singleton(name):
Engine.register_singleton(name, service)
func _exit_tree() -> void:
# Cleanup to prevent dangling pointers
if Engine.has_singleton(&"CombatService"):
Engine.unregister_singleton(&"CombatService")
- Consumption: Other systems fetch services via
Engine.get_singleton(&"Name"). This bypasses the global variable namespace and allows for O(1) lookups of non-node systems.
Pattern 2: Singleton-Dependency-Diagram (Visual Mapping)
Managing the initialization order and coupling of global systems.
- The Rule: Autoloads are initialized sequentially in the order they appear in the Project Settings. Singletons at the top of the list MUST NOT depend on those below them.
- The Template: Use a Mermaid diagram to map out "Who initializes whom".
graph TD
subgraph SceneTree [SceneTree Execution]
A[OS & Servers Initialize] --> B
subgraph Autoloads [Project Settings: Autoload Order]
B[1. GlobalAudio.gd] -->|Initialized First| C[2. ServiceLocator.gd]
C -->|Initialized Second| D[3. QuestManager.gd]
end
D --> E[Current Active Scene]
end
%% Dependency Coupling
E -->|Queries| C
D -->|Registers self into| C
E -->|Plays sound via| B
- Verification: If
SaveManager (pos 1) calls PlayerManager (pos 5) in _ready(), it will receive a null reference. Always move managers with dependencies to the bottom of the list.
Pattern 3: Singleton-Health-Check (State Verification)
Automated verification to ensure global states are initialized correctly.
- The Pattern: Create a specialized test utility that verifies core singletons are non-null and have their default values reset.
- Validation: Use
assert() for debug-time crashes and is_instance_valid() for runtime safety checks.
func run_health_checks() -> void:
# 1. Verify Autoload Node Existence
var player_vars := get_tree().root.get_node_or_null("PlayerVariables")
assert(player_vars != null, "Critical Error: PlayerVariables Autoload missing!")
# 2. Verify Dynamic Service Registration
assert(Engine.has_singleton(&"CombatService"), "Critical Error: CombatService not registered!")
# 3. Verify Memory Safety
assert(is_instance_valid(player_vars), "Critical Error: PlayerVariables instance invalid!")
- Integration: Run these checks during game boot (if in debug mode) or within a CI/CD test suite like GUT to prevent state regression.
Best Practices
1. Use Static Typing
# โ
Good
var score: int = 0
# โ Bad
var score = 0
2. Emit Signals for State Changes
# โ
Good - allows decoupled listeners
signal score_changed(new_score: int)
func add_score(points: int) -> void:
score += points
score_changed.emit(score)
# โ Bad - tight coupling
func add_score(points: int) -> void:
score += points
ui.update_score(score) # Don't directly call UI
Pitfalls
NEVER Do in AutoLoad Architecture
- NEVER access AutoLoads in
_init() โ AutoLoads are initialized sequentially. Accessing one in _init() may find a null reference.
- NEVER modify a Singleton's size or children in
_ready() โ If multiple Singletons refer to each other's trees during boot, it can cause layout/sorting errors.
- NEVER store highly localized, scene-specific data in AutoLoads โ This creates "God Objects" and introduces global side effects that are hard to debug.
- NEVER use
Parent.method() calls from an Autoload โ Autoloads sit at the root. They are the ultimate "top". Use signals to talk to the active scene.
- NEVER use an Autoload for pure data containers โ If you don't need
_process() or signals, use a static var in a class_name script instead.
- NEVER create circular dependencies between Singletons โ If A needs B and B needs A, Godot will hang during the splash screen.
- NEVER free an Autoload node manually โ Removing a singleton from the root can leave dangling references that crash the engine.
- NEVER use AutoLoads for UI elements that aren't global โ Popups that only exist in one level should be in that level, not a global singleton.
- NEVER assume
get_tree().current_scene is accurate in _ready() โ In Autoloads, the active scene might still be initializing. Access it via get_tree().root.get_child(-1).
- NEVER skip
process_mode configuration โ If your global console or music manager needs to work while the game is paused, set process_mode = PROCESS_MODE_ALWAYS.
Verification
Verify Autoload Registration
Check project.godot for the autoload section:
Select-String -Path "project.godot" -Pattern "\[autoload\]"
Expected output:
[autoload]
GameManager="*res://autoloads/game_manager.gd"
Verify Initialization Order
Load scripts/autoload_init_order_diag.gd and run it during boot to verify the initialization sequence of all singletons.
Verify Singleton Health
Load scripts/autoload_reference_checker.gd to validate that all Autoloads are correctly registered before attempting access.
Runtime Health Check
func run_health_checks() -> void:
var player_vars := get_tree().root.get_node_or_null("PlayerVariables")
assert(player_vars != null, "Critical Error: PlayerVariables Autoload missing!")
assert(Engine.has_singleton(&"CombatService"), "Critical Error: CombatService not registered!")
assert(is_instance_valid(player_vars), "Critical Error: PlayerVariables instance invalid!")
Reference
Related Skills