Maemaemae

Unityのシングルトンパターン

Unityプロジェクトでは、ゲーム状態の管理、オーディオの制御、入力処理など、アプリケーション全体で一貫して利用できる機能が必要になります。このような場合に役立つのが「シングルトンパターン」です。この記事では、Unityでのシングルトンパターンの実装方法、よくあるエラーとその対策、実践的な使用例について解説します。

シングルトンパターンとは

シングルトンパターンは、クラスのインスタンスが必ず1つだけ存在することを保証し、そのインスタンスへのグローバルなアクセスポイントを提供するデザインパターンです。ゲーム開発においては、以下のような場面で特に有用です:

基本的なシングルトン実装

Unityでの最も基本的なシングルトン実装は以下の通りです:

using UnityEngine;

public class GameManager : MonoBehaviour
{
    // 静的インスタンス参照
    private static GameManager _instance;

    // 公開アクセスポイント
    public static GameManager Instance
    {
        get
        {
            if (_instance == null)
            {
                Debug.LogError("GameManager is not available!");
            }
            return _instance;
        }
    }

    private void Awake()
    {
        // シングルトンの初期化
        if (_instance == null)
        {
            _instance = this;
            DontDestroyOnLoad(gameObject); // シーン遷移時も破棄しない
        }
        else
        {
            // 既に存在する場合は重複を破棄
            Destroy(gameObject);
        }
    }
}

この基本実装では:

  1. 静的変数 _instance がクラスのインスタンスを保持
  2. Instance プロパティが外部からのアクセスポイントになる
  3. Awake() メソッドでインスタンスの一意性を確保
  4. DontDestroyOnLoad() でシーン遷移時もインスタンスを維持

発展的なシングルトン実装

自動生成機能を持つシングルトン

シングルトンが存在しない場合に自動的に生成する機能を追加した実装です:

using UnityEngine;

public class AudioManager : MonoBehaviour
{
    private static AudioManager _instance;

    public static AudioManager Instance
    {
        get
        {
            // インスタンスがなければ自動生成
            if (_instance == null)
            {
                GameObject go = new GameObject("AudioManager");
                _instance = go.AddComponent<AudioManager>();
                DontDestroyOnLoad(go);
            }
            return _instance;
        }
    }

    private void Awake()
    {
        if (_instance == null)
        {
            _instance = this;
            DontDestroyOnLoad(gameObject);
        }
        else if (_instance != this)
        {
            Destroy(gameObject);
        }

        // 初期化処理...
    }
}

汎用的なシングルトン基底クラス

複数のマネージャークラスで再利用できるジェネリックな基底クラスを作成することで、コードの重複を避けられます:

using UnityEngine;

public abstract class Singleton<T> : MonoBehaviour where T : MonoBehaviour
{
    private static T _instance;

    public static T Instance
    {
        get
        {
            if (_instance == null)
            {
                // シーン内のインスタンスを検索
                _instance = FindObjectOfType<T>();

                // 見つからなければ新規作成
                if (_instance == null)
                {
                    GameObject go = new GameObject(typeof(T).Name);
                    _instance = go.AddComponent<T>();
                }

                DontDestroyOnLoad(_instance.gameObject);
            }
            return _instance;
        }
    }

    protected virtual void Awake()
    {
        if (_instance == null)
        {
            _instance = this as T;
            DontDestroyOnLoad(gameObject);
        }
        else if (_instance != this)
        {
            Destroy(gameObject);
        }
    }
}

// 使用例
public class UIManager : Singleton<UIManager>
{
    // UIManagerの固有メソッドや変数
    public void ShowMainMenu()
    {
        Debug.Log("メインメニューを表示");
    }

    protected override void Awake()
    {
        base.Awake();
        // UIManagerの初期化処理
    }
}

よくあるNullReferenceExceptionとその対策

シングルトンパターンを使用する際によく発生するのが NullReferenceException です。特に以下のケースに注意が必要です:

1. 初期化タイミングの問題

// エラーの例
void Start()
{
    // GameManagerが初期化される前にアクセスするとエラー
    GameManager.Instance.StartGame();
}

// 安全な実装
void Start()
{
    if (GameManager.Instance != null)
    {
        GameManager.Instance.StartGame();
    }
    else
    {
        Debug.LogWarning("GameManagerが初期化されていません");
        // 代替処理...
    }
}

2. シングルトン間の依存関係によるエラー

// 問題のあるコード
public class SaveManager : MonoBehaviour
{
    private static SaveManager _instance;
    public static SaveManager Instance => _instance;

