Laden im Hintergrund

When switching the main scene of your game (e.g. going to a new level), you might want to show a loading screen with some indication that progress is being made. The main load method (ResourceLoader::load or just load from GDScript) blocks your thread, making your game appear frozen and unresponsive while the resource is being loaded. This document discusses the alternative of using the ResourceInteractiveLoader class for smoother load screens.

ResourceInteractiveLoader

The ResourceInteractiveLoader class allows you to load a resource in stages. Every time the method poll is called, a new stage is loaded, and control is returned to the caller. Each stage is generally a sub-resource that is loaded by the main resource. For example, if you're loading a scene that loads 10 images, each image will be one stage.

Nutzung

Die Verwendung ist im Allgemeinen wie folgt

Abrufen eines ResourceInteractiveLoader

Ref<ResourceInteractiveLoader> ResourceLoader::load_interactive(String p_path);

Mit dieser Methode erhalten Sie einen ResourceInteractiveLoader, mit dem Sie den Ladevorgang verwalten.

Abfragen

Error ResourceInteractiveLoader::poll();

Use this method to advance the progress of the load. Each call to poll will load the next stage of your resource. Keep in mind that each stage is one entire "atomic" resource, such as an image, or a mesh, so it will take several frames to load.

Returns OK on no errors, ERR_FILE_EOF when loading is finished. Any other return value means there was an error and loading has stopped.

Ladefortschritt (optional)

Verwenden Sie die folgenden Methoden, um den Fortschritt des Ladevorgangs abzufragen:

int ResourceInteractiveLoader::get_stage_count() const;
int ResourceInteractiveLoader::get_stage() const;

get_stage_count returns the total number of stages to load. get_stage returns the current stage being loaded.

Beendigung erzwingen (optional)

Error ResourceInteractiveLoader::wait();

Use this method if you need to load the entire resource in the current frame, without any more steps.

Die Ressource erhalten

Ref<Resource> ResourceInteractiveLoader::get_resource();

Wenn alles gut geht, verwenden Sie diese Methode, um Ihre geladene Ressource abzurufen.

Beispiel

This example demonstrates how to load a new scene. Consider it in the context of the Singletons (Entwurfsmuster) - AutoLoad example.

First, we set up some variables and initialize the current_scene with the main scene of the game:

var loader
var wait_frames
var time_max = 100 # msec
var current_scene


func _ready():
    var root = get_tree().get_root()
    current_scene = root.get_child(root.get_child_count() -1)

The function goto_scene is called from the game when the scene needs to be switched. It requests an interactive loader, and calls set_process(true) to start polling the loader in the _process callback. It also starts a "loading" animation, which could show a progress bar or loading screen.

func goto_scene(path): # Game requests to switch to this scene.
    loader = ResourceLoader.load_interactive(path)
    if loader == null: # Check for errors.
        show_error()
        return
    set_process(true)

    current_scene.queue_free() # Get rid of the old scene.

    # Start your "loading..." animation.
    get_node("animation").play("loading")

    wait_frames = 1

_process is where the loader is polled. poll is called, and then we deal with the return value from that call. OK means keep polling, ERR_FILE_EOF means loading is done, anything else means there was an error. Also note we skip one frame (via wait_frames, set on the goto_scene function) to allow the loading screen to show up.

Note how we use OS.get_ticks_msec to control how long we block the thread. Some stages might load fast, which means we might be able to cram more than one call to poll in one frame; some might take way more than your value for time_max, so keep in mind we won't have precise control over the timings.

func _process(time):
    if loader == null:
        # no need to process anymore
        set_process(false)
        return

    # Wait for frames to let the "loading" animation show up.
    if wait_frames > 0:
        wait_frames -= 1
        return

    var t = OS.get_ticks_msec()
    # Use "time_max" to control for how long we block this thread.
    while OS.get_ticks_msec() < t + time_max:
        # Poll your loader.
        var err = loader.poll()

        if err == ERR_FILE_EOF: # Finished loading.
            var resource = loader.get_resource()
            loader = null
            set_new_scene(resource)
            break
        elif err == OK:
            update_progress()
        else: # Error during loading.
            show_error()
            loader = null
            break

Some extra helper functions. update_progress updates a progress bar, or can also update a paused animation (the animation represents the entire load process from beginning to end). set_new_scene puts the newly loaded scene on the tree. Because it's a scene being loaded, instance() needs to be called on the resource obtained from the loader.

