Unityを触り始めた頃によくあるパターンとして、Update() の中に「入力処理」「移動」「UI更新」「シーンロード」まで全部書いてしまう、というものがあります。
とくにシーン切り替えまわりでありがちなのが、
- ボタンが押されたら、その場で重いステージデータを読み込む
- 読み込みが終わるまでゲームが一瞬カクつく・フリーズする
- どのスクリプトがどのリソースを読み込んでいるか分からなくなる
これを放置していくと、「シーン管理用のGodクラス」が生まれ、
・どこに何が書いてあるか分からない
・ちょっとした仕様変更でも地雷原を踏む
という状態になりがちです。
そこでこの記事では、「シーンの重いステージデータを裏で先に読み込んでおく」という責務だけに絞った
ScenePreloader コンポーネントを用意して、コンポーネント単位でシンプルに管理する方法を紹介します。
内部では Unity の Resources フォルダと ResourceRequest を使って非同期ロードを行い、
「次に行く予定のステージ」を事前に読み込んでおくイメージですね。
【Unity】次のステージをこっそり準備!「ScenePreloader」コンポーネント
ここでは、
- 指定したパスのプレハブ(重いステージデータなど)を非同期で読み込み
- 読み込み進捗(0〜1)を取得可能
- 読み込み完了したら
Instantiateして実際にシーンへ配置 - 「いつ読み込みを開始するか」「いつ実体化するか」を外部から制御
といった機能を持つ ScenePreloader を作ります。
ResourceLoader 的な役割を中に閉じ込めておくことで、
他のコンポーネントは「先読み開始」「生成して」の2つだけ意識すればよくなります。
フルコード
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.Events;
/// <summary>
/// 重いステージプレハブを裏で非同期ロードしておき、
/// 必要なタイミングで実体化するためのコンポーネント。
///
/// - Resources フォルダ内のプレハブを対象とする
/// - ScenePreloader 自身は MonoBehaviour としてシーン上に1つ置いておく想定
/// - 「いつロードを開始するか」「いつ生成するか」は外部から呼び出して制御する
/// </summary>
public class ScenePreloader : MonoBehaviour
{
[Header("ロード対象の設定")]
[SerializeField]
[Tooltip("Resources フォルダからの相対パス (例: \"Stages/Stage01\")。拡張子は不要。")]
private string resourcePath;
[Header("自動開始オプション")]
[SerializeField]
[Tooltip("シーン開始時 (Start) に自動で先読みを開始するかどうか")]
private bool autoPreloadOnStart = true;
[Header("ロード完了イベント")]
[SerializeField]
[Tooltip("リソースの非同期ロードが完了したときに呼ばれるイベント")]
private UnityEvent onPreloadCompleted;
// 内部状態
private ResourceRequest _request; // 非同期ロード用
private UnityEngine.Object _loadedAsset; // ロード済みアセット (プレハブ想定)
private bool _isPreloading; // ロード中フラグ
private bool _isCompleted; // ロード完了フラグ
private bool _hasInstantiate; // Instantiate 済みかどうか
/// <summary>
/// ロード進捗 (0〜1)。ロードしていない場合は 0。
/// </summary>
public float Progress
{
get
{
if (_request == null) return 0f;
return _request.progress;
}
}
/// <summary>
/// 現在ロード中かどうか。
/// </summary>
public bool IsPreloading => _isPreloading;
/// <summary>
/// ロードが完了しているかどうか。
/// </summary>
public bool IsCompleted => _isCompleted;
/// <summary>
/// すでに Instantiate 済みかどうか。
/// </summary>
public bool HasInstantiate => _hasInstantiate;
private void Start()
{
// シーン開始時に自動でロードを始めるオプション
if (autoPreloadOnStart)
{
StartPreload();
}
}
/// <summary>
/// 先読み処理を開始する。
/// すでに開始済み、または完了済みの場合は何もしない。
/// </summary>
public void StartPreload()
{
if (_isPreloading || _isCompleted)
{
// すでにロード中、もしくは完了している場合は重複して開始しない
return;
}
if (string.IsNullOrEmpty(resourcePath))
{
Debug.LogError($"[ScenePreloader] resourcePath が設定されていません。GameObject: {name}");
return;
}
// コルーチンで非同期ロード開始
StartCoroutine(PreloadRoutine());
}
/// <summary>
/// ロード済みのプレハブを Instantiate して返す。
/// - ロードが完了していない場合は null を返し、エラーを出す。
/// - 1回しか Instantiate したくない場合は HasInstantiate を外部で見て制御するか、
/// このメソッドの引数 allowMultipleInstantiate を false に設定してください。
/// </summary>
/// <param name="position">生成位置</param>
/// <param name="rotation">生成回転</param>
/// <param name="parent">親 Transform (任意)</param>
/// <param name="allowMultipleInstantiate">複数回 Instantiate を許可するか</param>
public GameObject InstantiateLoaded(Vector3 position, Quaternion rotation, Transform parent = null, bool allowMultipleInstantiate = true)
{
if (!_isCompleted || _loadedAsset == null)
{
Debug.LogError("[ScenePreloader] まだロードが完了していないため Instantiate できません。");
return null;
}
if (!allowMultipleInstantiate && _hasInstantiate)
{
Debug.LogWarning("[ScenePreloader] すでに Instantiate 済みのため、新たに生成しません。");
return null;
}
var prefab = _loadedAsset as GameObject;
if (prefab == null)
{
Debug.LogError("[ScenePreloader] ロードされたアセットが GameObject プレハブではありません。");
return null;
}
var instance = Instantiate(prefab, position, rotation, parent);
_hasInstantiate = true;
return instance;
}
/// <summary>
/// ロード済みアセットを解放する。
/// - 生成済みインスタンスは破棄されないので、必要に応じて外部で Destroy してください。
/// - 再度 StartPreload を呼ぶことで、もう一度ロードし直すことができます。
/// </summary>
public void Unload()
{
if (_loadedAsset != null)
{
// Resources 経由でロードしたアセットを解放
Resources.UnloadAsset(_loadedAsset);
}
_request = null;
_loadedAsset = null;
_isPreloading = false;
_isCompleted = false;
_hasInstantiate = false;
}
/// <summary>
/// 実際の非同期ロード処理を行うコルーチン。
/// </summary>
private IEnumerator PreloadRoutine()
{
_isPreloading = true;
_isCompleted = false;
// Resources.LoadAsync で非同期ロード開始
_request = Resources.LoadAsync(resourcePath);
if (_request == null)
{
Debug.LogError($"[ScenePreloader] Resources.LoadAsync に失敗しました。パスを確認してください: {resourcePath}");
_isPreloading = false;
yield break;
}
// ロード完了まで待つ
yield return _request;
_isPreloading = false;
if (_request.asset == null)
{
Debug.LogError($"[ScenePreloader] アセットのロードに失敗しました。パスを確認してください: {resourcePath}");
yield break;
}
_loadedAsset = _request.asset;
_isCompleted = true;
// 完了イベントを発火
onPreloadCompleted?.Invoke();
Debug.Log($"[ScenePreloader] ロード完了: {resourcePath}");
}
/// <summary>
/// デバッグ用に、エディタ上から進捗を確認しやすくするためのログ出力。
/// 実運用で不要ならコメントアウトしてもOKです。
/// </summary>
private void Update()
{
// 進捗を確認したいときだけログを出す例
// ※毎フレームログを出すと重いので、開発中だけ有効にするなど工夫しましょう。
/*
if (_isPreloading)
{
Debug.Log($"[ScenePreloader] Loading... {Progress * 100f:0}%");
}
*/
}
}
使い方の手順
ここでは具体例として、
- タイトルシーンで「次のゲームステージ」を先読みしておく
- ゲームシーンで「次のフロア(重いマッププレハブ)」を先読みしておく
といったケースを想定して使い方を見ていきます。
手順①:Resources フォルダとステージプレハブを用意する
- プロジェクト内に
Resourcesフォルダを作成します。
例:Assets/Resources/Stages - 重いステージデータ(マップ、敵配置などをまとめたプレハブ)を作り、
Stage01.prefabのような名前でAssets/Resources/Stagesに保存します。 - この場合、
resourcePathにはStages/Stage01と指定します(拡張子は不要)。
手順②:シーン上に ScenePreloader を配置する
- 空の GameObject を作成し、名前を
ScenePreloaderにします。 ScenePreloaderスクリプトをアタッチします。- Inspector で以下を設定します:
- Resource Path :
Stages/Stage01 - Auto Preload On Start : タイトルシーンなら
ONにしておくと便利です。 - On Preload Completed : 読み込み完了時に UI を更新したい場合などはイベントを登録します。
- Resource Path :
手順③:プレイヤーやゲーム進行スクリプトから生成を指示する
たとえば「ゲームシーンに入ったら、次のステージをロードしておいて、
ボタンが押されたら実体化する」というような流れにしたい場合、
ゲーム進行用のコンポーネントから ScenePreloader を参照して使います。
using UnityEngine;
using UnityEngine.InputSystem; // 新InputSystemを使う場合
/// <summary>
/// プレイヤー入力に応じて、先読み済みのステージを生成する例。
/// </summary>
public class StageSpawnController : MonoBehaviour
{
[SerializeField]
private ScenePreloader scenePreloader;
[SerializeField]
private Transform spawnPoint; // ステージを出したい位置 (例: 空のGameObject)
private void Start()
{
// 開始時に自動で先読みしたくない場合は、ここで明示的に開始してもOK
if (!scenePreloader.IsPreloading && !scenePreloader.IsCompleted)
{
scenePreloader.StartPreload();
}
}
// 新InputSystemのアクションから呼ばれる想定
public void OnSpawnStage(InputAction.CallbackContext context)
{
if (!context.performed) return;
if (!scenePreloader.IsCompleted)
{
Debug.Log("まだステージのロードが終わっていません。");
return;
}
// 先読み済みのプレハブを指定位置に生成
scenePreloader.InstantiateLoaded(
spawnPoint.position,
spawnPoint.rotation,
parent: null,
allowMultipleInstantiate: false // 1回だけ生成したい場合
);
}
}
これで、ScenePreloader は「ロード専用」、
StageSpawnController は「いつ生成するかの制御専用」という分担になり、
それぞれのコンポーネントがシンプルに保たれます。
手順④:動く床や次フロアの先読みなど、他の用途にも使う
もう一つ具体例を挙げると、「ローグライク風のダンジョン」で、
- 現在のフロアをプレイしている間に、次のフロアプレハブを裏でロードしておく
- 出口の階段に到達した瞬間に、ロード済みのフロアを
Instantiateしてカメラを切り替える
といった使い方もできます。
using UnityEngine;
/// <summary>
/// ダンジョンの次フロアを先読みしておき、
/// プレイヤーが出口トリガーに入ったら生成する例。
/// </summary>
[RequireComponent(typeof(Collider))]
public class NextFloorTrigger : MonoBehaviour
{
[SerializeField]
private ScenePreloader nextFloorPreloader;
[SerializeField]
private Transform nextFloorSpawnPoint;
private void Start()
{
// 現在のフロア開始と同時に、次フロアの先読みを始める
nextFloorPreloader.StartPreload();
}
private void OnTriggerEnter(Collider other)
{
if (!other.CompareTag("Player")) return;
if (!nextFloorPreloader.IsCompleted)
{
Debug.Log("次のフロアがまだロード中です。少し待ってください。");
return;
}
// 次フロアを生成
nextFloorPreloader.InstantiateLoaded(
nextFloorSpawnPoint.position,
nextFloorSpawnPoint.rotation,
parent: null,
allowMultipleInstantiate: false
);
// ここでカメラの切り替えや、現在フロアの破棄などを行うとよいです
}
}
メリットと応用
ScenePreloader のように、「先読みだけを担当するコンポーネント」を用意しておくと、
- プレハブ管理がシンプルになる
どのステージプレハブをどこでロードしているかが明確になり、
「タイトルシーンのこのオブジェクトが Stage01 をロードしている」と一目で分かります。 - レベルデザイン時の負荷対策がしやすい
どのタイミングで先読みを開始するかを変えるだけで、
ロード時のカクつきを減らしたり、演出に合わせてロードを仕込んだりできます。 - 責務が分かれているので差し替えやすい
将来Addressablesや独自のロードシステムに切り替えたくなっても、
「ロードを担当するコンポーネント」一箇所を差し替えるだけで済む設計にしやすいです。 - テストやデバッグが楽
シーン上でScenePreloaderだけを置いて、
ロードパスと進捗ログを確認する、といった検証が簡単にできます。
応用としては、
- 複数の
ScenePreloaderをシーン上に置いて、
「次のステージ」「ボス用のステージ」「カットシーン用ステージ」などを並列に先読みする - UI のローディングバーに
Progressをバインドして、
「裏でロードしている感」をユーザーに見せる - ロード完了イベント
onPreloadCompletedを使って、
「ボタンを有効化」「フェード演出を開始」などを自動化する
など、レベルデザインと演出をつなぐハブとしても活躍してくれます。
改造案:進捗に応じてローディングバーを更新する
最後に、ScenePreloader の進捗を UI のスライダーに反映する、
簡単な補助コンポーネントの例を載せておきます。
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// ScenePreloader の進捗を Slider に表示するシンプルな例。
/// </summary>
public class PreloadProgressBar : MonoBehaviour
{
[SerializeField]
private ScenePreloader scenePreloader;
[SerializeField]
private Slider progressSlider;
private void Update()
{
if (scenePreloader == null || progressSlider == null) return;
// ロード中または完了時の進捗をそのままスライダーに反映
progressSlider.value = scenePreloader.Progress;
// ロードが完了したらスライダーを自動で非表示にする例
if (scenePreloader.IsCompleted && progressSlider.gameObject.activeSelf)
{
progressSlider.gameObject.SetActive(false);
}
}
}
このように、ロード専用コンポーネントとUI 更新専用コンポーネントを分けておくと、
それぞれが小さくまとまり、保守や拡張がかなり楽になります。
ぜひ、自分のプロジェクトでも「先読みの責務」を一つのコンポーネントに切り出してみてください。




