概要
動作確認環境: 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_threads を true にすると、サブリソース(テクスチャ、メッシュなど)も並列でロードされます。大量のサブリソースを含むシーンでは、ロード時間が大幅に短縮されることがあります。
# サブリソースも並列ロード(デフォルトは 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 キャッシュは「使うかもしれないが、メモリ節約を優先したい」リソースに最適です。
メモリ管理のベストプラクティス
個々のテクニックを把握したところで、プロジェクト全体の設計指針をまとめます。以下のガイドラインに沿ってリソース管理を設計すると、メモリ効率とパフォーマンスのバランスが取れます。
| 方針 |
|---|