func update_progress():
    var progress = float(loader.get_stage()) / loader.get_stage_count()
    # Update your progress bar?
    get_node("progress").set_progress(progress)

    # ...or update a progress animation?
    var length = get_node("animation").get_current_animation_length()

    # Call this on a paused animation. Use "true" as the second argument to
    # force the animation to update.
    get_node("animation").seek(progress * length, true)


func set_new_scene(scene_resource):
    current_scene = scene_resource.instance()
    get_node("/root").add_child(current_scene)

Using multiple threads

ResourceInteractiveLoader can be used from multiple threads. A couple of things to keep in mind if you attempt it:

Semaphoren verwenden

While your thread waits for the main thread to request a new resource, use a Semaphore to sleep (instead of a busy loop or anything similar).

Not blocking main thread during the polling

Wenn Sie ein Mutex haben, um Aufrufe vom Hauptthread zu Ihrer Loader-Klasse zuzulassen, sperren Sie den Hauptthread nicht, während Sie poll in Ihrer Loader-Klasse aufrufen. Wenn eine Ressource fertig geladen ist, benötigt sie möglicherweise einige Ressourcen von den APIs auf niedriger Ebene (VisualServer usw.), die möglicherweise den Hauptthread sperren müssen, um sie zu erhalten. Dies kann zu einem Deadlock führen, wenn der Hauptthread auf Ihren Mutex wartet, während Ihr Thread darauf wartet, eine Ressource zu laden.

Beispiel Klasse

Eine Beispielklasse für das Laden von Ressourcen in Threads finden Sie hier: resource_queue.gd. Die Benutzung ist wie folgt:

func start()

Rufe nach der Instanz die Klasse auf, um den Thread zu starten.

func queue_resource(path, p_in_front = false)

Stelle eine Ressource in die Warteschlange. Verwende das optionale Argument "p_in_front", um sie an den Anfang der Warteschlange zu stellen.

func cancel_resource(path)

Entferne eine Ressource aus der Warteschlange und verwerfe dabei jede geladene.

func is_ready(path)

Gibt true zurück, wenn eine Ressource vollständig geladen und bereit zum Abruf ist.

func get_progress(path)

Ruft den Fortschritt einer Ressource ab. Gibt -1 zurück, wenn ein Fehler aufgetreten ist (z.B. wenn die Ressource nicht in der Warteschlange steht), oder eine Zahl zwischen 0,0 und 1,0 mit dem Fortschritt des Ladevorgangs. Verwende diese Funktion hauptsächlich für kosmetische Zwecke (Aktualisierung von Fortschrittsbalken usw.), verwende is_ready, um herauszufinden, ob eine Ressource tatsächlich bereit ist.

func get_resource(path)

Gibt die vollständig geladene Ressource zurück, oder null bei einem Fehler. Wenn die Ressource nicht vollständig geladen ist (is_ready gibt false zurück), blockiert sie Ihren Thread und beendet das Laden. Wenn sich die Ressource nicht in der Warteschlange befindet, wird ResourceLoader::load aufgerufen, um sie normal zu laden und zurückzugeben.

Beispiel:

# Initialize.
queue = preload("res://resource_queue.gd").new()
queue.start()

# Suppose your game starts with a 10 second cutscene, during which the user
# can't interact with the game.
# For that time, we know they won't use the pause menu, so we can queue it
# to load during the cutscene:
queue.queue_resource("res://pause_menu.tres")
start_cutscene()

# Later, when the user presses the pause button for the first time:
pause_menu = queue.get_resource("res://pause_menu.tres").instance()
pause_menu.show()

# When you need a new scene:
queue.queue_resource("res://level_1.tscn", true)
# Use "true" as the second argument to put it at the front of the queue,
# pausing the load of any other resource.

# To check progress.
if queue.is_ready("res://level_1.tscn"):
    show_new_level(queue.get_resource("res://level_1.tscn"))
else:
    update_progress(queue.get_progress("res://level_1.tscn"))

# When the user walks away from the trigger zone in your Metroidvania game:
queue.cancel_resource("res://zone_2.tscn")

Bemerkung: Dieser Code wird in seiner derzeitigen Form nicht in realen Szenarien getestet. Wenn Sie auf irgendwelche Probleme stoßen, bitten Sie in einem von Godots Gemeinschaftskanälen um Hilfe.