后台加载

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

ResourceInteractiveLoader 类允许在阶段中加载资源。每次调用 poll 方法时,都会加载一个新阶段,并将控制权返回给调用方。每个阶段通常是由主资源加载的子资源。例如,如果您正在加载一个场景,该场景加载10幅图像,那么每个图像将是一个阶段。

用法

一般用法如下

获取ResourceInteractiveLoader

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

此方法将向您提供一个ResourceInteractiveLoader,您将使用它来管理加载操作。

轮询

Error ResourceInteractiveLoader::poll();

使用此方法可以推进加载的进度。 每次调用 poll 都会加载资源的下一个阶段。 请记住,每个阶段都是一个完整的``原子``资源,例如图像或网格,它往往需要几帧才能加载。

没有错误时返回 OK ,加载完成后返回 ERR_FILE_EOF 。 返回其他任何值时表示存在错误并且已停止加载。

加载进度(可选)

要查询加载进度,请使用以下方法:

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

get_stage_count 返回要加载的阶段总数。get_stage 返回当前正在加载的阶段。

强制完成(可选)

Error ResourceInteractiveLoader::wait();

如果需要在当前帧中加载整个资源,请使用此方法,而无需执行别的步骤。

获取资源

Ref<Resource> ResourceInteractiveLoader::get_resource();

如果一切顺利,请使用此方法检索已加载的资源。

示例

此示例演示如何加载新场景。 请结合 单例(自动加载) 示例来看。

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

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

    var t = OS.get_ticks_msec()
    while OS.get_ticks_msec() < t + time_max: # use "time_max" to control for how long we block this thread

        # 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

一些额外的辅助函数。 update_progress 更新进度条,或者也可以更新暂停的动画(动画从头到尾表示整个加载过程)。 set_new_scene 将新加载的场景放在树上。 因为它是一个被加载的场景,所以需要在从加载器获得的资源上调用 instance()

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)

使用多个线程

多个线程可以用在ResourceInteractiveLoader中。 如果您尝试一下,请记住以下几点:

Use a semaphore

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).

在轮询期间不阻塞主线程

If you have a mutex to allow calls from the main thread to your loader class, don't lock the main thread while you call poll on your loader class. When a resource is done loading, it might require some resources from the low-level APIs (VisualServer, etc), which might need to lock the main thread to acquire them. This might cause a deadlock if the main thread is waiting for your mutex while your thread is waiting to load a resource.

示例类

您可以在这里找到一个用于在线程中加载资源的示例类: resource_queue.gd。 用法如下:

func start()

在实例化类之后调用以启动线程。

func queue_resource(path, p_in_front = false)

Queue a resource. Use optional argument "p_in_front" to put it in front of the queue.

func cancel_resource(path)

从队列中删除资源,丢弃任何已完成的加载。

func is_ready(path)

Returns true if a resource is fully loaded and ready to be retrieved.

func get_progress(path)

Get the progress of a resource. Returns -1 if there was an error (for example if the resource is not in the queue), or a number between 0.0 and 1.0 with the progress of the load. Use mostly for cosmetic purposes (updating progress bars, etc), use is_ready to find out if a resource is actually ready.

func get_resource(path)

Returns the fully loaded resource, or null on error. If the resource is not fully loaded (is_ready returns false), it will block your thread and finish the load. If the resource is not on the queue, it will call ResourceLoader::load to load it normally and return it.

示例:

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

Note: this code, in its current form, is not tested in real world scenarios. If you run into any issues, ask for help in one of Godot's community channels.