バックグラウンド読み込み

ゲームのメインシーンを切り替える(たとえば、新しいレベルに移動する)場合は、進行中の状態を示すロード画面を表示することができます。メインのロードメソッド ResourceLoader::load または単にGDScriptからの load)はスレッドをブロックし、リソースのロード中にゲームがフリーズして応答しなくなるようにします。このドキュメントでは、ロード画面をよりスムーズにするために ResourceInteractiveLoader クラスを使用する代替方法について説明します。

リソースインタラクティブローダー

ResourceInteractiveLoader クラスを使用すると、リソースを段階的にロードできます。 メソッド poll が呼び出されるたびに、新しいステージがロードされ、制御が呼び出し元に返されます。 通常、各ステージは、メインリソースによってロードされるサブリソースです。 たとえば、10個の画像を読み込むシーンを読み込む場合、各画像は1つのステージになります。

使用法

使用法は一般的に以下の通りです

リソースインタラクティブローダーの取得

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

このメソッドを使用すると、ロード操作の管理に使用するリソースインタラクティブローダーが提供されます。

ポーリング

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();

すべてがうまくいけば、このメソッドを使用して読み込まれたリソースを取得します。

この例は、新しいシーンをロードする方法を示しています。 例題、シングルトン(自動読み込み) のコンテキストで考察してみましょう。

まず、いくつかの変数を設定し、ゲームのメインシーンで current_scene を初期化します:

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)

関数 goto_scene は、シーンを切り替える必要があるときにゲームから呼び出されます。インタラクティブなローダーを要求し、_process コールバックでローダーのポーリングを開始するために set_process(true) を呼び出します。また、プログレスバーまたはロード画面を表示できる「ロード」アニメーションも開始します。

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 はローダーがポーリングされる場所です。poll が呼び出され、その呼び出しからの戻り値を処理します。「OK」はポーリングを続けることを意味し、ERR_FILE_EOF は読み込みが完了したことを意味し、それ以外はエラーが発生したことを意味します。また、ロード画面が表示されるように、1フレームをスキップします( wait_frames を介して、goto_scene 関数で設定)。

OS.get_ticks_msec を使用してスレッドをブロックする時間を制御する方法に注意してください。一部のステージは高速でロードされる場合があります。つまり、1つのフレームで poll を複数回呼び出すことができます。いくつかは time_max の値よりもはるかに大きくなる可能性があるため、タイミングを正確に制御できないことに注意してください。

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

いくつかの追加のヘルパー関数。 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)

複数のスレッドの使用

リソースインタラクティブローダーは、複数のスレッドから使用できます。以下の点に注意してください:

セマフォを使用する

スレッドがメインスレッドが新しいリソースを要求するのを待っている間、(ビジーループまたは同様のものの代わりに) セマフォ(Semaphore) を使用してスリープします。

ポーリング中にメイン スレッドをブロックしない

メインスレッドからローダークラスへの呼び出しを許可するミューテックスがある場合、ローダークラスで poll を呼び出している間はメインスレッドをロックしないでください。リソースの読み込みが完了すると、リソースを取得するためにメイン スレッドをロックする必要がある低レベルの API (VisualServer など) からいくつかのリソースが必要になる場合があります。これは、スレッドがリソースのロードを待機している間にメインスレッドがミューテックスを待機している場合、デッドロックを引き起こす可能性があります。

サンプルクラス

スレッドでリソースをロードするためのサンプルクラスはここです: resource_queue.gd。使用法は次のとおりです:

func start()

スレッドを開始するクラスをインスタンスした後に呼び出します。

func queue_resource(path, p_in_front = false)

リソースをキューに入れます。キューの先頭に置く場合は、オプションの引数 "p_in_front" を使用します。

func cancel_resource(path)

キューからリソースを削除し、行われた読み込みを破棄します。

func is_ready(path)

リソースが完全に読み込まれて取得できる状態にある場合は、true を返します。

func get_progress(path)

リソースの進行状況を取得します。エラーが発生した場合(たとえば、リソースがキューにない場合)は-1を返します。または、読み込みの進行とともに0.0〜1.0の数値を返します。主に装飾を目的(進行状況バーの更新など)に使用し、is_ready を使用してリソースが実際に準備できているかどうかを確認します。

func get_resource(path)

完全に読み込まれたリソース、またはエラーの場合は null を返します。リソースが完全に読み込まれていない場合(is_readyfalse を返します)、スレッドがブロックされ、読み込みが終了します。リソースがキューにない場合は ResourceLoader::load を呼び出して、通常どおりに読み込んで返します。

例:

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