「シーンを切り替えた瞬間だけカクッと固まる」「大きなステージに入るときにロード画面を出したい」——ゲームが育ってくると、必ずリソースの 読み込み方 が課題になります。実はGodotでは、リソースを いつ・どうやって読むか を選ぶだけで、体感の快適さが大きく変わります。
考え方はシンプルで、「すぐ使う小さいものは先に読んでおく」「大きいものは必要になってから読む」「巨大なものはバックグラウンドで読みながらロード画面を出す」の3段階です。この記事では、preload() と load() の使い分けから、ResourceLoader による 非同期ロード 、進捗バー付き ロード画面 の実装、そして WeakRef を使った メモリ管理 までを解説します。
この記事でわかること
preload()とload()の違いと 使い分けResourceLoaderによる 非同期バックグラウンド読み込み の3ステップ- 進捗バー付 きロード画面 の作り方(
change_scene_to_packed)- 参照カウントと
WeakRefによるメモリ管理
動作確認環境: Godot 4.7
preload()とload()の使い分け
Godotのリソース読み込みには2つの基本関数があります。 いつ読むか と パスを変数にできるか が違うので、用途で使い分けます。

preload() ——先に 読んでおく
弾やSEなど、毎フレームのように使う小さなリソースには preload() が最適です。スクリプトが パースされる時点 でリソースも読み込まれるので、実行時に呼んだ瞬間の遅延がゼロになります。
const BULLET_SCENE := preload("res://scenes/bullet.tscn") # パスは定数リテラルのみ
const HIT_SOUND := preload("res://audio/hit.wav")
func shoot() -> void:
add_child(BULLET_SCENE.instantiate()) # 遅延なく即座に使える
- パスはリテラル文字列のみ (変数は不可)
- スクリプトのパース時にまとめて読み込まれる
- 小さい・頻繁に使うアセットに向く
load() ——必要になってから読む
プレイヤーが選んだ武器や、難易度で変わる敵など、 条件で変わる リソースには load() を使います。パスを 変数で組み立てられる のが最大の強みです。
func load_weapon(weapon_name: String) -> Node:
var path := "res://weapons/%s.tscn" % weapon_name # パスを動的に組み立て
return load(path).instantiate()
- パスを動的に組み立てられる
- 初回だけディスクから読み込み(以降はキャッシュから即座に返る)
- 大きい・条件付きのリソースに向く
tips:
preload()を大量に使うと、シーンの 初期ロードが遅く なります。特に大きなテクスチャや3Dモデルのpreload()は影響が大きいので、サイズの大きいものはload()や次のバックグラウンド読み込みに切り替えましょう。 数より合計サイズ がポイントです。
ResourceLoaderによる非同期ロード
大きなシーンを load() すると、読み込みが終わるまで メインスレッドがブロック され、ゲームが固まります。RPGのダンジョン遷移、オープンワールドのチャンク読み込みなど、重いリソースを扱う場面では ResourceLoader の 非同期API が効きます。バックグラウンドで読みながら、ゲームやアニメーションを動かし続けられます。

非同期ロードは 「依頼 → 進捗監視 → 取得」 の3ステップです。
# 1. 別スレッドで読み込みを依頼
ResourceLoader.load_threaded_request("res://levels/stage_2.tscn")
func _process(_delta: float) -> void:
var progress := [] # 進捗を受け取る空配列(後述)
var status := ResourceLoader.load_threaded_get_status(
"res://levels/stage_2.tscn", progress)
match status:
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
print("Loading: %d%%" % int(progress[0] * 100)) # progress[0]に0.0〜1.0
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("読み込みに失敗しました")
ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
printerr("パスが無効、または未リクエストです")
3つのAPIとステータスの役割はこうです。
| メソッド/ステータス | 役割 |
|---|---|
load_threaded_request(path) | 非同期ロードを開始 |
load_threaded_get_status(path, progress) | 進捗(0.0〜1.0)を取得 |
load_threaded_get(path) | 完了後にリソースを取得 |
THREAD_LOAD_IN_PROGRESS / LOADED / FAILED / INVALID_RESOURCE | 4種の状態 |
tips:
progressを 空配列で渡す のは「参照渡しで値を返す」Godotの規約です。GDScriptの関数は複数の値を直接返せないため、空配列を渡してprogress[0]に進捗を格納してもらいます。また、第3引数use_sub_threads = trueにすると、テクスチャやメッシュなどの サブリソースも並列 で読み込まれ、速くなることがあります(既定はfalse。いつも速くなるとは限りません)。
実践:進捗バー付きロード画面を組む
非同期ロードを覚えたら、それをプレイヤーに見せる ロード画面 を作りましょう。RPGのダンジョン遷移、ステージ制アクションの面切 り替え、オープンワールドのエリア移動——「重いシーンへ移る間、進捗バーで待たせる」場面はどのジャンルにもあります。

