IkinokoBattleで学ぶ シングルトンパターン
対象読者:C# のクラスと
staticを理解しており、Unity のコードを読み進められる方
このブログでは、いきのこバトル(IkinokoBattle) に登場する 3種類のシングルトン実装を比較しながら、「なぜシングルトンが必要か」「どう書き分けるか」を学びます。
Unity本格入門シリーズ(書籍補助の機能解説)とは別シリーズです。本シリーズ目次
記事の目次
おすすめの読み方
セクション一覧
ポイント一覧
- ① シングルトンとは「インスタンスをひとつに絞る」パターン
- ②
privateコンストラクタでnewを禁止する - ③
staticフィールドで唯一のインスタンスを保持する - ④ 遅延初期化(Lazy Initialization)で必要になるまで作らない
- ⑤
MonoBehaviourシングルトン(AudioManager)との違い - ⑥
DontDestroyOnLoadでシーンをまたいで生き続ける - ⑦ 重複インスタンスの防止の 2 つの方法
シングルトンとは
シングルトン(Singleton)パターンとは、あるクラスのインスタンスを アプリケーション全体でただひとつ に制限するオブジェクト指向設計のパターンです。
ゲーム開発で特に重宝されます。たとえば「プレイヤーの所持品データ」を管理するオブジェクトが複数存在すると、どちらが正しいデータを持っているか曖昧になってしまいます。「サウンドを再生するスピーカー」も2台あれば音が重なります。このような「どこからアクセスしても同じひとつのオブジェクト」が必要な場面で、シングルトンパターンが登場します。
いきのこバトルには、目的の異なる 3種類のシングルトン が実装されています。この記事では 3 つを並べて読むことで、「シングルトンの書き方は目的によって変わる」ことを体感してください。
題材の範囲と登場クラス
今回は以下の 3 クラスを題材にします。
| クラス | 役割 | シングルトンの種類 |
|---|---|---|
OwnedItemsData |
所持品データの保存・読み込み | 純粋 C# クラスのシングルトン(遅延初期化) |
AudioManager |
効果音の一元管理 | MonoBehaviour シングルトン(シーンまたぎ) |
LifeGaugeContainer |
HPゲージ UI の管理 | MonoBehaviour シングルトン(シーン内限定・厳格) |
注目ポイント:同じ「シングルトン」でも、クラスの性質(MonoBehaviour か否か、シーンをまたぐか否か)によってコードの書き方が変わります。
コードを全部見てみよう
OwnedItemsData.cs(純粋 C# クラスのシングルトン)
using System;
using System.Linq;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class OwnedItemsData
{
private const string PlayerPrefsKey = "OWNED_ITEMS_DATA";
public static OwnedItemsData Instance
{
get
{
if (null == _instance)
{
_instance = PlayerPrefs.HasKey(PlayerPrefsKey)
? JsonUtility.FromJson<OwnedItemsData>(PlayerPrefs.GetString(PlayerPrefsKey))
: new OwnedItemsData();
}
return _instance;
}
}
private static OwnedItemsData _instance;
public OwnedItem[] OwnedItems
{
get { return ownedItems.ToArray(); }
}
[SerializeField] private List<OwnedItem> ownedItems = new List<OwnedItem>();
private OwnedItemsData() { } // ← private コンストラクタ
public void Save()
{
var jsonString = JsonUtility.ToJson(this);
PlayerPrefs.SetString(PlayerPrefsKey, jsonString);
PlayerPrefs.Save();
}
public void Add(Item.ItemType type, int number = 1) { /* 省略 */ }
public void Use(Item.ItemType type, int number = 1) { /* 省略 */ }
public OwnedItem GetItem(Item.ItemType type) { /* 省略 */ }
[Serializable]
public class OwnedItem
{
public Item.ItemType Type { get { return type; } }
public int Number { get { return number; } }
[SerializeField] private Item.ItemType type;
[SerializeField] private int number;
public OwnedItem(Item.ItemType type) { this.type = type; }
public void Add(int number = 1) { this.number += number; }
public void Use(int number = 1) { this.number -= number; }
}
}AudioManager.cs(MonoBehaviour シングルトン・シーンまたぎ)
using System;
using System.Collections.Generic;
using UnityEngine;
public class AudioManager : MonoBehaviour
{
private static AudioManager instance;
[SerializeField] private AudioSource _audioSource;
private readonly Dictionary<string, AudioClip> _clips = new Dictionary<string, AudioClip>();
public static AudioManager Instance
{
get { return instance; }
}
private void Awake()
{
if (null != instance)
{
Destroy(gameObject); // ← 既存があれば自分を破棄
return;
}
DontDestroyOnLoad(gameObject); // ← シーンをまたいで存続
instance = this;
AudioClip[] audioClips = Resources.LoadAll<AudioClip>("2D_SE");
foreach (var clip in audioClips)
{
_clips.Add(clip.name, clip);
}
}
public void Play(string clipName)
{
if (!_clips.ContainsKey(clipName))
throw new Exception("Sound " + clipName + " is not defined");
_audioSource.clip = _clips[clipName];
_audioSource.Play();
}
}LifeGaugeContainer.cs(MonoBehaviour シングルトン・シーン内厳格)
using System;
using System.Collections.Generic;
using UnityEngine;
[RequireComponent(typeof(RectTransform))]
public class LifeGaugeContainer : MonoBehaviour
{
public static LifeGaugeContainer Instance
{
get { return _instance; }
}
private static LifeGaugeContainer _instance;
[SerializeField] private Camera mainCamera;
[SerializeField] private LifeGauge lifeGaugePrefab;
private RectTransform rectTransform;
private readonly Dictionary<MobStatus, LifeGauge> _statusLifeBarMap
= new Dictionary<MobStatus, LifeGauge>();
private void Awake()
{
if (null != _instance) throw new Exception("LifeBarContainer instance already exists."); // ← 重複は例外
_instance = this;
rectTransform = GetComponent<RectTransform>();
}
public void Add(MobStatus status)
{
var lifeGauge = Instantiate(lifeGaugePrefab, transform);
lifeGauge.Initialize(rectTransform, mainCamera, status);
_statusLifeBarMap.Add(status, lifeGauge);
}
public void Remove(MobStatus status)
{
Destroy(_statusLifeBarMap[status].gameObject);
_statusLifeBarMap.Remove(status);
}
}① シングルトンとは「インスタンスをひとつに絞る」パターン
通常、クラスは new するたびに新しいインスタンスが生まれます。
var a = new OwnedItemsData(); // ← 仮に public にできたとして
var b = new OwnedItemsData(); // a と b は別物所持品データが 2 つ存在すると、どちらに Add() すればいいか分からなくなります。シングルトンパターンはこの問題を根本から防ぎます。
設計の意図: 「このクラスのインスタンスはこれひとつ」という事実をコードで明示する。外から
newできないようにして、Instanceプロパティ経由で必ずその 1 つにアクセスさせる。
② private コンストラクタで new を禁止する
OwnedItemsData には次の宣言があります。
private OwnedItemsData() { }コンストラクタを private にすると、クラスの外側から new OwnedItemsData() と書いてもコンパイルエラーになります。これでインスタンスの生成口を「クラス自身の内部だけ」に限定できます。
注意:
MonoBehaviourを継承したクラスは Unity のエンジンがnewするため、この手法は使えません。AudioManagerとLifeGaugeContainerが異なるアプローチを取る理由はここにあります。
③ static フィールドで唯一のインスタンスを保持する
private static OwnedItemsData _instance;static フィールドはクラスに 1 つだけ存在し、インスタンスの数に関係なく共有されます。シングルトンではここにインスタンスを格納することで「どこからアクセスしても同じオブジェクト」を実現します。
AudioManager でも同じ役割を担う宣言があります。
private static AudioManager instance;public static プロパティ Instance(大文字)が外部への窓口、private static フィールド _instance / instance(小文字)が実体の保管庫、という命名規則になっています。
④ 遅延初期化(Lazy Initialization)で必要になるまで作らない
OwnedItemsData.Instance のゲッターを見てみましょう。
get
{
if (null == _instance)
{
_instance = PlayerPrefs.HasKey(PlayerPrefsKey)
? JsonUtility.FromJson<OwnedItemsData>(PlayerPrefs.GetString(PlayerPrefsKey))
: new OwnedItemsData();
}
return _instance;
}_instance が null(まだ作られていない)のときだけ初期化しています。これを**遅延初期化(Lazy Initialization)**と呼びます。
さらに「PlayerPrefs にセーブ済みのデータがあれば JSON から復元し、なければ新規作成する」という読み込み処理も Instance の中に含まれています。つまり、初めてアクセスしたそのタイミングで自動的に永続データを読み込んでくれます。
ポイント:初期化のタイミングを「使われる瞬間」に委ねることで、起動直後に重い処理を行わなくて済む。また初期化コードを
Instanceに集中させることで、呼び出し側は「Instanceを使えばいい」という簡潔な形で書ける。
⑤ MonoBehaviour シングルトン(AudioManager)との違い
OwnedItemsData は Unity のコンポーネント機能(Update や Awake など)を必要としないため、純粋 C# クラスとして実装されています。一方、AudioManager は AudioSource コンポーネントを使って実際に音声を再生するため、MonoBehaviour を継承しています。
MonoBehaviour クラスは new で作れないため、シングルトンの仕組みも変わります。
private void Awake()
{
if (null != instance)
{
Destroy(gameObject); // 既に存在するなら自分(後から生まれた方)を消す
return;
}
DontDestroyOnLoad(gameObject);
instance = this;
// ...(クリップ辞書の構築)
}インスタンスの作成は Unity エディタ上でシーンに GameObject として配置することで行います。Awake() の先頭で「既に instance が存在するか」を確認し、存在していれば自分自身(後から生まれた複製)を Destroy します。これが MonoBehaviour でのシングルトン実装の定石です。
⑥ DontDestroyOnLoad でシーンをまたいで生き続ける
通常、Unity ではシーンを切り替えると前のシーンにあった GameObject はすべて破棄されます。AudioManager は BGM・SE をシーンをまたいで途切れなく再生するために、これを回避する必要があります。
DontDestroyOnLoad(gameObject);この一行を Awake() に書くだけで、そのオブジェクトはシーン遷移時に破棄されなくなります。タイトルシーンで初期化された AudioManager がメインシーン・ゲームオーバーシーンでもそのまま生き続け、AudioManager.Instance.Play("SE_Name") でどこからでも呼べる状態を維持します。
注意点:
DontDestroyOnLoadは「存続させる」だけで「ひとつしか作らない」は別の仕組みが必要です。シーンを再ロードすると再びAwake()が走って 2 つ目が生まれようとするため、先に示した「既存があればDestroy」の処理と組み合わせてはじめてシングルトンが成立します。
⑦ 重複インスタンスの防止の 2 つの方法
重複防止の戦略は AudioManager と LifeGaugeContainer で異なります。
AudioManager:後から来た方を黙って削除
if (null != instance)
{
Destroy(gameObject);
return;
}2 つ目が誕生しようとした瞬間に自分を削除します。既存のインスタンスはそのまま生き続けるため、ゲームの動作に影響を与えません。シーンをまたいで長生きするオブジェクトに向いたアプローチです。
LifeGaugeContainer:重複を見つけたら例外を投げる
if (null != _instance) throw new Exception("LifeBarContainer instance already exists.");こちらは例外(Exception)を投げてプログラムを止めます。LifeGaugeContainer はメインシーンの UI に密接に紐付いており、2 つ存在すること自体が「シーン設計の誤り」として扱います。開発中に早期発見・早期修正するための厳格なアプローチです。
使い分けの目安:
DontDestroyOnLoadでシーンをまたぐものは「黙って削除」方式、シーン内限定で絶対に 1 つでなければならないものは「例外を投げる」方式が読みやすい傾向があります。
3種類のシングルトン比較
| 項目 | OwnedItemsData |
AudioManager |
LifeGaugeContainer |
|---|---|---|---|
| 基底クラス | 純粋 C# | MonoBehaviour | MonoBehaviour |
| インスタンス生成 | Instance ゲッター内で new |
Unity エディタで GameObject 配置 | Unity エディタで GameObject 配置 |
| 初期化タイミング | 初アクセス時(遅延) | Awake() |
Awake() |
| シーンをまたぐか | またぐ(static で自然に残る) | またぐ(DontDestroyOnLoad) | またがない |
| 重複防止 | private コンストラクタ | Destroy(黙って削除) | throw Exception(例外) |
| セーブ機能 | あり(PlayerPrefs + JSON) | なし | なし |
コードの流れを整理しよう
所持品データの流れ(OwnedItemsData)
ゲーム起動
└─ 初めて OwnedItemsData.Instance にアクセス
├─ PlayerPrefs にデータあり → JSON から復元してインスタンス化
└─ データなし → new OwnedItemsData()(空のリスト)
↓
アイテム取得(Item.cs の OnTriggerEnter)
└─ OwnedItemsData.Instance.Add(ItemType)
└─ Save() → JSON → PlayerPrefs へ書き込み音声再生の流れ(AudioManager)
タイトルシーン起動
└─ AudioManager(GameObject)の Awake()
└─ DontDestroyOnLoad → instance = this
└─ Resources.LoadAll で SE 辞書を構築
↓
任意のシーンから AudioManager.Instance.Play("se_name")
└─ 辞書からクリップを取得 → AudioSource.Play()自分でカスタマイズしてみよう!
課題 A:シングルトンをリセットできるようにする
OwnedItemsData にはインスタンスをリセットするメソッドがありません。「ニューゲーム開始時に所持品を全消去したい」という要件を追加してみましょう。
// OwnedItemsData に追加
public static void ResetInstance()
{
_instance = null;
PlayerPrefs.DeleteKey(PlayerPrefsKey);
}これを呼び出した後に Instance にアクセスすると、新しい空の所持品データが作られます。ゲームタイトル画面の「はじめから」ボタンのイベントに繋いでみましょう。
課題 B:AudioManager に BGM 再生を追加する
現在の AudioManager は SE(効果音)の再生のみです。AudioSource を 2 つ持つ(SE 用・BGM 用)か、BGM 専用のメソッドを追加して、ループ再生と切り替えを実装してみましょう。
[SerializeField] private AudioSource _bgmSource;
public void PlayBGM(string clipName)
{
if (!_clips.ContainsKey(clipName))
throw new Exception("BGM " + clipName + " is not defined");
_bgmSource.clip = _clips[clipName];
_bgmSource.loop = true;
_bgmSource.Play();
}
public void StopBGM()
{
_bgmSource.Stop();
}課題 C:シングルトンを汎用基底クラスにまとめる
3 クラスに共通する「static フィールドで自分を保持し、Awake で登録する」パターンを、ジェネリクスを使った基底クラスにまとめると再利用できます。
public abstract class SingletonMonoBehaviour<T> : MonoBehaviour where T : MonoBehaviour
{
public static T Instance { get; private set; }
protected virtual void Awake()
{
if (Instance != null) { Destroy(gameObject); return; }
Instance = this as T;
}
}
// 使い方
public class AudioManager : SingletonMonoBehaviour<AudioManager>
{
protected override void Awake()
{
base.Awake();
DontDestroyOnLoad(gameObject);
// 以降の初期化...
}
}まとめ
| ポイント | キーワード |
|---|---|
| ① シングルトンはインスタンスを 1 つに制限するパターン | Instance プロパティ |
② private コンストラクタで外からの new を禁止 |
アクセス修飾子 |
③ static フィールドが「唯一の実体」の保管庫 |
static |
| ④ 遅延初期化で「使う瞬間に初めて作る」 | Lazy Initialization |
⑤ MonoBehaviour は new できないので Awake で登録 |
Unity のライフサイクル |
⑥ DontDestroyOnLoad でシーンまたぎを実現 |
シーン管理 |
| ⑦ 重複防止は「黙って削除」か「例外を投げる」で使い分ける | 設計の意図の明示 |
シングルトンパターンは Unity ゲームにおいて最も頻繁に登場する設計パターンのひとつです。いきのこバトルの 3 クラスを並べて読むことで、「なぜ同じパターンでも書き方が変わるのか」が見えてきたでしょうか。次回は テンプレートメソッドパターン として MobStatus・PlayerStatus・EnemyStatus の継承設計を読み解きます。
← シリーズ目次に戻る
次の記事 → 第2回:テンプレートメソッドパターン(近日公開)
最終更新:2026年4月