【Unity】Unityで学ぶSingletonパターンの正しい実装と、知っておくべき注意点

作成: 2025-12-10最終更新: 2026-07-13

UnityでのSingletonパターンの正しい実装方法と、使用時の注意点を解説します。汎用MonoSingleton基底クラスの作り方、実行順序や重複生成の落とし穴、乱用を避ける判断基準まで紹介。

スコアの管理、BGMの再生、ゲームの進行状態——「どこからでもアクセスできる、ただ一つの管理役」が欲しい場面は、ゲーム開発で必ずやってきます。毎回Find系で探すのは重くて面倒、Inspectorでの手動設定はシーンをまたげない。そこで登場する定番が Singleton(シングルトン)パターン です。

ただしSingletonは、Unityのライフサイクルが絡むと独特の落とし穴があり、「たまにnullになる」「シーンを行き来したら2体になった」といったバグの温床にもなります。この記事では、汎用的なMonoSingleton<T>基底クラスによる安全な実装と、3つの落とし穴、そして「使いすぎない」ための判断基準を解説します。

Singletonのイメージ。多くのオブジェクトが、王冠を持つただ1体の管理役にアクセスしている

この記事でわかること

  • Singletonが解決する問題と、ゲーム開発での使いどころ
  • 汎用的なMonoSingleton<T>基底クラスの実装(コピペで使える)
  • 3つの落とし穴(重複生成・実行順序・破棄後の参照)と対策
  • 乱用を避ける判断基準と、Singletonに代わる選択肢

Sponsored

Singletonはどこで使う?

先に使いどころのイメージを掴んでください。Singletonが向いているのは「ゲーム全体でただ1つ」かつ「あちこちから呼ばれる」役割です。

Singletonの使いどころの実例図。サウンド管理・スコア管理・ゲーム進行管理という、ゲーム全体でただ1つの管理役3種
  • サウンドマネージャー: 効果音・BGMの再生窓口。敵もUIもアイテムも、全員がここに再生を頼む
  • スコア/セーブデータの管理: シーンをまたいで生き続ける数値の置き場所
  • ゲーム進行の管理: タイトル・プレイ中・ポーズなどの状態を握る司令塔(GameManagerパターンの記事 で詳しく扱います)

逆に、敵キャラクターや弾のように複数存在するもの、特定のシーンにしか登場しないものはSingletonにしてはいけません。この線引きが崩れたときの問題は後半で説明します。

Singletonパターンの基本概念

Singletonパターンは、クラスのインスタンスがアプリケーション全体でただ一つであることを保証し、そのインスタンスへのグローバルなアクセスポイントを提供する仕組みです。

Singletonの概念図。唯一のインスタンスが受付窓口となり、プレイヤー・敵・UIなどどこからでもInstance経由でアクセスできる

通常のC#クラスでこれを実現するには、主に以下の要素が必要です。

  1. 静的なインスタンス変数: クラス自身を保持するためのstaticな変数。
  2. プライベートなコンストラクタ: 外部からのnewによるインスタンス生成を防ぐため。
  3. 静的なアクセスプロパティ: 唯一のインスタンスを返すためのstaticなプロパティ。

UnityにおけるMonoBehaviour Singletonの実装

UnityでSingletonパターンを実装する場合、多くの管理クラスはMonoBehaviourを継承する必要があります。これは、AwakeUpdateといったUnityのライフサイクルメソッドを利用したり、Inspectorから設定を行ったりするためです。

ここでは、ジェネリックGeneric)を用いて、どんなクラスでもSingleton化できる汎用的な基底クラスを作成する方法を紹介します。この方法が、UnityでのSingleton実装のベストプラクティスの一つとされています。

汎用的な MonoSingleton<T> クラス

以下のコードをMonoSingleton.csとして保存してください。

using UnityEngine;

// TはMonoBehaviourを継承したクラスである必要があるという制約
public abstract class MonoSingleton<T> : MonoBehaviour where T : MonoSingleton<T>
{
    // 唯一のインスタンスを保持する静的変数
    private static T instance;

    // 外部からアクセスするための静的プロパティ
    public static T Instance
    {
        get
        {
            // インスタンスがまだ存在しない場合
            if (instance == null)
            {
                // シーン内からT型のオブジェクトを探す
                instance = FindFirstObjectByType<T>();

                // それでも見つからない場合は、新しいGameObjectを作成してアタッチする
                if (instance == null)
                {
                    GameObject singletonObject = new GameObject();
                    instance = singletonObject.AddComponent<T>();
                    singletonObject.name = typeof(T).ToString() + " (Singleton)";
                }
            }
            return instance;
        }
    }

