【Unity】ScenePreloader (シーン先読み) コンポーネントの作り方

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

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 フォルダとステージプレハブを用意する

  1. プロジェクト内に Resources フォルダを作成します。
    例: Assets/Resources/Stages
  2. 重いステージデータ(マップ、敵配置などをまとめたプレハブ)を作り、
    Stage01.prefab のような名前で Assets/Resources/Stages に保存します。
  3. この場合、resourcePath には Stages/Stage01 と指定します(拡張子は不要)。

手順②:シーン上に ScenePreloader を配置する

  1. 空の GameObject を作成し、名前を ScenePreloader にします。
  2. ScenePreloader スクリプトをアタッチします。
  3. Inspector で以下を設定します:
    • Resource Path : Stages/Stage01
    • Auto Preload On Start : タイトルシーンなら ON にしておくと便利です。
    • On Preload Completed : 読み込み完了時に UI を更新したい場合などはイベントを登録します。

手順③:プレイヤーやゲーム進行スクリプトから生成を指示する

たとえば「ゲームシーンに入ったら、次のステージをロードしておいて、
ボタンが押されたら実体化する」というような流れにしたい場合、
ゲーム進行用のコンポーネントから 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 更新専用コンポーネントを分けておくと、
それぞれが小さくまとまり、保守や拡張がかなり楽になります。
ぜひ、自分のプロジェクトでも「先読みの責務」を一つのコンポーネントに切り出してみてください。

Godot 4ゲーム制作 実践ドリル 100本ノック

新品価格
¥1,250から
(2025/12/13 21:27時点)

Godot4& GDScriptではじめる 2Dゲーム開発レシピ

新品価格
¥590から
(2025/12/13 21:46時点)

Unity 6 C#スクリプト 100本ノック

新品価格
¥1,230から
(2025/12/29 10:28時点)

Cocos Creator100本ノックTypeScriptで書く!

新品価格
¥1,250から
(2025/12/29 10:32時点)

URLをコピーしました!