- Scene Management
- Async loading, transitions, instance pooling, and caching define smooth scene workflows.
- Available Scripts
- async_scene_manager.gd
- Expert async scene loader with progress tracking, error handling, and transition callbacks.
- scene_pool.gd
- Object pooling for frequently spawned scenes (bullets, godot-particles, enemies).
- scene_state_manager.gd
- Preserves and restores scene state across transitions using "persist" group pattern.
- MANDATORY - For Smooth Transitions
- Read async_scene_manager.gd before implementing loading screens. NEVER Do in Scene Management NEVER use load() in gameplay code — var scene = load("res://level.tscn") blocks entire game until loaded. Use preload() OR ResourceLoader.load_threaded_request() . NEVER forget to check THREAD_LOAD_FAILED — Async loading without status check? Silent failure = black screen. MUST handle THREAD_LOAD_FAILED state. NEVER change scenes without cleaning up — Active timers/tweens persist across scenes = memory leak + unexpected behavior. Stop timers, disconnect signals before transition. NEVER use get_tree().change_scene_to_file() during _ready() — Changing scene in _ready() = crash (scene tree locked). Use call_deferred("change_scene") . NEVER instance scenes without null check — var obj = scene.instantiate() if scene load failed? Crash. Check scene != null first. NEVER forget queue_free() on dynamic instances — Spawned 1000 enemies, all dead, but not freed? Memory leak. Use queue_free() OR instance pooling.
Instant scene change
get_tree ( ) . change_scene_to_file ( "res://levels/level_2.tscn" )
Or with packed scene
var next_scene := load ( "res://levels/level_2.tscn" ) get_tree ( ) . change_scene_to_packed ( next_scene ) Scene Transition with Fade
scene_transitioner.gd (AutoLoad)
extends CanvasLayer signal transition_finished func change_scene ( scene_path : String ) -> void :
Fade out
$AnimationPlayer . play ( "fade_out" ) await $AnimationPlayer . animation_finished
Change scene
get_tree ( ) . change_scene_to_file ( scene_path )
Fade in
$AnimationPlayer . play ( "fade_in" ) await $AnimationPlayer . animation_finished transition_finished . emit ( )
Usage:
SceneTransitioner . change_scene ( "res://levels/level_2.tscn" ) await SceneTransitioner . transition_finished Async (Background) Loading extends Node var loading_status : int = 0 var progress := [ ] func load_scene_async ( path : String ) -> void : ResourceLoader . load_threaded_request ( path ) while true : loading_status = ResourceLoader . load_threaded_get_status ( path , progress ) if loading_status == ResourceLoader . THREAD_LOAD_LOADED : var scene := ResourceLoader . load_threaded_get ( path ) get_tree ( ) . change_scene_to_packed ( scene ) break
Update loading bar
print ( "Loading: " , progress [ 0 ] * 100 , "%" ) await get_tree ( ) . process_frame Loading Screen Pattern
loading_screen.gd
extends Control @ onready var progress_bar : ProgressBar = $ProgressBar func load_scene ( path : String ) -> void : show ( ) ResourceLoader . load_threaded_request ( path ) var progress := [ ] var status : int while true : status = ResourceLoader . load_threaded_get_status ( path , progress ) if status == ResourceLoader . THREAD_LOAD_LOADED : var scene := ResourceLoader . load_threaded_get ( path ) get_tree ( ) . change_scene_to_packed ( scene ) break elif status == ResourceLoader . THREAD_LOAD_FAILED : push_error ( "Failed to load scene: " + path ) break progress_bar . value = progress [ 0 ] * 100 await get_tree ( ) . process_frame hide ( ) Dynamic Scene Instances Add Scene as Child
Spawn enemy at runtime
const ENEMY_SCENE := preload ( "res://enemies/goblin.tscn" ) func spawn_enemy ( position : Vector2 ) -> void : var enemy := ENEMY_SCENE . instantiate ( ) enemy . global_position = position add_child ( enemy ) Instance Management
Keep track of spawned enemies
var active_enemies : Array [ Node ] = [ ] func spawn_enemy ( pos : Vector2 ) -> void : var enemy := ENEMY_SCENE . instantiate ( ) enemy . global_position = pos add_child ( enemy ) active_enemies . append ( enemy )
Clean up when enemy dies
enemy . tree_exited . connect ( func ( ) : active_enemies . erase ( enemy ) ) func clear_all_enemies ( ) -> void : for enemy in active_enemies : enemy . queue_free ( ) active_enemies . clear ( ) Sub-Scenes
Load UI as sub-scene
@ onready var ui := preload ( "res://ui/game_ui.tscn" ) . instantiate ( ) func _ready ( ) -> void : add_child ( ui ) Scene Persistence
Keep scene loaded when changing scenes
var persistent_scene : Node func make_persistent ( scene : Node ) -> void : persistent_scene = scene scene . get_parent ( ) . remove_child ( scene ) get_tree ( ) . root . add_child ( scene ) func restore_persistent ( ) -> void : if persistent_scene : get_tree ( ) . root . remove_child ( persistent_scene ) add_child ( persistent_scene ) Reload Current Scene
Restart level
get_tree ( ) . reload_current_scene ( ) Scene Caching
Cache frequently used scenes
var scene_cache : Dictionary = { } func get_cached_scene ( path : String ) -> PackedScene : if not scene_cache . has ( path ) : scene_cache [ path ] = load ( path ) return scene_cache [ path ]
Usage:
var enemy := get_cached_scene ( "res://enemies/goblin.tscn" ) . instantiate ( ) Best Practices 1. Use SceneTransitioner AutoLoad
Centralized scene management
All transitions go through one system
Consistent fade effects
- Preload Common Scenes
✅ Good - preload at compile time
const BULLET := preload ( "res://projectiles/bullet.tscn" )
❌ Bad - load at runtime
var bullet := load ( "res://projectiles/bullet.tscn" ) 3. Clean Up Before Transition func change_level ( ) -> void :
Clear timers, tweens, etc.
for timer in get_tree ( ) . get_nodes_in_group ( "timers" ) : timer . stop ( ) SceneTransitioner . change_scene ( "res://levels/next.tscn" ) 4. Error Handling func load_scene_safe ( path : String ) -> bool : if not ResourceLoader . exists ( path ) : push_error ( "Scene not found: " + path ) return false get_tree ( ) . change_scene_to_file ( path ) return true Reference Godot Docs: SceneTree Godot Docs: Background Loading Related Master Skill: godot-master