Scenes as Prefabs: Your Reusable Building Blocks
In Godot, a Scene is a saved tree of nodes. You can think of it as a “prefab”: a packaged object you can drop into other scenes, duplicate, and even extend into variants. This is the core workflow for building games in a modular way.
A Node is a single component in that tree. Nodes compose behavior by stacking responsibilities: one node might handle movement, another collisions, another visuals, another sound. A scene becomes reusable when it has a clear root node, a predictable set of children, and a script that exposes a small, stable API (properties and methods) for other scenes to use.
Why root node choice matters
- Node2D: best for “things that have a position/rotation/scale” but don’t need built-in physics movement. Great for containers (like a Level) or simple visual-only objects.
- CharacterBody2D: best for player/enemy characters that move and collide using kinematic-style movement. It provides
velocityandmove_and_slide(). - Area2D: best for triggers and overlaps (pickups, hitboxes, detection zones). It detects bodies/areas entering/exiting via signals.
Reusable Scene 1: Player
Goal
Create a Player.tscn scene that can be instanced into any level, moves with input, and can be detected by collectibles and enemies via groups.
Root node choice
Use CharacterBody2D as the root because the player needs collision-aware movement and a stable movement API.
Suggested node tree
Player (CharacterBody2D) [Player.gd] (add to group: "player"); (optional group: "damageable")
├─ Sprite2D
├─ CollisionShape2D
└─ (optional) AnimationPlayerStep-by-step
- Create a new scene, add CharacterBody2D as root, name it
Player, save asPlayer.tscn. - Add Sprite2D for visuals.
- Add CollisionShape2D and assign a shape (e.g., CapsuleShape2D or RectangleShape2D).
- In the root node, open the Node dock → Groups and add
player. This makes it easy for other objects to recognize the player without hard references. - Attach a script
Player.gdto the root.
Player.gd (simple movement + small API)
extends CharacterBody2D
@export var speed: float = 220.0
func _physics_process(delta: float) -> void:
var dir := Vector2(
Input.get_action_strength("ui_right") - Input.get_action_strength("ui_left"),
Input.get_action_strength("ui_down") - Input.get_action_strength("ui_up")
)
if dir.length() > 1.0:
dir = dir.normalized()
velocity = dir * speed
move_and_slide()
# Small, reusable API other scenes can call
func knockback(from_position: Vector2, force: float = 300.0) -> void:
var away := (global_position - from_position).normalized()
velocity = away * forceKeep the player script focused: movement and a couple of methods (like knockback) that other scenes can call without needing to know internal details.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Reusable Scene 2: Enemy (Base)
Goal
Create an EnemyBase.tscn that provides shared enemy functionality (movement, health hooks, group membership). Then create variants via scene inheritance.
Root node choice
Use CharacterBody2D as the root because enemies typically move and collide like the player.
Suggested node tree
EnemyBase (CharacterBody2D) [EnemyBase.gd] (add to group: "enemies")
├─ Sprite2D
├─ CollisionShape2D
└─ Detection (Area2D)
└─ CollisionShape2DThe Detection Area2D is a reusable pattern: it lets the enemy “notice” the player without mixing detection logic into the main collision shape.
Step-by-step
- Create a new scene with CharacterBody2D root named
EnemyBase, save asEnemyBase.tscn. - Add Sprite2D and CollisionShape2D for the enemy body.
- Add a child Area2D named
Detectionwith its own CollisionShape2D (e.g., a circle) to represent detection range. - Add the root to group
enemies. - Connect
Detection.body_enteredto the root script (or connect via code).
EnemyBase.gd (shared behavior)
extends CharacterBody2D
@export var move_speed: float = 120.0
@export var contact_damage: int = 1
var target: Node2D = null
func _ready() -> void:
add_to_group("enemies")
func _physics_process(delta: float) -> void:
if target and is_instance_valid(target):
var dir := (target.global_position - global_position).normalized()
velocity = dir * move_speed
move_and_slide()
else:
velocity = Vector2.ZERO
func _on_detection_body_entered(body: Node) -> void:
if body.is_in_group("player"):
target = body as Node2D
func set_target(new_target: Node2D) -> void:
target = new_targetThis base script is intentionally generic. Variants can override movement style, stats, or reactions while keeping the detection and targeting pattern consistent.
Scene Inheritance: Two Enemy Types Without Duplicating Scripts
Scene inheritance lets you create a new scene that “extends” another scene. The child scene keeps the parent’s node structure and scripts, but can override exported values, replace nodes, add new nodes, or attach an additional script that extends the base script.
Create Enemy Type A: Chaser
- Right-click
EnemyBase.tscnin the FileSystem → choose New Inherited Scene. - Save as
EnemyChaser.tscn. - In the Inspector, tweak exported values on the root: set
move_speedhigher, maybe adjust detection shape size. - Optionally change the sprite/texture to visually distinguish it.
This variant can use the same EnemyBase.gd without any new script.
Create Enemy Type B: Patroller (extends the base script)
- Create another inherited scene from
EnemyBase.tscn, save asEnemyPatroller.tscn. - Add a new script to the root that extends
EnemyBase.gd. This keeps shared code in one place.
extends "res://EnemyBase.gd"
@export var patrol_distance: float = 120.0
@export var patrol_speed: float = 90.0
var start_x: float
var direction: float = 1.0
func _ready() -> void:
super._ready()
start_x = global_position.x
func _physics_process(delta: float) -> void:
# If we have a target, use base chasing behavior
if target and is_instance_valid(target):
super._physics_process(delta)
return
# Otherwise patrol left/right
if abs(global_position.x - start_x) >= patrol_distance:
direction *= -1.0
velocity = Vector2(direction * patrol_speed, 0)
move_and_slide()Now you have two enemy types that share detection and targeting, but differ in movement. This keeps scripts maintainable: common logic stays in EnemyBase.gd, and only differences live in the derived script.
Reusable Scene 3: Collectible
Goal
Create a Collectible.tscn that detects the player, awards value, and removes itself. This should be easy to place many times in a level.
Root node choice
Use Area2D as the root because a collectible is primarily an overlap trigger, not a physics-moving character.
Suggested node tree
Collectible (Area2D) [Collectible.gd] (add to group: "collectibles")
├─ Sprite2D
└─ CollisionShape2DStep-by-step
- Create a new scene with Area2D root named
Collectible, save asCollectible.tscn. - Add Sprite2D and CollisionShape2D (e.g., CircleShape2D).
- In the root, enable monitoring (default) and ensure the collision layers/masks allow overlap with the player.
- Add the root to group
collectibles. - Connect the
body_enteredsignal to the script.
Collectible.gd
extends Area2D
@export var value: int = 1
signal collected(value: int)
func _ready() -> void:
add_to_group("collectibles")
func _on_body_entered(body: Node) -> void:
if body.is_in_group("player"):
collected.emit(value)
queue_free()Using a signal keeps the collectible reusable: the Level (or UI) can listen for collected and update score without the collectible needing to know about the score system.
Reusable Scene 4: Level (Composition and Instancing)
Goal
Create a Level.tscn that instances Player, Enemies, and Collectibles, and uses groups to find and manage them. The Level acts as a composition root: it wires objects together.
Root node choice
Use Node2D as the root because a level is primarily an organizer/container for many 2D nodes. It may not need physics behavior itself.
Suggested node tree
Level (Node2D) [Level.gd]
├─ TileMap (optional)
├─ PlayerSpawn (Marker2D)
├─ Enemies (Node2D)
├─ Collectibles (Node2D)
└─ UI (CanvasLayer) (optional)Using container nodes like Enemies and Collectibles keeps the scene tidy and makes it easy to spawn and iterate over related objects.
Instancing scenes in the editor
- Open
Level.tscn. - Drag
Player.tscnfrom the FileSystem into the scene tree (or use Instance Child Scene). - Drag
EnemyChaser.tscn,EnemyPatroller.tscn, andCollectible.tscninto their respective container nodes. - Position them in the 2D view.
This is the fastest workflow for hand-placed content.
Instancing scenes via code (spawning)
For procedural placement or respawning, load a PackedScene and instantiate it.
extends Node2D
@export var player_scene: PackedScene
@export var chaser_scene: PackedScene
@export var patroller_scene: PackedScene
@export var collectible_scene: PackedScene
@onready var player_spawn: Marker2D = $PlayerSpawn
@onready var enemies_root: Node2D = $Enemies
@onready var collectibles_root: Node2D = $Collectibles
var player: Node2D
func _ready() -> void:
spawn_player()
spawn_demo_enemies()
spawn_demo_collectibles()
func spawn_player() -> void:
player = player_scene.instantiate() as Node2D
add_child(player)
player.global_position = player_spawn.global_position
func spawn_demo_enemies() -> void:
var e1 := chaser_scene.instantiate() as Node2D
enemies_root.add_child(e1)
e1.global_position = player_spawn.global_position + Vector2(200, 0)
var e2 := patroller_scene.instantiate() as Node2D
enemies_root.add_child(e2)
e2.global_position = player_spawn.global_position + Vector2(-200, 0)
func spawn_demo_collectibles() -> void:
for i in 5:
var c := collectible_scene.instantiate() as Area2D
collectibles_root.add_child(c)
c.global_position = player_spawn.global_position + Vector2(i * 48, -80)
c.collected.connect(_on_collectible_collected)
func _on_collectible_collected(value: int) -> void:
# Hook this into your score/ UI logic
print("Collected: ", value)Groups for Categorization and Loose Coupling
Groups let you label nodes (e.g., player, enemies, collectibles) so other systems can find them without hard-coded paths. This is especially useful when you instance scenes dynamically or rearrange the scene tree.
Common patterns
- Detection by group: the collectible checks
body.is_in_group("player")rather than checking a specific node name. - Batch operations: the level can find all enemies to pause them, reset them, or apply difficulty scaling.
func freeze_all_enemies() -> void:
for e in get_tree().get_nodes_in_group("enemies"):
if e is CharacterBody2D:
(e as CharacterBody2D).velocity = Vector2.ZEROWhen to prefer groups over node paths
| Need | Prefer | Why |
|---|---|---|
| Find “the player” regardless of where it is in the tree | Group player | Works with instancing and refactors |
Access a specific child node you own (like $Sprite2D) | Node path | It’s internal structure, stable within the scene |
| Apply an effect to many objects (all enemies) | Group enemies | Batch operations are simple |
Keeping Scripts Maintainable with Reusable APIs
Expose configuration via exported variables
Use @export for values you want to tweak per instance or per inherited scene (speed, detection radius, collectible value). This reduces the need for multiple scripts that only differ by constants.
Keep scene-internal references local
Inside a scene, it’s fine to use @onready var sprite = $Sprite2D. Outside the scene, prefer signals, groups, or small public methods (like set_target()).
Use scene inheritance for variants, not copy/paste
Copying an enemy scene to make a new type often leads to diverging scripts and bugs. Inheritance keeps the shared structure and logic centralized, while allowing each variant to override only what’s different (stats, visuals, or a small derived script).