    // インスタンスが生成された直後に呼ばれる
    protected virtual void Awake()
    {
        // 既にインスタンスが存在する場合(重複生成の防止)
        if (instance != null && instance != this)
        {
            // 自身を破棄して、重複を防ぐ
            Debug.LogWarning($"[Singleton] 既にインスタンスが存在するため、{typeof(T).Name}を破棄します。");
            Destroy(gameObject);
            return;
        }

        // 自身を唯一のインスタンスとして設定
        instance = (T)this;

        // シーンを跨いでオブジェクトを永続化させる
        // ただし、エディタ上での挙動に注意が必要なため、実行時のみ適用
        if (Application.isPlaying)
        {
            DontDestroyOnLoad(gameObject);
        }

        // 初期化処理を子クラスに任せるためのメソッド
        OnInitialize();
    }

    // 破棄時に静的参照をクリアする
    protected virtual void OnDestroy()
    {
        if (instance == this)
        {
            instance = null;
        }
    }

    // 子クラスで初期化処理をオーバーライドするためのメソッド
    protected virtual void OnInitialize() { }
}

補足: シーン検索にはFindFirstObjectByType<T>()を使っています。古い解説にあるFindObjectOfTypeはUnity 6(2023.1以降)で非推奨になった旧APIです。読み替えの詳細は 新旧API読み替えガイド を参照してください。

使い方(例:サウンドマネージャー)

次に、この基底クラスを継承して、実際の管理クラスを作成します。

using UnityEngine;

// MonoSingleton<SoundManager>を継承する
public class SoundManager : MonoSingleton<SoundManager>
{
    // 外部からアクセスするための公開メソッド
    public void PlayBGM(AudioClip clip)
    {
        Debug.Log($"BGMを再生しました: {clip.name}");
        // 実際のBGM再生処理...
    }

    public void PlaySE(AudioClip clip)
    {
        Debug.Log($"効果音を再生しました: {clip.name}");
        // 実際のSE再生処理...
    }

    // 初期化処理をオーバーライド
    protected override void OnInitialize()
    {
        // サウンド設定のロードなど、初期化時に一度だけ実行したい処理を記述
        Debug.Log("SoundManagerの初期化が完了しました。");
    }
}

アクセス方法

これで、ゲーム内のどこからでも、シーンを気にすることなく簡単にアクセスできます。

// どこかの別のスクリプトから
public class PlayerController : MonoBehaviour
{
    public AudioClip jumpSound;

    void Jump()
    {
        // 唯一のインスタンスにアクセスし、メソッドを呼び出す
        SoundManager.Instance.PlaySE(jumpSound);
    }
}

ポイント: MonoSingleton<T>クラスのInstanceプロパティは、インスタンスが存在しない場合に自動的にシーン内を検索し、それでも見つからなければ新しいGameObjectを作成してアタッチします。これにより、手動でシーンに配置し忘れてもエラーにならず、安全性が高まります。

Sponsored

初心者が陥りやすい3つの落とし穴と対策

Singletonパターンは便利ですが、Unity特有のライフサイクルが絡むため、初心者が陥りやすい問題がいくつかあります。

Singletonの3つの落とし穴の図。シーン遷移での重複生成、実行順序によるnullアクセス、破棄後に残る静的参照

1. シーン遷移時の重複インスタンス生成

最もよくある問題は、DontDestroyOnLoadを使っているSingletonクラスが、シーンを移動した際に重複して生成されてしまうことです。

  • 間違いの例: シーンAでSoundManagerが生成され、DontDestroyOnLoadで永続化されます。次にシーンBをロードした際、シーンBにもSoundManagerのプレハブが配置されていると、2つ目のインスタンスが生成されてしまいます。
  • 対策: 上記のMonoSingleton<T>の実装にあるように、Awakeメソッド内で既にinstanceが存在するかどうかをチェックし、存在する場合は Destroy(gameObject);で自身を即座に破棄 します。この「門番チェック」の仕組みは DontDestroyOnLoadの記事 で図解しています。

2. 参照順序の問題(Script Execution Order)

