Unityチュートリアル:DontDestroyOnLoadでプレイヤーをシーン間に保持する(責務分離対応)

対象者

  • Unity初心者〜中級者
  • プレイヤーを複数シーンで共通に使いたい方
  • スクリプトの責務分離や保守性にも関心のある方

プロジェクト構成例

Assets/
├── Prefabs/
│   ├── Player.prefab
│   └── Canvas.prefab(共通UI)
├── Scenes/
│   ├── TitleScene.unity
│   ├── StageSelectScene.unity
│   └── GameScene.unity
├── Scripts/
│   ├── PlayerController.cs         ← プレイヤーの永続化のみ
│   ├── PlayerSpawnHandler.cs       ← 出現位置移動の処理
│   ├── TitleSceneController.cs
│   ├── StageSelectController.cs
│   └── SceneNameDisplay.cs

シーン概要

シーン名内容
TitleSceneプレイヤーを初回生成、ステージ選択へ
StageSelectSceneステージ選択画面
GameSceneゲーム本編、出現地点にプレイヤー配置

1. プレイヤーの作成と分離管理

1-1. PlayerController.cs(永続化・Singleton)

using UnityEngine;

public class PlayerController : MonoBehaviour
{
    public static PlayerController Instance;

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

このスクリプトは 「プレイヤーを一度だけ生成し、以降のシーン間で維持」 する責任のみを持ちます。


1-2. PlayerSpawnHandler.cs(出現位置制御)

using UnityEngine;
using UnityEngine.SceneManagement;

public class PlayerSpawnHandler : MonoBehaviour
{
    void OnEnable()
    {
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    void OnDisable()
    {
        SceneManager.sceneLoaded -= OnSceneLoaded;
    }

    void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        if (PlayerController.Instance == null) return;

        var spawn = GameObject.FindWithTag("SpawnPoint");
        if (spawn != null)
        {
            PlayerController.Instance.transform.position = spawn.transform.position;
        }
    }
}

GameManager や空オブジェクトにアタッチするだけで機能します。

Player に直接アタッチしても問題ありません(初学者向けにはこの方が扱いやすい)。


2. プレイヤープレハブの作成

  1. Hierarchy に Player を作成(3Dなら Capsule、2Dなら Sprite)
  2. Tag を “Player" に設定
  3. Rigidbody, Collider などを必要に応じて追加
  4. PlayerController.cs をアタッチ
  5. Prefab化して Assets/Prefabs/Player.prefab に保存

3. 出現地点(SpawnPoint)の配置

  • 各ゲーム用シーン(例:GameScene)に Empty オブジェクトを配置
  • 名前:任意(例:SpawnPoint)
  • Tag:SpawnPoint

4. TitleSceneの設定

ボタンのUI設定(例:Show Stage Select)

  • Canvas上に Button を配置し、Textは Show Stage Select
  • OnClick() に以下の関数を追加:
TitleSceneController.OnStartGame()

スクリプト:TitleSceneController.cs

using UnityEngine;
using UnityEngine.SceneManagement;

public class TitleSceneController : MonoBehaviour
{
    public GameObject playerPrefab;

    public void OnStartGame()
    {
        if (PlayerController.Instance == null)
        {
            Instantiate(playerPrefab);
        }

        SceneManager.LoadScene("StageSelectScene");
    }
}

インスペクタから playerPrefab にプレイヤーのプレハブを割り当てます。

Unityのコード上でButtonのOnClickイベントを設定することも可能です。TitleSceneController で、Start()や Awake() のタイミングで Button コンポーネントを取得し、onClick にリスナーとしてメソッドを追加する形になります。

以下にその実装例を示します。


✅ TitleSceneController.cs(イベント登録をコードで設定)

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.SceneManagement;

public class TitleSceneController : MonoBehaviour
{
    public GameObject playerPrefab;
    public Button showStageSelectButton; // インスペクターでアサインする

    void Awake()
    {
        if (showStageSelectButton != null)
        {
            showStageSelectButton.onClick.AddListener(OnStartGame);
        }
        else
        {
            Debug.LogWarning("showStageSelectButton がアサインされていません。");
        }
    }

    public void OnStartGame()
    {
        if (PlayerController.Instance == null)
        {
            Instantiate(playerPrefab);
        }

        SceneManager.LoadScene("StageSelectScene");
    }
}

補足設定(Unityエディタ上)

  1. Canvas > Button を作成
  2. Textを “Show Stage Select” に変更
  3. 作成したボタンを TitleSceneController の showStageSelectButton にドラッグしてアサイン
  4. playerPrefab も忘れずにアサイン

備考