    void Awake()
    {
        _instance = this;
        // GameManagerが初期化されていない可能性がある
        GameManager.Instance.RegisterSaveSystem(this);
    }
}

// 改善策
public class SaveManager : MonoBehaviour
{
    private static SaveManager _instance;
    public static SaveManager Instance => _instance;

    void Awake()
    {
        _instance = this;
        DontDestroyOnLoad(gameObject);
    }

    void Start()
    {
        // Startメソッドは全てのAwakeの後に呼ばれる
        if (GameManager.Instance != null)
        {
            GameManager.Instance.RegisterSaveSystem(this);
        }
    }
}

3. シーン遷移時の問題

// よくある間違い
public class InputManager : MonoBehaviour
{
    public static InputManager Instance { get; private set; }

    void Awake()
    {
        if (Instance == null)
        {
            Instance = this;
            // DontDestroyOnLoadを忘れると
            // シーン遷移時にInstanceがnullになる
        }
        else
        {
            Destroy(gameObject);
        }
    }
}

// 正しい実装
public class InputManager : MonoBehaviour
{
    public static InputManager Instance { get; private set; }

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

実践的なエラー対策テクニック

シングルトン存在チェックメソッド

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { get; private set; }

    // シングルトンの存在確認メソッド
    public static bool HasInstance()
    {
        return Instance != null;
    }

    // 安全にアクセスするメソッド
    public static GameManager GetSafeInstance()
    {
        if (Instance == null)
        {
            Debug.LogWarning("GameManagerのインスタンスが利用できません");
            // 必要に応じて新規作成
        }
        return Instance;
    }
}

// 使用例
void DoSomething()
{
    if (GameManager.HasInstance())
    {
        GameManager.Instance.ProcessAction();
    }
}

初期化順序の制御

public class GameInitializer : MonoBehaviour
{
    void Awake()
    {
        // シングルトンの初期化順序を明示的に制御
        CreateSingleton<GameManager>();
        CreateSingleton<AudioManager>();
        CreateSingleton<UIManager>();
        CreateSingleton<SaveManager>();
    }

    private void CreateSingleton<T>() where T : MonoBehaviour
    {
        if (FindObjectOfType<T>() == null)
        {
            GameObject go = new GameObject(typeof(T).Name);
            go.AddComponent<T>();
            DontDestroyOnLoad(go);
        }
    }
}

Lazy Initialization(遅延初期化)の安全な実装

public class ResourceManager : MonoBehaviour
{
    private static ResourceManager _instance;
    private static object _lock = new object();
    private static bool _isInitializing = false;

    public static ResourceManager Instance
    {
        get
        {
            if (_instance == null && !_isInitializing)
            {
                lock (_lock)
                {
                    if (_instance == null && !_isInitializing)
                    {
                        _isInitializing = true;
                        GameObject go = new GameObject("ResourceManager");
                        _instance = go.AddComponent<ResourceManager>();
                        DontDestroyOnLoad(go);
                        _isInitializing = false;
                    }
                }
            }
            return _instance;
        }
    }
}

実用的なシングルトン実装例

ゲームマネージャー

public class GameManager : Singleton<GameManager>
{
    // ゲーム状態
    public enum GameState { MainMenu, Playing, Paused, GameOver }
    private GameState _currentState;

    // 公開プロパティ
    public GameState CurrentState
    {
        get => _currentState;
        private set
        {
            _currentState = value;
            OnGameStateChanged?.Invoke(_currentState);
        }
    }

    // ゲーム状態変更イベント
    public event System.Action<GameState> OnGameStateChanged;

    // スコア管理
    private int _score;
    public int Score
    {
        get => _score;
        set
        {
            _score = value;
            OnScoreChanged?.Invoke(_score);
        }
    }

    public event System.Action<int> OnScoreChanged;

    // ゲーム開始
    public void StartGame()
    {
        Score = 0;
        CurrentState = GameState.Playing;
        Debug.Log("ゲーム開始");
    }

    // ゲーム一時停止
    public void PauseGame()
    {
        if (CurrentState == GameState.Playing)
        {
            CurrentState = GameState.Paused;
            Time.timeScale = 0f;
        }
        else if (CurrentState == GameState.Paused)
        {
            CurrentState = GameState.Playing;
            Time.timeScale = 1f;
        }
    }

    // ゲームオーバー
    public void GameOver()
    {
        CurrentState = GameState.GameOver;
        Debug.Log($"ゲームオーバー、最終スコア: {Score}");
    }
}