Unityでは、スクリプトの実行順序(AwakeStartが呼ばれる順番)は基本的に不定です。あるスクリプトのAwakeでSingletonのInstanceにアクセスしようとしたとき、まだそのSingletonのAwakeが実行されておらず、instancenullである可能性があります。「昨日まで動いていたのに、今日はnullエラーが出る」という不安定なバグの正体は、たいていこれです。

  • 対策:
    1. Instanceプロパティ内でインスタンスを生成するロジック(上記のコード)を採用することで、アクセスされた時点で必ずインスタンスが存在するようにします。
    2. Script Execution Order設定Edit -> Project Settings -> Script Execution Order)で、Singletonクラスを他のスクリプトよりも早く実行されるように設定します。これにより、他のスクリプトがアクセスする前に初期化が完了することを保証できます。

3. OnDestroyによる静的参照のクリア忘れ

シーン遷移時やゲーム終了時に、Singletonオブジェクトが破棄されることがあります。このとき、staticinstance変数をクリアし忘れると、既に破棄されたオブジェクトへの参照が残り続け、次にアクセスした際に予期せぬエラー(MissingReferenceExceptionなど)を引き起こす可能性があります。

  • 対策: MonoSingleton<T>クラスにOnDestroyメソッドを実装し、オブジェクトが破棄される際にinstance = null;として静的参照をクリアします。
    // 破棄時に静的参照をクリアする
    protected virtual void OnDestroy()
    {
        if (instance == this)
        {
            instance = null;
        }
    }
Sponsored

注意点:乱用すると密結合の温床になる

Singletonパターンは非常に強力ですが、「どこからでもアクセスできる」という長所は、そのまま短所にもなります。

Singleton乱用の図。少数の管理役に留めればすっきりするが、何でもSingleton化すると全クラスが相互依存して絡み合う
  1. 密結合(強い依存性): Singletonはグローバルなアクセスポイントを提供するため、どのクラスからでも簡単にアクセスできてしまいます。その結果、多くのクラスが特定のSingletonクラスに依存するようになり、コード間の結合度(密結合)が高まります。密結合は、コードの一部を変更した際に、予期せぬ場所でバグが発生するリスクを高めます。

  2. テストの困難さ: ユニットテストを行う際、Singletonクラスはグローバルな状態を持つため、テスト間で状態が共有されてしまい、テストの独立性を保つのが難しくなります。また、Singletonクラスをモック(偽のオブジェクト)に置き換えることが難しく、テストの柔軟性が損なわれます。

補足(大規模開発との違い): 大規模開発では、これらのデメリットを避けるために DI(Dependency Injection:依存性の注入) と呼ばれる手法がSingletonの代替として採用されることが多くあります。しかし、個人や小規模なチームでの開発においては、Singletonのシンプルさと利便性が上回る場合も多いため、使用する範囲を限定することが重要です。目安は「サウンド・進行管理・セーブなど、本当にゲームに1つの役割だけ」です。

実践:BGMサービスを1つだけシーン間で共有する

仕上げに、ここまでの部品を「1つのゲーム」に組み込んでみましょう。タイトル→ゲーム→リザルトの3シーン構成で、BGM・効果音の窓口はSoundManagerただ1つ。敵が10体いても、敵自身は絶対にSingletonにしない——この 線引きごと 体験するのが目的です。

Singletonにする役としない役の線引きの図。ゲームに1つだけのSoundManagerへプレイヤー・UI・敵から矢印が集まる一方、複数存在する敵のHPはSingletonにしない

組み立て手順:

  1. タイトルシーンにSoundManagerを配置します。Instance内の自動生成があるので置き忘れても動きますが、AudioClipなどInspector設定を持つマネージャーは、起動シーンに「本物」を置いておくのが確実です
  2. プレイヤーのジャンプ、UIの決定音、敵の被弾音は、どこからでもSoundManager.Instance.PlaySE(...)で鳴らします
  3. シーンを3往復して、再生中Hierarchyの「DontDestroyOnLoad」にSoundManager1つだけ いることを確認します

そして、同じゲームの中でSingletonにする役・しない役を分けます。

役割Singletonにする?理由
SoundManagerゲームに1つ・シーンを跨ぐ・全員が呼ぶ
敵のHealth・頭上HPバー複数存在する。「個体の話」は instanceイベント
ポーズメニューのUIそのシーン限り。Inspectorの普通の参照で足りる

「どこから音が鳴ったか探せない」症状

