Every game moves between menus and levels, and most ship a hard cut, a frozen frame on a
heavy load, or — the classic — a double change_scene() that leaves two scenes
half-swapped and the fade stuck on screen. SceneVault is the drop-in that fades cleanly,
loads asynchronously with real progress, blocks input while the screen is covered, and
guards against the double-transition race. Plus a small history stack so back() and
reload_current() just work.
Godot 4.x · GDScript · no dependencies · ~230 lines you can read in one sitting.
- Fades instead of cutting. Configurable colour and duration; the screen covers, the swap happens behind it, then it uncovers — no jarring flash.
- Blocks input mid-transition. The overlay is
MOUSE_FILTER_STOP, so a stray click during the change can't leak to the scene fading out or the one fading in. - Guards the double-load race. A second
change_scene()call while one is in flight is rejected, not queued into a half-swap. This is the bug the addon exists to kill. - Async loading with progress. Heavy scenes load on a worker thread and emit
load_progress(ratio)so you can drive a loading bar instead of freezing the main thread. - History that just works.
back()walks a capped stack of prior scenes;reload_current()re-enters the current one.
- Copy
addons/scene_vault/into your project. - Project → Project Settings → Plugins → enable SceneVault
(or add
scene_vault.gdas an autoload namedSceneVaultyourself).
func _ready() -> void:
SceneVault.fade_color = Color.BLACK
SceneVault.fade_duration = 0.4
# Anywhere you'd call get_tree().change_scene_to_file():
SceneVault.change_scene("res://levels/level_2.tscn")
# Drive a loading bar off the threaded load:
SceneVault.load_progress.connect(func(ratio): $Bar.value = ratio * 100.0)
# The safe point to e.g. reset state is when the screen is fully covered:
SceneVault.covered.connect(func(path): reset_hud())if SceneVault.can_go_back():
SceneVault.back() # return to the previous scene
await SceneVault.reload_current() # re-enter the current scene (e.g. on death)
SceneVault.clear_history() # e.g. when returning to the main menuSceneVault.change_scene("res://boss.tscn", true) # skip the fade| Call | Does |
|---|---|
change_scene(path, instant=false) |
Fade out, load, swap, fade in. Returns true on a completed swap. |
reload_current(instant=false) |
Re-enter the last scene SceneVault loaded. |
back(instant=false) |
Transition to the previous scene on the history stack. |
can_go_back() |
Whether the history stack is non-empty. |
clear_history() |
Empty the history stack. |
current_scene_path() |
Path of the scene SceneVault last loaded. |
is_transitioning() |
Whether a transition is in flight. |
fade_color / fade_duration |
Overlay colour and per-half duration (default black / 0.3s). |
overlay_layer |
CanvasLayer.layer for the overlay (default 128, on top). |
max_history |
Cap on the history stack (default 32). |
transition_started(path) · covered(path) (screen fully covered — safe to swap) ·
load_progress(ratio) · transition_finished(path) · transition_failed(path).
tests/test_scene_vault.gd is a headless suite covering the double-transition guard, the
history back-stack (including the max-history cap and same-scene dedup), the fade-overlay
configuration (full-rect cover, input blocked, starts transparent), and the threaded
load-status mapping across every ResourceLoader outcome. Run it:
godot --headless --path . -s tests/test_scene_vault.gd
SaveVault (saves), AudioVault (audio), and SettingsVault (options + keybinds) — the things every Godot game needs and most ship half-broken.
MIT. Drop it in a commercial game, no attribution required.