Module 7: Connecting Worlds: Scene Transitions
13 min read
#What We Have So Far
An animated player character with a state machine, walking around the town of Willowbrook with proper collision and Y-sorting. But the town is an island, and there's no way to leave.
#What We're Building This Module
A second area (Whisperwood Forest), exit zones that detect when the player walks to the edge of a map, and a SceneManager autoload that handles smooth fade-to-black transitions between locations. By the end, Crystal Saga will have two areas you can walk between.
#Why Scenes Map to Locations
In a JRPG, each location is typically one scene:
- Willowbrook (town) →
willowbrook.tscn - Whisperwood (forest) →
whisperwood.tscn - Crystal Cavern (dungeon) →
crystal_cavern.tscn - Battle Screen →
battle.tscn
When the player walks to the edge of town, the game transitions to the forest. When they walk to the forest entrance, it transitions back to town. This is a scene change: the current scene is freed (removed from memory), and the new scene is loaded and instanced.
The challenge: how do we manage these transitions cleanly? Who handles the fade effect? How does the new scene know where to spawn the player?
The answer is our first autoload.
#Autoloads: Your First Singleton
You've been using autoloads since Module 2; you just didn't know it. Input, Engine, Time, Performance, and AudioServer are all globally available singletons that Godot provides. When you write Input.is_action_pressed("ui_right"), you're calling a method on an autoloaded singleton.
Now we're going to create our own.
An autoload (also called a singleton) is a scene or script that:
- Is loaded automatically when the game starts
- Persists across scene changes (it's never freed)
- Is accessible from anywhere by name
This makes autoloads perfect for game-wide systems: scene management, inventory, audio, quest tracking, game state. We'll build several throughout this tutorial.
See: Singletons (Autoload), the official guide to autoloads, including when and why to use them.
Warning: Autoloads are powerful but easy to overuse. Not everything needs to be global. If a system only matters within a single scene (like the layout of a specific room), keep it local. We'll use autoloads for systems that genuinely need to persist across the entire game.
#Building the SceneManager
The SceneManager handles scene transitions: fading out, loading the new scene, fading in, and positioning the player at the correct spawn point.
#Step 1: Create the Script
Create a new folder res://autoloads/ and a new script res://autoloads/scene_manager.gd:
extends Node
## Manages scene transitions with fade effects.
## Registered as an autoload. Accessible as SceneManager from anywhere.
signal transition_started
signal transition_finished
@onready var _color_rect: ColorRect = $TransitionLayer/ColorRect
@onready var _anim_player: AnimationPlayer = $TransitionLayer/AnimationPlayer
var _target_scene_path: String = ""
var _target_spawn_point: String = ""
var _is_transitioning: bool = false
func change_scene(scene_path: String, spawn_point: String = "default") -> void:
if _is_transitioning:
return
_is_transitioning = true
_target_scene_path = scene_path
_target_spawn_point = spawn_point
transition_started.emit()
_anim_player.play("fade_out")
await _anim_player.animation_finished
get_tree().change_scene_to_file(_target_scene_path)
# Wait for the new scene to be added to the tree.
# change_scene_to_file() is deferred, so we need to wait for the swap.
await get_tree().tree_changed
_place_player_at_spawn()
_anim_player.play("fade_in")
await _anim_player.animation_finished
_is_transitioning = false
transition_finished.emit()
func _place_player_at_spawn() -> void:
# Find the spawn point marker in the new scene
var spawn_markers := get_tree().get_nodes_in_group("spawn_points")
for marker in spawn_markers:
if marker.name == _target_spawn_point:
var player := get_tree().get_first_node_in_group("player")
if player:
player.global_position = marker.global_position
return
# If no matching spawn point, use "default"
for marker in spawn_markers:
if marker.name == "default":
var player := get_tree().get_first_node_in_group("player")
if player:
player.global_position = marker.global_position
return
#Step 2: Create the Scene
The SceneManager needs visible nodes (a ColorRect for the black overlay, an AnimationPlayer for the fade). Create a scene for it.
- Create a new scene with
Nodeas root. Rename it toSceneManager. - Add a CanvasLayer child. Rename it to
TransitionLayer. Set its Layer to100in the Inspector (so it draws on top of everything).
In every JRPG, the fade effects and dialogue boxes must render on top of the game world no matter where the camera is or how the scene is structured. In Earthbound, the swirling battle transition overlay covers everything: the map, the enemies, the party. A regular node would be affected by the camera's position and zoom, and could sort incorrectly with other nodes. CanvasLayer creates an entirely separate rendering surface that is immune to camera transforms and always draws at its designated layer number.
- Inside
TransitionLayer, add a ColorRect child. Set its color to black (Color(0, 0, 0, 1)). - Set the ColorRect to cover the full screen: Layout → Anchors Preset → Full Rect (or set all anchors to cover the viewport).
- Set the ColorRect's Modulate alpha to
0(fully transparent by default). - Add an AnimationPlayer as a child of
TransitionLayer.
#Step 3: Create the Fade Animations
Select the AnimationPlayer and create two animations. Here's the step-by-step for the first one:
fade_out (0.3 seconds):
- In the Animation panel at the bottom, click Animation → New. Name it
fade_out. - Set the animation length to
0.3(the number field next to the timeline). - Click Add Track → Property Track. Select the
ColorRectnode. - Choose the
modulateproperty from the list. - Right-click the timeline at time
0.0and choose Insert Key. Set the value's alpha to0.0(transparent). - Right-click at time
0.3and insert another key. Set alpha to1.0(fully opaque/black).
See: Introduction to animations, explaining how to create animations with property tracks in AnimationPlayer.
fade_in (0.3 seconds):
Same process, but reversed:
- At time 0:
modulatealpha =1.0(fully black) - At time 0.3:
modulatealpha =0.0(transparent)
Attach the scene_manager.gd script to the root SceneManager node. Save the scene as res://autoloads/scene_manager.tscn.
Note: We use a CanvasLayer with a high layer number (100) so the fade overlay draws on top of everything: UI, game world, particles, all of it. CanvasLayer nodes exist outside the normal rendering order.
See: CanvasLayer, explaining how CanvasLayer works and why it's essential for UI and overlays.
#Step 4: Register the Autoload
- Go to Project → Project Settings → Autoload.
- Click the folder icon and select
res://autoloads/scene_manager.tscn. - The name will auto-fill as
SceneManager. Keep it. - Click Add.
Now SceneManager is globally accessible. Any script in the game can call SceneManager.change_scene(...).
See: Change scenes manually, covering the built-in
change_scene_to_file()and why a wrapper autoload is often needed.
#Understanding await
The SceneManager uses await, a GDScript keyword that pauses the function until a signal is emitted, then resumes.
_anim_player.play("fade_out")
await _anim_player.animation_finished # Pause here until the animation finishes
# ...this code runs after the animation is done
This makes async sequences (fade out → change scene → fade in) readable as linear code. Without await, you'd need callbacks or a state machine just for the transition.
await can wait for any signal:
await get_tree().create_timer(1.0).timeout # Wait 1 second
await some_node.some_signal # Wait for a custom signal
#Exit Zones
In every JRPG from Dragon Quest to Pokemon, walking to the edge of a town seamlessly transitions you to the next area. The player never clicks a "leave town" button; they just walk south and the game detects that they have crossed an invisible boundary. The alternative, checking the player's position every frame with if position.x > map_width is fragile, hard-coded, and needs rewriting for every map shape. Exit zones are reusable: the same script works on every map edge, every door, and every warp point.
An exit zone is an Area2D that detects when the player enters it and triggers a scene change. We'll set up bidirectional exits between Willowbrook and Whisperwood.
#Creating an Exit Zone
In the Willowbrook scene, add an Area2D:
- Add an Area2D child to
Willowbrook. Rename it toExitToWhisperwood. - Add a CollisionShape2D child to the Area2D.
- Set the shape to a
RectangleShape2Dand position/size it at the map edge where the forest exit should be (e.g., along the south edge of the map).
Create a script for exit zones. Save as res://scenes/exit_zone.gd:
extends Area2D
## A zone that triggers a scene transition when the player enters.
@export_file("*.tscn") var target_scene: String
@export var target_spawn_point: String = "default"
func _ready() -> void:
# In Module 3, we connected signals through the editor UI. That works when both
# sender and receiver are in the same scene and you are placing nodes manually.
# But the exit zone script is designed to be reusable: attach it to any Area2D
# in any scene and it just works. Connecting the signal in code means the
# connection is self-contained. As your game grows, code-based connections
# become the standard for reusable components.
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if body.is_in_group("player"):
SceneManager.change_scene(target_scene, target_spawn_point)
Note: The Area2D's default collision mask monitors layer 1, which is the same layer the player's CharacterBody2D is on by default. If you changed collision layers in Module 5, make sure the exit zone's Collision → Mask includes the player's layer.
Attach this script to ExitToWhisperwood. In the Inspector, set:
- Target Scene:
res://scenes/whisperwood/whisperwood.tscn - Target Spawn Point:
from_town
Note:
@export_file("*.tscn")creates a file picker in the Inspector that only shows.tscnfiles. Much easier than typing paths manually.
#Player Groups
The exit zone checks body.is_in_group("player"). We need to add the player to this group.
Groups are tags you can assign to any node. A node can belong to multiple groups, and you can find all nodes in a group with get_tree().get_nodes_in_group("name") or get the first match with get_tree().get_first_node_in_group("name"). Think of them as labels for querying; they let systems find nodes without hard-coded paths.
- Open
player.tscn. - Select the
Player(CharacterBody2D) root node. - Go to the Node tab (next to Inspector) → Groups section.
- Type
playerand click Add.
See: Groups: Godot's node tagging system. We'll use groups again in later modules for encounter zones, UI elements, and save points.
#Spawn Points
In Pokemon, walking from Route 1 into Viridian City places you at the south entrance. Flying to Viridian City places you at the Pokemon Center. Same destination, different spawn position depending on how you arrived. This is why spawn points need names: the SceneManager doesn't just load a scene, it loads a scene and places you at a specific named location.
A spawn point is a simple Marker2D node that marks where the player should appear. Add them to your scenes:
In willowbrook.tscn:
- Add a Marker2D node. Rename it to
default. - Position it where the player should start (town center).
- Add it to the
spawn_pointsgroup. - Add another Marker2D named
from_forest, positioned near the south exit. - Add it to the
spawn_pointsgroup too.
The SceneManager's _place_player_at_spawn() finds these markers by group and name, and teleports the player to the matching one.
#Building Whisperwood Forest
Create a second area to connect to:
- Create a new folder:
res://scenes/whisperwood/ - Create a new scene:
Node2Droot, rename toWhisperwood. - Save as
res://scenes/whisperwood/whisperwood.tscn.
Reuse the same TileSet from Module 5 (town_tileset.tres). Add TileMapLayers using the same workflow (Ground, Detail, Objects, AbovePlayer) and assign the TileSet to each. In Module 16, we'll create a dedicated dungeon tileset with a different aesthetic.
Design a simple forest area (at least 20x15 tiles). Use grass tiles for ground, tree tiles for borders, and path tiles through the center:
- Paint rows 0-2 and 13-15 as dense trees on the Ground layer (collision-enabled)
- Leave a 3-tile-wide path winding through the middle
- An entrance on the north side (connecting to Willowbrook)
- An exit on the south side (leading to the Crystal Cavern, which we'll build in Module 16)
Whisperwood scene structure checklist (make sure you have all of these):
Whisperwood(Node2D), rootGround(TileMapLayer), grass, pathsDetail(TileMapLayer), small decorationsYSortGroup(Node2D,y_sort_enabled = true)Objects(TileMapLayer,y_sort_enabled = true), trees, rocks- Player instance (
player.tscn)
AbovePlayer(TileMapLayer), treetop canopy- Spawn points (Marker2D nodes, added to
spawn_pointsgroup):from_town: near the north entrancedefault: same position asfrom_town
- Exit zone:
ExitToWillowbrook(Area2D +exit_zone.gd) at the north edge, pointing tores://scenes/willowbrook/willowbrook.tscnwith spawn pointfrom_forest
If you test and see an empty forest with no player, check that you instanced player.tscn into the YSortGroup (not the root).
Note: For now, we're placing the Player instance directly in each scene. This means there are technically multiple Player instances across scenes. That's fine because only one scene is loaded at a time. In a more complex setup, you might have the SceneManager spawn the player dynamically.
#Signal Lifecycle Across Scene Changes
An important detail to understand: when you call get_tree().change_scene_to_file(), the current scene is freed, and all its nodes are removed from the tree and deleted. This means:
- All signal connections within that scene are cleaned up automatically (as we discussed in Module 3).
- The SceneManager's signals (
transition_started,transition_finished) still work because the SceneManager is an autoload, so it's never freed. - Any signals connected TO an autoload FROM a scene node are also cleaned up when the scene node is freed, so there are no dangling references.
This is why autoloads are the right home for cross-scene systems. They're the stable foundation that persists while the world changes around them.
#Testing the Flow
- Set
willowbrook.tscnas the main scene (Project Settings → Application → Run → Main Scene). - Press F5.
- Walk the player to the south exit.
- The screen should fade to black, then the forest appears, with the player at the
from_townspawn point. - Walk north in the forest to return to Willowbrook, arriving at the
from_forestspawn point.
If it works, congratulations. You have a connected game world.
#Troubleshooting
| Problem | Likely Cause |
|---|---|
| Player doesn't trigger the exit zone | Player not in player group, or exit zone collision shape is missing |
| Scene changes but player is at wrong position | Spawn point name doesn't match, or spawn point isn't in spawn_points group |
| Fade effect not visible | CanvasLayer layer not high enough, or ColorRect not covering the screen |
| Crash on scene change | Target scene path is wrong. Check for typos in the Inspector |
| Player stuck after transition | _is_transitioning flag not reset. Check await chain |
#The Autoload Reference Card
We'll maintain this running table throughout the tutorial, adding each new autoload as we build it:
| Autoload | Module | Purpose |
|---|---|---|
| SceneManager | 7 | Scene transitions with fade effects |
Updated in future modules as we add more autoloads.
#What We've Learned
- Autoloads are globally accessible singletons that persist across scene changes.
SceneManageris our first custom one. Input,Engine,Time, etc. are Godot's built-in autoloads; you've been using them since Module 2.get_tree().change_scene_to_file()is Godot's built-in scene change, but it's abrupt. A SceneManager adds fade transitions and spawn point management.awaitpauses a function until a signal fires, making async sequences readable as linear code.- Area2D exit zones detect the player and trigger scene changes.
- Spawn points (Marker2D nodes in groups) tell the SceneManager where to place the player in the new scene.
- CanvasLayer with a high layer number draws overlays on top of everything.
- Signal connections are automatically cleaned up when scene nodes are freed. Autoload signals persist.
@export_file("*.tscn")creates a filtered file picker in the Inspector.
#What You Should See
When you press F5:
- Playing in Willowbrook, walking to the south edge triggers a fade-to-black
- The Whisperwood forest fades in with the player at the entrance
- Walking north in the forest fades back to Willowbrook
- Transitions are smooth (0.3s fade out, 0.3s fade in)
- Player appears at the correct spawn point each time
#Next Module
We can explore two areas now, but the world feels empty. Before we add NPCs and dialogue, we need to establish our data architecture. In Module 9: Resources, The Data Layer, we'll learn how to define game data (items, characters, NPC info) as reusable, editor-friendly Resource classes. This is the foundation every system from inventory to combat will build on.