呼び出し側がSoundManager.Instance.PlaySE(...)だらけになると、密結合の痛みが具体的な症状で現れます。「なぜか同じSEが二重に鳴る」——さて、誰が鳴らしたのでしょう? Instanceは誰でも押せる公衆のボタンなので、 呼び出し元が匿名 になり、調査はプロジェクト全文検索頼みになります。

開発中の実用的な対策は2つです。PlaySEの中にDebug.Log(clip.name)を1行仕込めば、Consoleでログをクリックしたとき スタックトレースに呼び出し元が表示 されます。もう1つは、鳴らす場所を増やしすぎないこと——「被弾音は敵のHealthイベントを購読した1箇所だけが鳴らす」のように発火点を絞ると、依存が追跡できる範囲に収まります。

わざと壊して、直す

  1. 各シーンに置いて複製する: ゲームシーンにもSoundManagerを置いて往復すると、Awakeの門番が2体目を破棄してConsoleに警告が出ます(正常動作)。門番のDestroyをコメントアウトすると2体が共存し、BGMが二重に鳴り始めます
  2. 初期化前にアクセスする: 別スクリプトのAwakeからPlayBGMを呼んでみましょう。Instance内生成のおかげでnullにはなりませんが、OnInitialize(設定ロード)より先に再生要求が届く順序は起こり得ます。「起動シーンに配置+Script Execution OrderでSingletonを先に」で順序を固定するのが安心です
  3. 終了中に再生成する(ゾンビSingleton): エディタの再生を停止する瞬間、他オブジェクトのOnDestroyからInstanceに触ると、破棄済みなのにゲッターが新しいGameObjectを生成することがあります。「停止したのにHierarchyにSoundManager (Singleton)が増えている」がこの症状のサインです

補足(終了フラグ): 3の対策は、OnApplicationQuitisQuitting = trueを立て、Instanceゲッターの先頭でif (isQuitting) return null;と弾く「終了フラグ」が定番です。本体コードをシンプルに保つため本文には入れていませんが、症状が出たらこの1組を足してください。

仕上げに眺めてほしいのは、動きよりもコードのほうです。プロジェクトでInstanceを検索して、出てくるのがSoundManagerのような「ゲームに1つだけの役」ばかりなら、線引きは成功しています。敵やHPバーのコードに紛れ込んでいたら、そこはイベントやInspector参照に返してあげましょう。

おまけ:先に知っておくと良いこと

Singletonを使いこなせるようになったら、次はこのあたりが視野に入ってきます。

  • 「通知だけ」ならSingletonすら不要: 「マネージャーのメソッドを呼びたいだけ」の連携は、イベントとデリゲートイベントチャネル で置き換えられることが多く、密結合を避けられます。「アクセスしたい」のか「知らせたいだけ」なのかを区別する癖をつけましょう。
  • 永続化の仕組みそのもの: DontDestroyOnLoadの動作と重複生成の仕組みは DontDestroyOnLoadの記事 で独立して解説しています。本記事のMonoSingleton<T>の背景理解に最適です。
  • Singletonの代表的な使い先: ゲーム全体の状態管理を担う GameManager は、Singletonの最も典型的な応用です。GameManagerパターンの記事 で設計の全体像を掴めます。

まとめ

Unity開発におけるSingletonパターンの正しい実装と注意点について解説しました。このパターンを適切に活用することで、ゲームの管理構造をシンプルに保つことができます。

  • Singletonパターンは、インスタンスが一つであることを保証し、グローバルなアクセスを提供するデザインパターンです。使いどころは「ゲームに1つ・あちこちから呼ばれる」管理役だけ。
  • Unityでは、MonoBehaviourを継承したジェネリックな基底クラスMonoSingleton<T>)を作成することで、安全かつ汎用的なSingletonを実装するのがベストプラクティスです。
  • Awakeメソッド内で重複インスタンスのチェックと破棄を行うことで、シーン遷移時のバグを防ぎます。
  • 実行順序によるnullInstanceプロパティ内生成とScript Execution Orderで、破棄後の参照残りOnDestroyでのクリアで対策します。
  • Singletonの乱用は密結合を引き起こします。「知らせたいだけ」の連携はイベントで代替し、Singletonはグローバルな管理役に限定しましょう。

これらの知識を活かし、あなたのUnityプロジェクトをより効率的で堅牢なものにしてください。