Current scene returns null issue with background loading in 4.2

Godot Version

4.2.1 Stable

Question

I would like to know the current scene right after it’s loaded.

The code is the following:

extends Node

@onready var loading_screen_scene = preload("res://loading_screen/loading_screen.tscn")

var new_scene_path: String
var loading_screen_scene_instance
var loading: bool = false

func load_scene(path: String) -> void:
	var current_scene = get_tree().current_scene
	
	loading_screen_scene_instance = loading_screen_scene.instantiate()
	get_tree().root.call_deferred("add_child", loading_screen_scene_instance)
	
	if ResourceLoader.has_cached(path):
		ResourceLoader.load_threaded_get(path)
	else:
		ResourceLoader.load_threaded_request(path)
	
	current_scene.queue_free()
	
	loading = true
	new_scene_path = path

func _process(_delta: float) -> void:
	if not loading:
		return
	
	var progress: Array = []
	var status = ResourceLoader.load_threaded_get_status(new_scene_path, progress)
	
	if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
		var progress_bar = loading_screen_scene_instance.get_node("ColorRect/MarginContainer/VBoxContainer/ProgressBar")
		progress_bar.value = progress[0] * 100
	elif status == ResourceLoader.THREAD_LOAD_LOADED:
		get_tree().change_scene_to_packed(ResourceLoader.load_threaded_get(new_scene_path))
		loading_screen_scene_instance.queue_free()
		loading = false
	else:
		print("Something went wrong when loading new scene...")

As per the docs, calling get_tree().current_scene right after change_scene_to_packed does indeed returns null and that is intended behaviour.

Problem is, even using call_deferred - or call_deferred_thread_group - right after change_scene_to_packed also returns null. I thought this way it was supposed to return the new current_scene because it would be called after the frame ended?

What I was able to do without adding arbitrary timers - even a 0.00001 timer would work, becase all it needs is to wait for the next frame I guess - , is to call_deferred after call_deferred_thread_group and that got me the expected behaviour: to return the new current_scene right after it’s finished loading.

So it would be something like:

...
	elif status == ResourceLoader.THREAD_LOAD_LOADED:
		get_tree().change_scene_to_packed(ResourceLoader.load_threaded_get(new_scene_path))
		call_deferred("defer_tg_print_current_scene")
		loading_screen_scene_instance.queue_free()
		loading = false
	else:
		print("Something went wrong when loading new scene...")

func print_current_scene() -> void:
	print(get_tree().current_scene)

func defer_tg_print_current_scene() -> void:
	call_deferred_thread_group("print_current_scene")

But I supposed that’s not the way this should be done?

Help please?

If you need access to the new scene immediately, you could change the scene manually instead of using change_scene_to_packed().

func _process(_delta: float) -> void:
    ...
    _goto_scene.call_deferred(ResourceLoader.load_threaded_get(new_scene_path))

func _goto_scene(new_scene: PackedScene) -> void:
    var tree := get_tree()
    tree.current_scene.free()
    current_scene = new_scene.instantiate()
    tree.root.add_child(current_scene)
    # Now current_scene is set and added to the tree.

Thanks for the insight @grulps, but got a bunch of errors with this approach.

_goto_scene.call_deferred(ResourceLoader.load_threaded_get(new_scene_path))

Should be called on THREAD_LOAD_LOADED, right?

If so, these were the problems:

  • current_scene was already null because it was freed in load_scene, I’d say - and even after commenting it in load_scene, current_scene would be null on other occasions.
  • Scenes would not load/unload properly, with visual nodes of different scenes overlapping.

Sorry I can’t be of much help here, I’m still learning the ins and outs of ResourceLoader. The last time I dabbled with loading screens was with Godot 3.2, and I have barely touched programming since I came back to this project.

I guess I could write a more detailed example. Let’s start like this.

extends Node

const LOADING_SCREEN_SCENE = preload("res://loading_screen/loading_screen.tscn")
var new_scene_path: String
var loading: bool = false

Functions that change scenes should be deferred to avoid glitchy behaviour.

func load_scene(path: String) -> void:
    new_scene_path = path
    _load_scene.call_deferred()

func _load_scene() -> void:
    var tree = get_tree()

    # The old scene can be freed immediately.
    # There's no need to call queue_free() when this function is already deferred.
    tree.current_scene.free()

    # If the requested scene is already loaded, we can change to the new scene.
    var status = ResourceLoader.load_threaded_get_status(new_scene_path)
    if status == ResourceLoader.THREAD_LOAD_LOADED:
        var new_scene = ResourceLoader.load_threaded_get(new_scene_path)
        tree.current_scene = new_scene.instantiate()
        tree.root.add_child(current_scene)
    # Otherwise we need to change to the loading screen and start the loading process.
    elif status == ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
        tree.current_scene = LOADING_SCREEN_SCENE.instantiate()
        tree.root.add_child(current_scene)
        ResourceLoader.load_threaded_request(new_scene_path)
        loading = true

func _process(_delta: float) -> void:
    if not loading:
        return

    var progress: Array = []
    var status = ResourceLoader.load_threaded_get_status(new_scene_path, progress)
    if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS:
        update_progress_bar_or_something(progress)
    elif status == ResourceLoader.THREAD_LOAD_LOADED:
        loading = false
        _goto_scene.call_deferred(ResourceLoader.load_threaded_get(new_scene_path))

And then add the _goto_scene() as I showed before.