
Godot Scene Management
- 249 installs
- 454 repo stars
- Updated July 28, 2026
- thedivergentai/gd-agentic-skills
Use godot-scene-management for development tasks
About
godot-scene-management: A skill for development. This provides functionality for development workflows.
- godot-scene-management
Godot Scene Management by the numbers
- 249 all-time installs (skills.sh)
- +29 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,539 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/thedivergentai/gd-agentic-skills --skill godot-scene-managementAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 249 |
|---|---|
| repo stars | ★ 454 |
| Last updated | July 28, 2026 |
| Repository | thedivergentai/gd-agentic-skills ↗ |
What it does
Use godot-scene-management for development tasks
Files
Scene Management
Async loading, transitions, instance pooling, and caching define smooth scene workflows.
Available Scripts
background_resource_loader.gd
Expert asynchronous scene loading with progress tracking and thread-safe transition.
scene_transition_manager.gd
Clean implementation of scene fades and transitions using Tweens and Shaders.
additive_ui_layering.gd
Managing UI overlays and menus without destroying the current world scene.
node_unparent_reparent.gd
Safe, transform-preserving reparenting of nodes between different scene trees.
persistent_data_preservation.gd
Pattern for using Autoloads to maintain player state and game data across scene changes.
scene_instancing_pooling.gd
High-performance object pooling to eliminate the cost of frequent instantiation and freeing.
subviewport_scene_layering.gd
Running parallel worlds or specialized rendering layers using SubViewport nodes.
node_path_safe_retrieval.gd
Robust node reference architecture using Unique Names and error-guarded @onready.
dynamic_script_attachment.gd
Runtime script manipulation for modding systems or highly dynamic entity behavior.
async_scene_manager.gd
Expert async scene loader with progress tracking, error handling, and transition callbacks. 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 load large scenes synchronously —
load("res://large_scene.tscn")on the Main Thread causes "hiccups" or full freezes during level transitions. UseResourceLoader.load_threaded_request()for async loading with a progress bar. - NEVER use `get_tree().change_scene_to_file()` for transient state — This method purges the current scene and all its local variables. Use an Autoload (Singleton) or a persistent 'Game' node to store state across levels.
- NEVER instance 100+ identical nodes per frame — Use Object Pooling to reuse bullets, debris, or enemies. Constant
instantiate()andqueue_free()calls spike CPU and trigger the Garbage Collector too often. - NEVER hardcode `get_node("../../Path/To/Node")` — These paths break as soon as you move a node in the editor. Use Scene Unique Names (
%NodeName) or@export var target_node: Nodefor robust references. - NEVER reparent nodes mid-physics-step without care — Reparenting can cause one-frame transform "teleports". Always store the
global_transformand re-apply it after theadd_child()call. - NEVER rely on the SceneTree for 10,000+ objects — If you don't need SceneTree features (signals, per-node scripts), use
PhysicsServerandRenderingServerdirectly for raw performance. - NEVER forget to handle `NOTIFICATION_WM_CLOSE_REQUEST` — On desktop, if you don't handle the close request in a persistent node, the game may close during a critical save operation.
- NEVER use deep recursion for node cleanup —
queue_free()is natively recursive in Godot 4. Freeing the root node automatically cleans up all children [6, 7]. Manual loops are redundant and inefficient. - NEVER mix `SubViewport` and main world inputs without a plan — By default, input events bubble up. Use
set_input_as_handled()to prevent UI clicks in a subviewport from triggering gameplay in the main world. - NEVER use `change_scene` to "Reset" a level — It reloads everything from disk. For a quick respawn, just reset the variables and move the player to the start position.
---
# 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_finishedAsync (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_frameLoading 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()Expert Scene Patterns
1. Node-Pooling-Pre-instantiation
To avoid frame drops during combat, pre-fill your pools during a loading screen. This absorbs the instantiation cost upfront [1].
# Inside Pool Manager
func pre_fill_pool(count: int):
for i in range(count):
var instance = scene.instantiate()
instance.process_mode = Node.PROCESS_MODE_DISABLED
instance.hide()
add_child(instance)
pool.append(instance)2. Scene-Transition-Staging
Load sub-scenes or upcoming levels in the background during active gameplay using ResourceLoader.load_threaded_request() to prevent transition hitches [3, 4].
func start_background_load(path: String):
ResourceLoader.load_threaded_request(path)
func _process(_d):
var status = ResourceLoader.load_threaded_get_status(path, progress)
if status == ResourceLoader.THREAD_LOAD_LOADED:
var scene = ResourceLoader.load_threaded_get(path)
# Transition when ready...3. Scene Patcher (Runtime PCK Overrides)
Hot-swap scenes or load modular DLC using ProjectSettings.load_resource_pack(). This mounts a .pck file into the virtual filesystem, overriding existing res:// paths [4, 6].
func patch_scene(pck_path: String):
if ProjectSettings.load_resource_pack(pck_path):
# The next load() call will fetch the patched version from the PCK
get_tree().change_scene_to_file("res://patched_level.tscn")4. Memory Leak Detector
Track orphan nodes during scene transitions using the Performance singleton. If OBJECT_ORPHAN_NODE_COUNT is > 0, nodes were leaked [2, 10].
func check_leaks():
var orphans = Performance.get_monitor(Performance.OBJECT_ORPHAN_NODE_COUNT)
if orphans > 0:
print_warning("Leaked %d nodes!" % orphans)
Node.print_orphan_nodes()5. Natively Recursive Cleanup
In Godot 4, queue_free() handles the entire node tree. You never need a manual for child in get_children(): child.queue_free() loop. This is handled at the engine level for maximum efficiency [7].
Best Practices
1. Use SceneTransitioner AutoLoad
# Centralized scene management
# All transitions go through one system
# Consistent fade effects2. 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 trueReference
Related
- Master Skill: godot-master
# additive_ui_layering.gd
# Managing multiple UI layouts without replacing the whole scene
extends Node
# EXPERT NOTE: Don't change the entire scene for just a menu.
# Load UI scenes as children of a persistent 'UI' node.
func open_menu(path: String):
var scene = load(path).instantiate()
add_child(scene)
# Pause game logic if it's a pause menu
get_tree().paused = true
func close_menu(menu: Node):
menu.queue_free()
get_tree().paused = false
# skills/scene-management/code/async_scene_manager.gd
extends Node
## Async Scene Manager Expert Pattern
## Implements Threaded Loading with Progress Tracking and Data Payloads.
signal loading_progress(progress: float)
signal loading_complete(scene_resource: PackedScene)
var _target_scene_path: String = ""
var _scene_payload: Dictionary = {}
# 1. Threaded Loading Request
# Expert logic: NEVER use 'change_scene_to_file' for large levels.
func load_scene(path: String, payload: Dictionary = {}) -> void:
_target_scene_path = path
_scene_payload = payload
# Start the threaded load
var err = ResourceLoader.load_threaded_request(path)
if err != OK:
push_error("Failed to start loading: ", path)
return
set_process(true)
func _process(_delta: float) -> void:
# 2. Async Progress Tracking
# Professional pattern: Poll status to update UI loading bars.
var progress = []
var status = ResourceLoader.load_threaded_get_status(_target_scene_path, progress)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
loading_progress.emit(progress[0])
ResourceLoader.THREAD_LOAD_LOADED:
_finalize_load()
ResourceLoader.THREAD_LOAD_FAILED:
set_process(false)
push_error("Threaded load failed for: ", _target_scene_path)
func _finalize_load() -> void:
set_process(false)
var new_scene_res = ResourceLoader.load_threaded_get(_target_scene_path)
var new_scene = new_scene_res.instantiate()
# 3. Scene Payload System (Data Injection)
# Standardize how to pass complex data before _ready().
if new_scene.has_method("initialize"):
new_scene.initialize(_scene_payload)
get_tree().root.add_child(new_scene)
get_tree().current_scene.queue_free()
get_tree().current_scene = new_scene
loading_complete.emit(new_scene_res)
## EXPERT NOTE:
## Use 'Memory Profiling': Inside the loading screen, call
## 'Performance.get_monitor(Performance.MEMORY_STATIC)' and
## 'print_stray_nodes()' to detect memory leaks during scene swaps.
## For 'scene-management', implement a 'Background Pre-loading'
## system that starts loading the next level while the player
## is still finishing the current one, hidden behind a 'Victory'
## or 'Dialogue' UI to mask the transition entirely.
## NEVER hardcode scene paths; use a 'Registry' resource that
## maps 'LEVEL_1' to the actual '.tscn' path.
# background_resource_loader.gd
# Async resource loading with progress tracking
extends Node
# EXPERT NOTE: For large levels, use ResourceLoader.load_threaded_request
# to prevent the game from freezing while loading a new scene.
var _pending_scene: String = ""
func load_scene(path: String):
_pending_scene = path
var error = ResourceLoader.load_threaded_request(path)
if error != OK:
push_error("Failed to start loading: " + path)
func _process(_delta: float) -> void:
if _pending_scene == "": return
var progress = []
var status = ResourceLoader.load_threaded_get_status(_pending_scene, progress)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
# Update loading bar: progress[0]
pass
ResourceLoader.THREAD_LOAD_LOADED:
var scene = ResourceLoader.load_threaded_get(_pending_scene)
get_tree().change_scene_to_packed(scene)
_pending_scene = ""
ResourceLoader.THREAD_LOAD_FAILED:
_pending_scene = ""
# dynamic_script_attachment.gd
# Attaching scripts to nodes at runtime [Modding System]
extends Node
func apply_script_to_node(node: Node, script_path: String):
var script = load(script_path)
if script is Script:
node.set_script(script)
# Re-run _ready if needed, or call init
node.notification(NOTIFICATION_READY)
# node_path_safe_retrieval.gd
# Robust node path handling to prevent 'Null Instance' errors
extends Node
# EXPERT NOTE: Avoid hardcoded Nodepaths like get_node("../../Player")
# as they break if the hierarchy changes. Use @onready and Unique Names.
@onready var player = %Player # Using Scene Unique Name (%)
func _ready() -> void:
if not player:
push_error("Critical error: Player not found in scene!")
# node_unparent_reparent.gd
# Safely moving nodes between scene hierarchies
extends Node
# PROBLEM: Reparenting mid-frame can cause issues with
# transform synchronization.
func reparent_node(node: Node, new_parent: Node):
# Preserve global transform during reparenting
var global_xform = node.global_transform if node is Node2D or node is Node3D else null
node.get_parent().remove_child(node)
new_parent.add_child(node)
if global_xform:
node.global_transform = global_xform
# persistent_data_preservation.gd
# Using an Autoload for scene-crossing variables [State Management]
extends Node
# EXPERT NOTE: Values in a Scene are lost when get_tree().change_scene is called.
# Use a Singleton (Autoload) to keep state.
var player_hp: int = 100
var current_level_seed: int = 1234
var unlocked_items: Array = []
func save_state():
# Logic to serialize variables to a config file
pass
# scene_instancing_pooling.gd
# Object pooling for high-frequency scene instancing
extends Node
@export var scene_to_pool: PackedScene
@export var pool_size := 50
var _pool: Array = []
func _ready() -> void:
for i in pool_size:
var instance = scene_to_pool.instantiate()
instance.visible = false
instance.process_mode = Node.PROCESS_MODE_DISABLED
add_child(instance)
_pool.append(instance)
func spawn(pos: Vector2):
for i in _pool:
if i.process_mode == Node.PROCESS_MODE_DISABLED:
i.global_position = pos
i.visible = true
i.process_mode = Node.PROCESS_MODE_INHERIT
return i
return null # Pool exhausted
# skills/scene-management/scripts/scene_pool.gd
extends Node
## Scene Pool Expert Pattern
## Object pooling for frequently spawned/destroyed scenes to reduce instantiation overhead.
class_name ScenePool
var _pool: Dictionary = {} # PackedScene path → Array of inactive instances
func prewarm(scene_path: String, count: int) -> void:
var scene := load(scene_path) as PackedScene
if not scene:
push_error("Failed to load scene: %s" % scene_path)
return
if scene_path not in _pool:
_pool[scene_path] = []
for i in range(count):
var instance := scene.instantiate()
instance.set_meta("_pooled", true)
_pool[scene_path].append(instance)
func acquire(scene_path: String, parent: Node) -> Node:
if scene_path not in _pool or _pool[scene_path].is_empty():
# Pool empty, create new instance
var scene := load(scene_path) as PackedScene
var instance := scene.instantiate()
instance.set_meta("_pooled", true)
parent.add_child(instance)
return instance
# Reuse pooled instance
var instance := _pool[scene_path].pop_back()
parent.add_child(instance)
return instance
func release(instance: Node) -> void:
if not instance.has_meta("_pooled"):
instance.queue_free()
return
var scene_path := instance.scene_file_path
if scene_path.is_empty():
instance.queue_free()
return
instance.get_parent().remove_child(instance)
if scene_path not in _pool:
_pool[scene_path] = []
_pool[scene_path].append(instance)
func clear_pool(scene_path: String = "") -> void:
if scene_path.is_empty():
for path in _pool:
for instance in _pool[path]:
instance.queue_free()
_pool.clear()
elif scene_path in _pool:
for instance in _pool[scene_path]:
instance.queue_free()
_pool.erase(scene_path)
## EXPERT USAGE:
## var pool := ScenePool.new()
## pool.prewarm("res://projectiles/bullet.tscn", 50)
##
## # Spawn
## var bullet := pool.acquire("res://projectiles/bullet.tscn", self)
##
## # Return to pool
## pool.release(bullet)
# skills/scene-management/scripts/scene_state_manager.gd
extends Node
## Scene State Manager Expert Pattern
## Preserves and restores scene state across transitions (player position, collected items, NPC states).
class_name SceneStateManager
var _scene_states: Dictionary = {} # scene_path → state data
func save_current_scene() -> void:
var current_scene := get_tree().current_scene
if not current_scene:
return
var scene_path := current_scene.scene_file_path
var state := {}
# Save all nodes in "persist" group
for node in get_tree().get_nodes_in_group("persist"):
var node_data := {}
if node is Node2D or node is Node3D:
node_data["position"] = node.global_position
if node.has_method("save_state"):
node_data["custom"] = node.save_state()
state[node.get_path()] = node_data
_scene_states[scene_path] = state
print("Saved state for: %s (%d nodes)" % [scene_path, state.size()])
func restore_scene(scene_path: String) -> void:
if scene_path not in _scene_states:
return
await get_tree().process_frame # Wait for scene to load
var state: Dictionary = _scene_states[scene_path]
for node_path in state:
var node := get_tree().current_scene.get_node_or_null(node_path)
if not node:
continue
var node_data: Dictionary = state[node_path]
if "position" in node_data:
node.global_position = node_data["position"]
if "custom" in node_data and node.has_method("load_state"):
node.load_state(node_data["custom"])
print("Restored state for: %s (%d nodes)" % [scene_path, state.size()])
func clear_scene_state(scene_path: String) -> void:
_scene_states.erase(scene_path)
func clear_all_states() -> void:
_scene_states.clear()
## EXPERT USAGE:
## Add nodes to "persist" group + implement save_state/load_state:
##
## func save_state() -> Dictionary:
## return {"health": health, "inventory": inventory}
##
## func load_state(data: Dictionary) -> void:
## health = data.get("health", 100)
## inventory = data.get("inventory", [])
# scene_transition_manager.gd
# Smooth transitions between scenes using Tweens/Shaders
extends CanvasLayer
@onready var color_rect := $ColorRect
func transition_to(scene_path: String):
# Fade to black
var tween = create_tween()
await tween.tween_property(color_rect, "color:a", 1.0, 0.5).finished
get_tree().change_scene_to_file(scene_path)
# Fade back in
tween = create_tween()
tween.tween_property(color_rect, "color:a", 0.0, 0.5)
# subviewport_scene_layering.gd
# Running two different scenes in parallel using Viewports
extends SubViewportContainer
# EXPERT NOTE: Use SubViewports for Mini-maps, Split-screen,
# or 3D UI elements rendered in a 2D world.
func _ready() -> void:
# Ensure the viewport is correctly capturing its own world
$SubViewport.own_world_3d = true