Carga en segundo plano

Al cambiar la escena principal del juego (por ejemplo, al pasar a un nuevo nivel), puede que quieras mostrar una pantalla de carga con alguna indicación de que se está progresando. El método de carga principal (ResourceLoader::load or just load de GDScript) bloquea el hilo, haciendo que el juego parezca congelado y sin respuesta mientras se carga el recurso. Este documento presenta una alternativa al uso de la clase ResourceInteractiveLoader para que las pantallas de carga sean más fluidas.

ResourceInteractiveLoader

La clase ResourceInteractiveLoader te permite cargar un recurso por etapas. Cada vez que se llama al método poll, se carga una nueva etapa, y el control devuelve la llamada. Cada etapa es generalmente un subrecurso que es cargado por el recurso principal. Por ejemplo, si estás cargando una escena que carga 10 imágenes, cada imagen será una etapa.

Uso

El uso por lo general es el siguiente

Obtención de un ResourceInteractiveLoader

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

Este método te proporcionará un ResourceInteractiveLoader que usarás para gestionar la operación de carga.

Polling

Error ResourceInteractiveLoader::poll();

Utiliza este método para avanzar en el progreso de carga. Cada llamada a poll cargará la siguiente etapa de su recurso. Ten en cuenta que cada etapa es un recurso "atómico" entero, como una imagen, o un mesh, por lo que tardará varios fotogramas en cargarse.

Regresa OK si no hay errores, ERR_FILE_EOF cuando la carga ha terminado. Cualquier otro valor de retorno significa que hubo un error y la carga se ha detenido.

Progreso de carga (opcional)

Para consultar el progreso de carga, utiliza los siguientes métodos:

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

get_stage_count devuelve el número total de etapas a cargar. get_stage devuelve la etapa actual que se está cargando.

Forzar su finalización (opcional)

Error ResourceInteractiveLoader::wait();

Utiliza este método si necesitas cargar todo el recurso en el fotograma actual, sin más pasos.

Obtención del recurso

Ref<Resource> ResourceInteractiveLoader::get_resource();

Si todo va bien, utiliza este método para recuperar tu recurso cargado.

Ejemplo

Este ejemplo demuestra cómo cargar una nueva escena. Considéralo en el contexto del ejemplo Singletons (AutoLoad).

Primero, creamos algunas variables e iniciamos la current_scene con la escena principal del juego:

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 función goto_scene es llamada desde el juego cuando la escena necesita ser cambiada. Solicita un cargador interactivo, y llama a set_process(true) para empezar a consultar al cargador en la llamada _process. También inicia una animación de "carga", que podría mostrar una barra de progreso o una pantalla de carga.

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 es donde el cargador es consultado. poll es llamado, y luego nos ocupamos del valor de retorno de esa llamada. OK significa seguir consultando, ERR_FILE_EOF significa que la carga está completa, cualquier otra cosa significa que hubo un error. También ten en cuenta que nos saltamos un cuadro (a través de wait_frames, en la función goto_scene) para permitir que se muestre la pantalla de carga.

Fíjate en cómo se utiliza OS.get_ticks_msec para controlar el tiempo que bloqueamos el hilo. Algunas etapas pueden cargarse rápidamente, lo que significa que podemos ser capaces de meter más de una llamada a poll en un fotograma; algunas pueden tomar mucho más que tu valor de time_max, así que ten en cuenta que no tendremos un control preciso de los tiempos.

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

Algunas funciones de ayuda extra. update_progress actualiza una barra de progreso, o también puede actualizar una animación pausada (la animación representa el proceso de carga completo de principio a fin). set_new_scene pone la escena recién cargada en el árbol. Debido a que es una escena que se está cargando, instance() necesita ser llamada en el recurso obtenido del cargador.

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)

Usando múltiples hilos

ResourceInteractiveLoader puede ser usado desde múltiples hilos. Hay un par de cosas que hay que tener en cuenta si lo intentas:

Usa un semáforo

Mientras tu hilo espera a que el hilo principal solicite un nuevo recurso, usa un Semaphore para dormir (en lugar de un bucle o algo similar).

No bloquear el hilo principal durante la consulta

Si tienes un mutex para permitir llamadas desde el hilo principal a la clase del cargador, no bloquees el hilo principal mientras llames a poll en la clase del cargador. Cuando un recurso termina de cargar, puede requerir ciertos recursos de las APIs de bajo nivel (VisualServer, etc.), que pueden necesitar bloquear el hilo principal para obtenerlos. Esto podría causar un bloqueo si el hilo principal está esperando tu mutex mientras tu hilo está esperando para cargar un recurso.

Ejemplo de clase

Puedes encontrar un ejemplo de clase para cargar recursos en los hilos aquí: resource_queue.gd. Su uso es el siguiente:

func start()

Llama después de su instancia a la clase para iniciar el hilo.

func queue_resource(path, p_in_front = false)

Pon en la cola un recurso. Usa el argumento opcional "p_in_front" para ponerlo delante de la cola.

func cancel_resource(path)

Elimina un recurso de la cola, descartando cualquier carga realizada.

func is_ready(path)

Devuelve true si un recurso está completamente cargado y listo para ser recuperado.

func get_progress(path)

Obtiene el progreso de un recurso. Devuelve -1 si hubo un error (por ejemplo si el recurso no está en la cola), o un número entre 0.0 y 1.0 según el progreso de la carga. Úsalo principalmente para fines estéticos (actualizar las barras de progreso, etc.), usa is_ready para saber si un recurso está realmente listo.

func get_resource(path)

Devuelve el recurso completamente cargado, o null en caso de error. Si el recurso no está completamente cargado (is_ready devuelve false), bloqueará tu hilo y terminará la carga. Si el recurso no está en la cola, llamará a ResourceLoader::load para cargarlo normalmente y devolverlo.

Ejemplo:

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

Nota: este código, en su forma actual, no está probado en escenarios reales. Si te encuentras con algún problema, pide ayuda en uno de los canales de la comunidad de 'Godot' <https://godotengine.org/community>`__.