How to transition between scenes (levels)

Godot Version

4.5

Question

Very similar to this thread - I tried to copy the solution but I couldn’t implement the animation because I couldn’t find any documentation on how to actually make them. I don’t care about the actual animation but I worry that the delay caused by animations is the reason for my unexpected behaviour.

Here is the relevant code (largely copied from the thread I linked, with the animation parts commented out):

# main.gd
extends Node

func _ready() -> void:
	TransitionLayer.current_scene = self
	
	await self.ready
	TransitionLayer.call_deferred("transition_to_scene", "surface")
# transition_layer.gd
extends Node

# Variable that holds the current scene as a reference
var current_scene

# Variable that holds all scene paths
var scenes :Dictionary = {
	"surface": load("res://Scenes/surface_world.tscn"),
	"level_1": load("res://Scenes/Dungeon/level1.tscn")
}

# We force the TransitionLayer to spend as much as time as it wants until all scenes are loaded
func _ready() -> void:
	await self.ready

# Utility function that implements the transition to a new scene (with a proper transition animation)
func transition_to_scene(levelname: String) -> void:
	print_debug("transitioning")
	if scenes[levelname] == null:
		push_error("Levelname: ", levelname, " does NOT exist!")
		return
		
	#$AnimationPlayer.play("fade_to_black")
	#await $AnimationPlayer.animation_finished
	
	# We make a switcheroo of scenes. First add the new scene to the root, then remove the old scene.
	var instance = scenes[levelname].instantiate()
	get_tree().root.add_child(instance)
	get_tree().root.remove_child(current_scene)
	current_scene = instance
	
	#$AnimationPlayer.play_backwards("fade_to_black")
# surface_world.gd
class_name SurfaceWorld extends Node2D

func _ready() -> void:
	# Allow all actions:
	Globals.can_commit_action = true
	
	$Player.position = Vector2(8,40)
	$Player.target = $Player.position

func _on_interact(facing_tile_coordinates:Vector2):
	# Check if the interaction is valid:
	var data = $TileMap/Foreground.get_cell_tile_data(facing_tile_coordinates)
	if data != null and data.get_custom_data("waypoint"):
		#get_tree().change_scene_to_file("res://Scenes/level1.tscn")
		TransitionLayer.transition_to_scene("level_1")
# tilemap.gd (confusing name but this is level 1)
func _on_interact(facing_tile_coordinates):
    var data = foreground.get_cell_tile_data(facing_tile_coordinates)
	if data != null:
		if data.get_custom_data("waypoint"):
			#get_tree().change_scene_to_file("res://Scenes/surface_world.tscn")
			TransitionLayer.transition_to_scene("surface")

I’m not receiving any errors, but when I launch the game (main is the “main scene”, transition_layer is an autoload) it doesn’t show anything but a grey screen. I know that the transition_to_scene() function is being called but it doesn’t seem to be doing anything.

Apparently the current scene is null?

I just checked my Project. The grey screen is likely caused by two things: your TransitionLayer extends Node (so it can’t render anything visual), and the manual root child swapping can leave the scene tree in a weird state.

A approach that avoids both problems: make your TransitionLayer a “CanvasLayer” and use get_tree().change_scene_to_file() instead of manually adding/removing root children. This way Godot handles the scene swap cleanly, and the CanvasLayer can draw a transition overlay on top.

Here’s a minimal working version (set as Autoload):

# transition_layer.gd
extends CanvasLayer

var _overlay: ColorRect
var _active: bool = false

func _ready() -> void:
    layer = 100
    _overlay = ColorRect.new()
    _overlay.color = Color(0, 0, 0, 0)
    _overlay.anchors_preset = Control.PRESET_FULL_RECT
    _overlay.mouse_filter = Control.MOUSE_FILTER_IGNORE
    add_child(_overlay)

func transition_to_scene(path: String, dur: float = 0.4) -> void:
    if _active: return
    _active = true
    var tw := create_tween()
    tw.tween_property(_overlay, "color:a", 1.0, dur)
    tw.tween_callback(func(): get_tree().change_scene_to_file(path))
    tw.tween_interval(0.1)
    tw.tween_property(_overlay, "color:a", 0.0, dur)
    tw.tween_callback(func(): _active = false)

Then call it with the actual file path:

TransitionLayer.transition_to_scene("res://Scenes/surface_world.tscn")

No need to preload scenes into a dictionary, no manual root child management, no AnimationPlayer needed. The ColorRect fades to black, Godot swaps the scene, then it fades back in.

3 Likes

If you want another example (or a plugin that does it for you) take a look at my Game Template Plugin It gives you a nice loading progress bar. It also handles a bunch of other things.

Specifically take a look at the Loading State, the Gameplay State and which ever of the example folders (2D or 3D) that fits your project.

3 Likes

Thank you so much! Works perfectly!

1 Like