Can you write this from memory?
Preload the enemy scene from "res://scenes/enemy.tscn" into a constant called EnemyScene.
Scene instancing is Godot's core superpower: build a thing once (a bullet, enemy, pickup) and spawn copies on demand.
This page focuses on the patterns and pitfalls you'll actually hit: wrong positions, add_child timing errors, and safe spawning in callbacks.
| Symptom | Fix |
|---|---|
Godot 3 code uses instance() | Renamed to PackedScene.instantiate() in Godot 4 |
| Spawned node at wrong position | Set global_position after add_child() |
| "Parent node is busy setting up children, add_child() failed" | Use add_child.call_deferred(child) |
| Spawning in physics/signal callback complains | Defer the add_child or the spawn call |
| Game stutters when loading scenes | Use ResourceLoader.load_threaded_request() |
This is essentially the factory pattern—a method creates and returns a configured object without the caller knowing the details.
- Get a
PackedScene(usuallypreload) instantiate()itadd_child()to the right parent
const BulletScene: PackedScene = preload("res://scenes/bullet.tscn")
@onready var projectiles: Node = %Projectiles
func shoot() -> void:
var bullet := BulletScene.instantiate() as Area2D
projectiles.add_child(bullet)
bullet.global_position = global_position
bullet.direction = aim_direction
Add to a dedicated container node (%Projectiles, %Enemies) rather than get_tree().current_scene—it keeps your scene tree organized and makes debugging easier. When complex scenes have many container nodes, this starts to resemble the facade pattern—one root node hides internal complexity (see the Godot facade pattern blog post for a deeper look).
If you're following older tutorials or migrating a project:
| Godot 3 | Godot 4 |
|---|---|
PackedScene.instance() | PackedScene.instantiate() |
The old method name gives you: "Invalid call. Nonexistent function 'instance' in base 'PackedScene'."
| Method | When to use |
|---|---|
preload() | Scenes you always need—resolved at parse time, instant at runtime |
load() | Dynamic paths or optional content—blocks main thread |
@export var | Designer-friendly—drag scenes in Inspector |
# preload: embedded at compile time, path must be literal
const Bullet: PackedScene = preload("res://bullet.tscn")
# load: happens at runtime, path can be variable
var level_scene: PackedScene = load("res://levels/" + level_name + ".tscn")
# @export: assign in Inspector (team-friendly)
@export var enemy_scene: PackedScene
Threaded loading (avoid hitches)
If load() causes frame drops, use background loading:
var level_path := ""
func load_level_async(path: String) -> void:
level_path = path
ResourceLoader.load_threaded_request(path)
func _process(_delta: float) -> void:
if level_path.is_empty():
return
var status := ResourceLoader.load_threaded_get_status(level_path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
var scene: PackedScene = ResourceLoader.load_threaded_get(level_path)
get_tree().root.add_child(scene.instantiate())
level_path = "" # Stop checking
Use this for large optional scenes (level transitions, boss arenas) where blocking the main thread is noticeable.
- Set local
positionbeforeadd_child()when thinking "relative to parent" - Set world
global_positionafteradd_child()when thinking "place it in the world"
# Local position (relative to parent)
bullet.position = Vector2(16, 0)
add_child(bullet)
# World position (absolute coordinates)
add_child(bullet)
bullet.global_position = Vector2(100, 100)
Common bug: Setting global_position before add_child() can give wrong results—the node doesn't have a parent transform yet, so "global" isn't computed correctly.
If you see:
Parent node is busy setting up children, add_child() failed. Consider using add_child.call_deferred(child) instead.
You're trying to add children while the parent is still initializing (often in _ready() or during scene setup). Defer it:
var bullet := BulletScene.instantiate() as Area2D
add_child.call_deferred(bullet)
# Position and setup in bullet's _ready(), or defer those too
This also applies when spawning during physics callbacks or signal handlers—the scene tree might be mid-update.
Every instantiate() should eventually have a queue_free(). Use it for:
- Bullets hitting targets or leaving screen
- Temporary effects (explosions, particles)
- Enemies dying
func _on_lifetime_timeout() -> void:
queue_free()
func _on_hit(body: Node) -> void:
# Do damage, play effects, etc.
queue_free()
queue_free() deletes at end of frame—safe to call during callbacks. Without cleanup, your scene tree grows forever.
A generic helper keeps spawn logic consistent:
func spawn(scene: PackedScene, parent: Node, pos: Vector2) -> Node:
var instance := scene.instantiate()
parent.add_child(instance)
if instance is Node2D:
instance.global_position = pos
return instance
Usage: spawn(BulletScene, %Projectiles, muzzle.global_position)
Reference important nodes without fragile paths:
# Fragile: breaks if you move the node
@onready var sprite: Sprite2D = $Player/Body/Sprite2D
# Robust: works regardless of position in tree
@onready var sprite: Sprite2D = %PlayerSprite
Set up: Right-click node → "Access as Unique Name". Works scene-wide, and you can chain them: get_node("%Sword/%Hilt").
When to Use GDScript Scene Instancing
- Spawning bullets, enemies, or collectibles at runtime from preloaded scenes
- Building procedural levels by instantiating scene fragments and positioning them via code
- Creating object pools by pre-instantiating PackedScenes and toggling visibility or re-parenting
Check Your Understanding: GDScript Scene Instancing
Check Your Understanding: What is the difference between preload and load in GDScript, and when should you use each for scene instancing?
preload() resolves the resource path at compile time and embeds it in the script, so it is instant at runtime, but the scene must exist when the script is parsed. load() resolves at runtime and can accept variable paths, but blocks the main thread while loading. Use preload for scenes you always need (bullets, known enemies). Use load or ResourceLoader.load_threaded_request for optional or large scenes to avoid stalling.
What You'll Practice: GDScript Scene Instancing
Common GDScript Scene Instancing Pitfalls
- Setting global_position before add_child()—the node has no parent transform yet, so "global" isn't computed correctly
- Using load() in _physics_process or tight loops—it blocks the main thread every call; preload as a const instead
- Forgetting queue_free() on temporary nodes (bullets, explosions)—causes ever-growing scene trees
- Hard-coding get_tree().current_scene.add_child() everywhere—prefer a dedicated container node (%Projectiles, %Enemies)
- Ignoring "Parent node is busy" errors—use add_child.call_deferred() when spawning during initialization or callbacks
GDScript Scene Instancing FAQ
How do I spawn a scene instance in Godot 4?
Preload the scene: const BulletScene = preload("res://scenes/bullet.tscn"). Call BulletScene.instantiate() to create a node. Add it to a parent with add_child(). Then set global_position.
Why does my instanced scene appear at the wrong position?
Set global_position after add_child(), not before. Before add_child(), the node has no parent transform, so global coordinates aren't computed correctly. For relative positioning, use local position before adding.
What's the difference between instance() and instantiate()?
instance() was renamed to instantiate() in Godot 4. If you get "Nonexistent function 'instance'" error, change it to instantiate(). Many older tutorials still show the Godot 3 syntax.
How do I fix "Parent node is busy setting up children, add_child() failed"?
Use add_child.call_deferred(child) instead of add_child(). This defers adding until the parent finishes initializing. Common when spawning in _ready() or during signal callbacks.
preload vs load—when do I use each?
Use preload for scenes you always need (bullets, common enemies)—it's instant at runtime. Use load when the path is dynamic or the scene is optional. For large scenes that cause hitches, use ResourceLoader.load_threaded_request().
Why is my spawned node at (0,0)?
You forgot to set its position, or you set global_position before add_child() (which doesn't work correctly). Always set global_position after add_child() for predictable world coordinates.
How do I spawn into a container node instead of current_scene?
Create a dedicated container (%Projectiles, %Enemies) and add_child to that: %Projectiles.add_child(bullet). This keeps your scene tree organized. Use Unique Names (%) for easy reference.
How do I avoid frame drops when loading large scenes?
Use ResourceLoader.load_threaded_request(path) to load in the background. Poll with load_threaded_get_status() until THREAD_LOAD_LOADED, then call load_threaded_get() to retrieve the PackedScene.
Why does $NodeName break when I move nodes?
$ is shorthand for get_node() with a relative path. Paths break when you restructure. Use Unique Names (%NodeName) for important references—they work regardless of node position in the tree.
Do I need to call queue_free() on instanced nodes?
Yes. Every instantiate() should eventually queue_free(), otherwise your scene tree grows forever. Use it when bullets hit, effects finish, or enemies die. queue_free() is safe to call during callbacks—it deletes at end of frame.
GDScript Scene Instancing Syntax Quick Reference
const BulletScene: PackedScene = preload("res://scenes/bullet.tscn")
@onready var projectiles: Node = %Projectiles
func shoot() -> void:
var bullet := BulletScene.instantiate() as Area2D
projectiles.add_child(bullet)
bullet.global_position = muzzle.global_position
bullet.direction = aim_directionfunc spawn(scene: PackedScene, parent: Node, pos: Vector2) -> Node:
var instance := scene.instantiate()
parent.add_child(instance)
if instance is Node2D:
instance.global_position = pos
return instance
# Usage
spawn(EnemyScene, %Enemies, spawn_point.global_position)func _on_timer_timeout() -> void:
var enemy := EnemyScene.instantiate() as CharacterBody2D
add_child.call_deferred(enemy)
# Set position in enemy's _ready(), or:
# enemy.set_deferred("global_position", spawn_pos)var level_path := "res://levels/boss.tscn"
func start_loading() -> void:
ResourceLoader.load_threaded_request(level_path)
func _process(_delta: float) -> void:
if level_path.is_empty(): return
if ResourceLoader.load_threaded_get_status(level_path) == ResourceLoader.THREAD_LOAD_LOADED:
var scene: PackedScene = ResourceLoader.load_threaded_get(level_path)
get_tree().root.add_child(scene.instantiate())
level_path = "" # Stop checkingGDScript Scene Instancing Sample Exercises
Fill in the blank to create a new scene instance.
instantiateDeclare an exported variable called enemy_scene of type PackedScene.
@export var enemy_scene: PackedSceneCreate a new instance from the enemy_scene exported variable, storing it in a variable called e.
var e = enemy_scene.instantiate()+ 10 more exercises