【Godot】Godotの動的ロードとリソース管理【load・preload・バックグラウンド読み込み】

作成: 2026-02-08

load()とpreload()の使い分け、ResourceLoaderによるバックグラウンド読み込み、ロード画面の実装、メモリ管理のベストプラクティスを解説します。

概要

動作確認環境: Godot 4.3+

ゲーム開発で「シーン切り替え時にカクつく」「ロード画面を出したい」と思ったことはありませんか? Godotではリソースの読み込み方法を適切に選ぶだけで、体感パフォーマンスが大きく変わります。

Godotのリソース読み込みには段階があります。「すぐ使う小さなリソースは事前に読んでおく」「大きなリソースは必要になってから読む」「巨大なリソースはバックグラウンドで読みながらロード画面を出す」。この使い分けが、快適なゲーム体験の鍵になります。

本記事では、preload() / load() の基本的な違いから、ResourceLoader による非同期ロード、ロード画面の実装、そしてメモリ管理のコツまでを実践的に解説します。

preload()とload()の違い

Godotのリソース読み込みには2つの基本関数があります。読み込みタイミングとパス指定の柔軟性が異なるため、用途に応じて使い分けることが重要です。

preload() -- スクリプトロード時の読み込み

弾丸やSEなど、毎フレームのように使うリソースには preload() が最適です。スクリプトがロード(パース)される時点でリソースもメモリに読み込まれるので、実行時に呼び出した瞬間の遅延がゼロになります。

「コンパイル時に読み込まれる」と表現されることもありますが、GDScriptはインタプリタ言語なので厳密には「スクリプトのパース時(parse time)」に読み込まれます。実用上の違いはありませんが、知っておくと正確に理解できます。

# スクリプトのロード時に即座に利用可能(パスは定数のみ)
const BulletScene = preload("res://scenes/bullet.tscn")
const HitSound = preload("res://audio/hit.wav")

func shoot():
    var bullet = BulletScene.instantiate()
    add_child(bullet)
  • パスはリテラル文字列のみ (変数不可)
  • スクリプトがロード(パース)される時点でまとめて読み込まれる
  • 小さなリソースや頻繁に使うアセットに最適

load() -- 実行時読み込み

一方、プレイヤーが選んだ武器や、難易度に応じた敵など、条件によって変わるリソースには load() を使います。パスを変数で組み立てられるのが最大の強みです。

# 実行時に動的にロード(変数でパス指定可能)
func load_weapon(weapon_name: String):
    var scene_path = "res://weapons/%s.tscn" % weapon_name
    var weapon_scene = load(scene_path)
    return weapon_scene.instantiate()

# 条件に応じた切り替え
func get_enemy_scene(difficulty: int) -> PackedScene:
    if difficulty >= 3:
        return load("res://enemies/boss.tscn")
    return load("res://enemies/normal.tscn")
  • パスを動的に組み立てられる
  • 初回呼び出し時にディスクから読み込み(キャッシュ後は即座に返る)
  • 大きなリソースや条件付きロードに適する

使い分け早見表

項目preload()load()
読み込みタイミングスクリプトロード時(パース時)実行時
パス指定リテラルのみ変数可
ブロッキングシーン読み込み時呼び出し時
推奨用途弾丸、SE、UI部品ステージデータ、選択式アセット
キャッシュ自動自動(初回のみディスクI/O)

tips: preload() を大量に使うとシーンの初期ロードが遅くなります。特に大きなテクスチャや3Dモデルを preload() するとロード時間への影響が大きいため、サイズの大きなリソースは load() やバックグラウンド読み込みへの切り替えを検討してください。数は問題ではなく、リソースの合計サイズがポイントです。

ResourceLoaderによるバックグラウンド読み込み

preload()load() の使い分けがわかったところで、さらに一歩進んだ読み込み方法を見てみましょう。大きなシーンやステージデータを読み込む場合、load() はメインスレッドをブロックしてフリーズの原因になります。RPGのダンジョン遷移や広大なオープンワールドのチャンク読み込みなど、重いリソースを扱う場面では、ResourceLoader の非同期APIが威力を発揮します。バックグラウンドで読み込みつつゲームを動かし続けられます。

基本的な非同期ロードの流れ