  • これにより、OnClick登録が完全にコードで行われるので、シーン内のUI設定ミスを防ぎやすくなります。
  • 複数のボタンを同様に管理する場合は、Dictionary<string, Button> などを使って柔軟な処理も可能です。

5. ステージ選択画面の設定

スクリプト:StageSelectController.cs

using UnityEngine;
using UnityEngine.SceneManagement;

public class StageSelectController : MonoBehaviour
{
    public void OnStageSelected(int stageNumber)
    {
        SceneManager.LoadScene("GameScene");
    }
}

ステージごとに異なるシーンをロードする構成への拡張も可能です。


6. シーン名表示(TextMeshPro対応)

スクリプト:SceneNameDisplay.cs

using UnityEngine;
using UnityEngine.SceneManagement;
using TMPro;

public class SceneNameDisplay : MonoBehaviour
{
    void Start()
    {
        if (TryGetComponent<TextMeshProUGUI>(out var text))
        {
            text.text = SceneManager.GetActiveScene().name;
        }
    }
}
  • Canvas に Text(TMP) を配置してアタッチするだけで、各シーンの名前が自動表示されます。

7. タイトルに戻る(オプション)

public void OnReturnToTitle()
{
    if (PlayerController.Instance != null)
    {
        Destroy(PlayerController.Instance.gameObject);
    }
    SceneManager.LoadScene("TitleScene");
}

プレイヤーを明示的に破棄してから再起動する設計です。


8. Build Settingsの確認

File > Build Settings にて次のシーンを登録:

  • Scenes/TitleScene
  • Scenes/StageSelectScene
  • Scenes/GameScene

順番は任意ですが、必ず登録が必要です。


チェックリスト

  • プレイヤーは TitleScene で1度だけ生成される
  • シーン間でプレイヤーが維持される(DontDestroyOnLoad)
  • 各シーンで SpawnPoint に移動できる
  • シーン名が UI に表示される
  • タイトルに戻ると再生成される

発展アイデア

内容概要
ステータス管理HPやアイテムなどをPlayerControllerに追加し、維持する
カメラ制御Cinemachine導入で追従・境界制御など
ステージ番号の受け渡しScriptableObjectや静的変数を使用
UIManagerの導入UI制御を一元管理して再利用性アップ

まとめ

このチュートリアルでは:

  • プレイヤーを DontDestroyOnLoad で維持
  • 出現地点をシーンごとに配置し、自動で移動
  • シーン名を自動表示
  • 責務分離で保守しやすいコード構造を実現

初心者でも取り組みやすく、今後のプロジェクト設計にも応用できる構成です。

通常この構成で問題ありません。SceneManager.sceneLoaded イベントは、シーンのロード完了後(≒すべてのGameObjectが生成された後)に呼ばれるため、GameObject.FindWithTag(“SpawnPoint") や PlayerController.Instance が有効になるタイミングとしては適切です。


安全な理由(仕組み)

  • SceneManager.sceneLoaded は 非同期ロードが完了した後に発火されるイベントです。
  • この時点で、HierarchyにあるGameObjectはすべて生成済みなので、
    • GameObject.FindWithTag(“SpawnPoint") は正しく取得可能
    • PlayerController.Instance が DontDestroyOnLoad オブジェクトなら正しく参照可能

注意点

ただし、以下のようなケースでは注意が必要です:

1. SpawnPointの生成が遅延している

  • スクリプトの Start() や Awake() 内で tag を設定している場合、FindWithTag に間に合わない可能性があります。
    • → 対処策:SpawnPoint オブジェクトは必ずシーン内でプレハブとして配置・タグ付けしておく

2. PlayerController.Instance の生成が遅い

  • TitleScene → StageSelectScene → GameScene と複数のシーンを挟んでいる場合、PlayerController がロード前に破棄されていると null になります。
    • → DontDestroyOnLoad とシングルトンのチェックが正しく機能しているか確認

改善例(遅延対策)

PlayerController.Instance が存在するまで少し待つ、という保険をかける場合は以下のようなCoroutineを使う方法もあります:

void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
    StartCoroutine(WaitAndWarp());
}

IEnumerator WaitAndWarp()
{
    yield return null; // 1フレーム待つ(すべての Start() 後)

    if (PlayerController.Instance == null) yield break;

    var spawn = GameObject.FindWithTag("SpawnPoint");
    if (spawn != null)
    {
        PlayerController.Instance.transform.position = spawn.transform.position;
    }
}

結論

  • 今のコードは 通常のUnityのシーン読み込みフローでは安全です。
  • ただし、生成タイミングのズレや複雑なシーン構成があるなら、保険として Coroutine を使った1フレーム待機がより堅牢になります。

Unity

Posted by hidepon