ロード画面は CanvasLayer で作り、_process で進捗を監視してバーを更新します。完了したら change_scene_to_packed() でシーンを切り替えます。
# loading_screen.gd(CanvasLayer の子に ProgressBar と Label を置いて loading_screen.tscn として保存し、その .tscn を Autoload に登録)
extends CanvasLayer
@onready var progress_bar: ProgressBar = $ProgressBar
@onready var label: Label = $Label
var _target_path := ""
func _ready() -> void:
hide() # 起動直後は隠しておく
func load_scene(scene_path: String) -> void:
_target_path = scene_path
show()
var err := ResourceLoader.load_threaded_request(scene_path) # 読み込み開始
if err != OK:
printerr("ロード開始に失敗: %s" % scene_path)
return
set_process(true)
func _process(_delta: float) -> void:
if _target_path.is_empty():
return
var progress := []
match ResourceLoader.load_threaded_get_status(_target_path, progress):
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
progress_bar.value = progress[0] * 100 # バーを進捗に合わせる
label.text = "Loading... %d%%" % int(progress[0] * 100)
ResourceLoader.THREAD_LOAD_LOADED:
var scene := ResourceLoader.load_threaded_get(_target_path)
get_tree().change_scene_to_packed(scene) # 完成したシーンへ切り替え
_target_path = ""
set_process(false)
hide()
ResourceLoader.THREAD_LOAD_FAILED:
label.text = "読み込みに失敗しました"
set_process(false)
呼び出しは1行です。
LoadingScreen.load_scene("res://levels/stage_2.tscn")
ポイントは2つです。
- ロード画面は Autoload に登録する :
change_scene_to_packed()は今のシーン(current_scene)を丸ごと置き換えます。ロード画面が今のシーンの中にあると 自分ごと消えてしまう ため、Autoloadで常駐させてシーン遷移をまたいで生き残らせます。 - 進捗は
progress[0]から :load_threaded_get_status()が空配列に入れてくれる0.0〜1.0を、そのままProgressBar.valueに反映します。プレイヤーに「あとどれくらいか」を伝えられます。
メモリ管理とWeakRefキャッシュ
「どう読むか」だけでなく「どう手放すか」も同じくらい大切です。大きなゲームでは、使わないリソースを解放しないとメモリが不足します。
まず、Godotは load() したリソースを パスベースでキャッシュ します。同じパスを再度 load() しても、ディスクではなくメモリのキャッシュが返ります(同一インスタンス)。
Godotのリソースは 参照カウント方式 です。参照している変数の数を内部で数え、 0になった瞬間に解放 します(JavaやC#のトレーシング型GCとは別物)。通常の変数で持つと参照が残り解放されませんが、WeakRef は 参照カウントを増やしません 。

これを使うと、「保持したいけど、他が使わなくなったら手放してよい」 ソフトキャッシュ が作れます。
var _cache: Dictionary = {}
func get_resource(path: String) -> Resource:
if _cache.has(path):
var res: Resource = _cache[path].get_ref() # 弱参照から取り出す
if res:
return res # まだ生きていれば再利用
var loaded := load(path)
_cache[path] = weakref(loaded) # 参照カウントを増やさず記録
return loaded
get_ref() が null なら、そのリソースはすでに解放済みなので load() で読み直します。アクセス頻度が高いものは通常の変数で持つ方が効率的で、WeakRefは「使うかもしれないが、メモリ節約を優先したい」リソース向きです。
設計の指針をまとめると、こうなります。
