Chargement en arrière-plan

Lorsque vous changez la scène principale de votre jeu (par exemple pour passer à un nouveau niveau), vous pouvez afficher un écran de chargement avec une indication de progression. La méthode de chargement principale (ResourceLoader::load ou simplement load depuis GDScript) bloque votre thread, faisant apparaître votre jeu comme figé et ne répondant pas pendant le chargement de la ressource. Ce document discute de l'alternative utilisant la classe ResourceInteractiveLoader pour des écrans de chargement plus fluides.

ResourceInteractiveLoader

La classe ResourceInteractiveLoader permet de charger une ressource par étapes. Chaque fois que la méthode poll est appelée, une nouvelle étape est chargée, et le contrôle est renvoyé à l'appelant. Chaque étape est généralement une sous-ressource qui est chargée par la ressource principale. Par exemple, si vous chargez une scène qui contient 10 images, chaque image sera une étape.

Utilisation

L'utilisation est généralement comme il suit

Obtenir un ResourceInteractiveLoader

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

Cette méthode vous donnera un ResourceInteractiveLoader que vous utiliserez pour gérer l'opération de chargement.

Interrogation

Error ResourceInteractiveLoader::poll();

Utilisez cette méthode pour faire avancer la progression du chargement. Chaque appel de poll chargera l'étape suivante de votre ressource. Gardez à l'esprit que chaque étape est une ressource "atomique" entière, comme une image ou un maillage, et qu'il faudra donc plusieurs images(frames) pour la charger.

Retourne OK si aucune erreur, ERR_FILE_EOF quand le chargement est terminé. Toute autre valeur de retour signifie qu'il y a eu une erreur et que le chargement s'est arrêté.

Progression du chargement (en option)

Pour interroger la progression du chargement, utilisez les méthodes suivantes :

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

get_stage_count renvoie le nombre total d'étapes à charger. get_stage renvoie l'étage en cours de chargement.

Forcer l'achèvement (facultatif)

Error ResourceInteractiveLoader::wait();

Utilisez cette méthode si vous avez besoin de charger la ressource entière pendant l'image(frame) actuelle, sans aucune autre étape.

Obtention de la ressource

Ref<Resource> ResourceInteractiveLoader::get_resource();

Si tout va bien, utilisez cette méthode pour récupérer votre ressource chargée.

Exemple

Cet exemple montre comment charger une nouvelle scène. Considérez le dans le contexte de l'exemple Singletons (Chargement Automatique).

Tout d'abord, nous mettons en place quelques variables et initialisons la scène_courante avec la scène principale du jeu :

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)

La fonction goto_scene est appelée depuis le jeu lorsque la scène doit être changée. Il demande un chargeur interactif, et appelle set_process(true) pour commencer à interroger le chargeur dans le callback _process. Il démarre également une animation de "chargement", qui peut afficher une barre de progression ou un écran de chargement.

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 est l'endroit où le chargeur est interrogé. poll est appelé, et ensuite nous traitons la valeur de retour de cet appel. OK veut dire continuer le polling, ERR_FILE_EOF veut dire que le chargement est fait, tout autre chose veut dire qu'il y a eu une erreur. Notez aussi que nous sautons une image (via wait_frames, défini sur la fonction goto_scene) pour permettre à l'écran de chargement de s'afficher.

Notez comment nous utilisons OS.get_ticks_msec pour contrôler la durée de blocage du thread. Certaines étapes peuvent se charger rapidement, ce qui veut dire que nous pourrions être capables de faire plus d'un appel à poll dans une seule image ; certaines peuvent prendre beaucoup plus que votre valeur time_max, donc gardez à l'esprit que nous n'aurons pas de contrôle précis sur les 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

Quelques fonctions d'aide supplémentaires. update_progress met à jour une barre de progression, ou peut aussi mettre à jour une animation en pause (l'animation représente le processus de chargement complet du début à la fin). set_new_scene met la scène nouvellement chargée sur l'arbre. Comme il s'agit d'une scène en cours de chargement, instance() doit être appelée sur la ressource obtenue du 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)

Utiliser plusieurs fils d'exécution

ResourceInteractiveLoader peut être utilisé à partir de plusieurs threads. Deux ou trois choses à garder à l'esprit si vous essayez :

Utiliser un sémaphore

Pendant que votre thread attend que le thread principal demande une nouvelle ressource, utilisez un Semaphore pour dormir (au lieu d'une boucle occupée ou quelque chose de similaire).

Ne pas bloquer le fil principal pendant l'interrogation

Si vous avez un mutex pour autoriser les appels du thread principal à votre classe de chargeur, ne verrouillez pas le thread principal pendant que vous appelez poll sur votre classe de chargeur. Lorsqu'une ressource a fini de se charger, elle peut nécessiter certaines ressources des API de bas niveau (VisualServer, etc.), qui peuvent avoir besoin de verrouiller le thread principal pour les acquérir. Cela peut provoquer un blocage si le fil principal attend votre mutex alors que votre fil attend de charger une ressource.

Exemple de classe

Vous pouvez trouver un exemple de classe pour charger des ressources dans les threads ici : resource_queue.gd. L'utilisation est la suivante :

func start()

Appelez la classe après l'avoir instanciée pour démarrer le thread.

func queue_resource(path, p_in_front = false)

Mettez une ressource en file d'attente. Utilisez l'argument optionnel "p_in_front" pour le mettre en tête de la file d'attente.

func cancel_resource(path)

Supprimez une ressource de la file d'attente, en éliminant tout chargement effectué.

func is_ready(path)

Retourne true si une ressource est entièrement chargée et prête à être récupérée.

func get_progress(path)

Obtenir la progression d'une ressource. Renvoie -1 s'il y a eu une erreur (par exemple si la ressource n'est pas dans la file d'attente), ou un nombre entre 0.0 et 1.0 avec l'avancement de la charge. Utilisé surtout pour des raisons esthétiques (mise à jour des barres de progression, etc), utilisez "is_ready" pour savoir si une ressource est réellement prête.

func get_resource(path)

Renvoie la ressource entièrement chargée, ou null en cas d'erreur. Si la ressource n'est pas complètement chargée (is_ready renvoie false), elle bloquera votre thread et terminera le chargement. Si la ressource n'est pas dans la file d'attente, il appellera ResourceLoader::load pour la charger normalement et la renvoyer.

Exemple :

# 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")

Remarque : ce code, dans sa forme actuelle, n'est pas testé dans des scénarios réels. Si vous rencontrez un problème, demandez de l'aide sur l'un des canaux communautaires de Godot.