オーディオマネージャー

public class AudioManager : Singleton<AudioManager>
{
    // 音楽再生用
    [SerializeField] private AudioSource musicSource;
    // 効果音再生用
    [SerializeField] private AudioSource sfxSource;

    // 音楽クリップディクショナリ
    private Dictionary<string, AudioClip> musicClips = new Dictionary<string, AudioClip>();
    // 効果音クリップディクショナリ
    private Dictionary<string, AudioClip> sfxClips = new Dictionary<string, AudioClip>();

    protected override void Awake()
    {
        base.Awake();

        // AudioSourceがない場合は追加
        if (musicSource == null)
        {
            musicSource = gameObject.AddComponent<AudioSource>();
            musicSource.loop = true;
        }

        if (sfxSource == null)
        {
            sfxSource = gameObject.AddComponent<AudioSource>();
        }

        // 音楽と効果音を読み込む
        LoadAudioClips();
    }

    private void LoadAudioClips()
    {
        // Resources/Audio/Musicフォルダから全ての音楽を読み込む
        AudioClip[] loadedMusic = Resources.LoadAll<AudioClip>("Audio/Music");
        foreach (AudioClip clip in loadedMusic)
        {
            musicClips.Add(clip.name, clip);
        }

        // Resources/Audio/SFXフォルダから全ての効果音を読み込む
        AudioClip[] loadedSFX = Resources.LoadAll<AudioClip>("Audio/SFX");
        foreach (AudioClip clip in loadedSFX)
        {
            sfxClips.Add(clip.name, clip);
        }

        Debug.Log($"読み込み完了: 音楽 {musicClips.Count}個, 効果音 {sfxClips.Count}個");
    }

    // 音楽を再生
    public void PlayMusic(string name)
    {
        if (musicClips.TryGetValue(name, out AudioClip clip))
        {
            musicSource.clip = clip;
            musicSource.Play();
        }
        else
        {
            Debug.LogWarning($"音楽 '{name}' が見つかりません");
        }
    }

    // 効果音を再生
    public void PlaySFX(string name)
    {
        if (sfxClips.TryGetValue(name, out AudioClip clip))
        {
            sfxSource.PlayOneShot(clip);
        }
        else
        {
            Debug.LogWarning($"効果音 '{name}' が見つかりません");
        }
    }

    // 音量設定
    public void SetMusicVolume(float volume)
    {
        musicSource.volume = Mathf.Clamp01(volume);
    }

    public void SetSFXVolume(float volume)
    {
        sfxSource.volume = Mathf.Clamp01(volume);
    }
}

シングルトンパターンの注意点とベストプラクティス

使用する際の注意点

  1. 過剰な使用を避ける: すべてのクラスをシングルトンにする誘惑に負けないようにしましょう。必要な場面でのみ使用しましょう。

  2. テスト難易度: シングルトンはユニットテストを難しくします。テスト可能性を高めるために依存性注入などの手法を検討しましょう。

  3. 依存関係: シングルトン間の循環依存を避け、明確な初期化順序を設定しましょう。

  4. スレッドセーフティ: マルチスレッド環境では、適切な同期機構(lockなど)を使用しましょう。

ベストプラクティス

  1. 専用のブートストラップシーン: すべてのシングルトンを初期化するための専用シーンを作成しましょう。

  2. 初期化順序の制御: 依存関係のあるシングルトン間の初期化順序を明示的に制御しましょう。

  3. シングルトン間の通信方法: イベントベースの通信を採用して、シングルトン間の結合度を下げましょう。

  4. NullReferenceExceptionの対策: 常にnullチェックを行い、フォールバックや自動生成機能を実装しましょう。

  5. ドキュメント化: プロジェクト内のシングルトンの役割と責任範囲を明確に文書化しましょう。

まとめ

Unityにおけるシングルトンパターンは、ゲーム全体で一貫して利用される機能を提供するための強力な手法です。この記事で紹介した実装方法とエラー対策テクニックを活用して、より堅牢なゲームアプリケーションを構築してください。

ただし、シングルトンパターンの使用は適材適所で行い、過剰な依存を作り出さないよう注意しましょう。適切に実装されたシングルトンは、コードの可読性と保守性を高め、開発効率を向上させる強力なツールとなります。

参考資料


Unity 2020.3以上を対象としています。より古いバージョンでも基本的な概念は適用できますが、一部のC#機能は利用できない場合があります。

UnityC#デザインパターンシングルトンゲーム開発