非同期ロードは「リクエスト → 進捗監視 → 取得」の3ステップで実装します。通常の load() は読み込みが完了するまでゲームが止まりますが、この方法なら進捗を監視しながらゲームを動かし続けられます。

# 1. リクエスト開始(別スレッドで読み込み開始)
ResourceLoader.load_threaded_request("res://levels/stage_2.tscn")

# 2. 進捗を確認(毎フレーム呼び出す)
func _process(_delta):
    var progress = []  # 配列で渡す(Godotの仕様)
    var status = ResourceLoader.load_threaded_get_status(
        "res://levels/stage_2.tscn", progress
    )

    match status:
        ResourceLoader.THREAD_LOAD_IN_PROGRESS:
            # progress[0]に0.0〜1.0の進捗が入る
            print("Loading: %d%%" % int(progress[0] * 100))
        ResourceLoader.THREAD_LOAD_LOADED:
            # 3. 読み込み完了 → リソースを取得
            var scene = ResourceLoader.load_threaded_get(
                "res://levels/stage_2.tscn"
            )
            _on_load_complete(scene)
        ResourceLoader.THREAD_LOAD_FAILED:
            printerr("Load failed!")
        ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
            printerr("Invalid path or not requested!")

tips: progress を配列で渡すのは「参照渡しで値を返す」ための Godot の規約です。GDScript の関数は複数の値を直接返せないため、空の配列を渡して progress[0] に進捗値を格納してもらう仕組みです。

3つのAPIの役割

メソッド役割
load_threaded_request(path)非同期ロードを開始
load_threaded_get_status(path, progress)進捗を取得(0.0〜1.0)
load_threaded_get(path)完了後にリソースを取得

load_threaded_get_status() が返すステータスは4種類あります。

ステータス意味
THREAD_LOAD_IN_PROGRESS読み込み中
THREAD_LOAD_LOADED読み込み完了
THREAD_LOAD_FAILED読み込み失敗
THREAD_LOAD_INVALID_RESOURCEパスが無効、またはリクエストされていない

tips: load_threaded_request() の第2引数に "PackedScene" などの型ヒントを渡すと、型チェック付きでロードできます。

use_sub_threads でさらに高速化

load_threaded_request() の第3引数 use_sub_threadstrue にすると、サブリソース(テクスチャ、メッシュなど)も並列でロードされます。大量のサブリソースを含むシーンでは、ロード時間が大幅に短縮されることがあります。

# サブリソースも並列ロード(デフォルトは false)
ResourceLoader.load_threaded_request(
    "res://levels/stage_2.tscn",
    "",     # 型ヒント(空文字で自動判定)
    true    # use_sub_threads = true
)

tips: 同じパスで load_threaded_request() を2回呼ぶとエラーになります。複数箇所からロードを呼び出す可能性がある場合は、事前に load_threaded_get_status() でステータスを確認してから呼び出してください。

ロード画面の実装

非同期ロードのAPIを理解したら、次はそれをプレイヤーに見せるUIを作ってみましょう。バックグラウンド読み込みを使った実用的なロード画面の実装例です。進捗バーとパーセンテージ表示を組み合わせることで、ユーザーに待ち時間を明確に伝えられます。

# LoadingScreen.gd
extends CanvasLayer

@onready var progress_bar: ProgressBar = $ProgressBar
@onready var label: Label = $Label

var target_scene_path: String = ""

func load_scene(scene_path: String):
    target_scene_path = scene_path
    show()

    # 非同期ロード開始
    var err = ResourceLoader.load_threaded_request(scene_path)
    if err != OK:
        printerr("Failed to start loading: %s" % scene_path)
        return

    set_process(true)

func _process(_delta):
    if target_scene_path.is_empty():
        return

    var progress = []
    var status = ResourceLoader.load_threaded_get_status(
        target_scene_path, progress
    )

    match status:
        ResourceLoader.THREAD_LOAD_IN_PROGRESS:
            progress_bar.value = progress[0] * 100
            label.text = "Loading... %d%%" % int(progress[0] * 100)
        ResourceLoader.THREAD_LOAD_LOADED:
            progress_bar.value = 100
            var scene = ResourceLoader.load_threaded_get(target_scene_path)
            get_tree().change_scene_to_packed(scene)
            target_scene_path = ""
            set_process(false)
            hide()
        ResourceLoader.THREAD_LOAD_FAILED:
            label.text = "Load failed."
            set_process(false)

使い方(Autoload登録推奨):

# どこからでも呼び出せる
LoadingScreen.load_scene("res://levels/stage_2.tscn")

tips: この LoadingScreen は Autoload(自動読み込みシングルトン)として登録する必要があります。Autoload でないと、change_scene_to_packed() が現在のシーンツリーを置き換える際に LoadingScreen 自体も破棄されてしまいます。「プロジェクト設定 → Autoload」で登録すれば、シーン遷移後も LoadingScreen は保持されます。

メモリ管理とキャッシュ戦略

ここまでは「どう読み込むか」に注目してきましたが、読み込んだリソースを「どう管理するか」も同じくらい大切です。リソースの読み込みだけでなく、不要になったリソースの解放も重要です。大きなゲームでは、ステージごとに使わないリソースを適切に解放しないとメモリ不足に陥る可能性があります。

Godotのリソースキャッシュ

まず、Godotの組み込みキャッシュの仕組みを知っておきましょう。Godotは load() で読み込んだリソースをパスベースでキャッシュします。同じパスを再度 load() しても、メモリ上のキャッシュから返されます。

# 2回目以降はキャッシュから即座に返る(ディスクI/Oなし)
var tex_a = load("res://textures/player.png")
var tex_b = load("res://textures/player.png")
# tex_a == tex_b(同一インスタンス)

WeakRefで参照を保持する

大きなリソースをキャッシュしつつ、不要時には自動で解放したい場合は WeakRef が有効です。

Godot のリソース(Resource)は 参照カウント方式 で管理されています。これは、そのリソースを参照している変数の数を内部でカウントし、カウントが0になった時点で即座に解放する仕組みです(JavaやC#のようなトレーシング型GCとは異なります)。

通常の変数でリソースを保持すると参照カウントが1以上のままなので解放されませんが、WeakRef は参照カウントを増やしません。つまり、他に参照がなくなれば自動的に解放される「弱い参照」として機能します。

var _cache: Dictionary = {}

func get_resource(path: String) -> Resource:
    # キャッシュにあれば返す
    if _cache.has(path):
        var weak: WeakRef = _cache[path]
        var res = weak.get_ref()
        if res:
            return res

    # なければ読み込んでキャッシュ
    var res = load(path)
    _cache[path] = weakref(res)
    return res

func clear_cache():
    _cache.clear()
    # WeakRefしか持っていないリソースは参照カウントが0になり即解放

get_ref()null を返した場合、そのリソースはすでにメモリから解放されています。その場合は load() で再度読み込むため、アクセス頻度が高いリソースでは通常の変数でキャッシュした方が効率的です。WeakRef キャッシュは「使うかもしれないが、メモリ節約を優先したい」リソースに最適です。

メモリ管理のベストプラクティス

個々のテクニックを把握したところで、プロジェクト全体の設計指針をまとめます。以下のガイドラインに沿ってリソース管理を設計すると、メモリ効率とパフォーマンスのバランスが取れます。

方針具体策
ステージ単位で管理ステージ遷移時に不要なリソース参照を破棄
preloadは最小限に全シーンで使うものだけ。ステージ固有はload()
WeakRefでソフトキャッシュ再利用の可能性があるが常に保持は不要なリソースに
大きなリソースは非同期テクスチャアトラスや3Dモデルはload_threaded_request()

まとめ

  • preload() はスクリプトロード時に読み込み。小さく頻繁に使うリソースに最適
  • load() は実行時読み込み。動的パスや条件付きロードに対応
  • ResourceLoader.load_threaded_request() でバックグラウンド読み込みが可能。 use_sub_threads = true でさらに高速化
  • load_threaded_get_status() で進捗を監視し、ロード画面のプログレスバーを更新。4種類のステータスでエラーハンドリングも可能
  • Godot のリソースは 参照カウント方式 で管理される。 WeakRef を使えば参照カウントを増やさないソフトキャッシュが実装できる
  • ロード画面は Autoload として登録し、シーン遷移後も破棄されないようにする

さらに学ぶために