MonoBehaviour向けのポリシー駆動型シングルトン基底クラス(Domain Reload ON/OFF 両対応)
Features • Requirements • Installation • Quick Start • API • Architecture • Constraints • Limitations • Debug • Troubleshooting • References
PolicyDrivenSingleton は、MonoBehaviour向けの ポリシー駆動型シングルトン基底クラスです。
GlobalSingleton<T>:シーン間永続 + 見つからなければ自動生成SceneSingleton<T>:シーンスコープ + 自動生成しない(シーン配置必須)
Enter Play Mode Options の Reload Domain を無効化して static が Play 間で残る環境でも、Playセッション境界で確実にキャッシュを無効化し、再探索・再初期化できるように設計しています。
| ✅ 本ライブラリが適している場合 | 💡 代替を検討すべき場合 |
|---|---|
| 常駐マネージャ(Audio, Input, Game など) | テスト容易性を重視 → DI コンテナ(Zenject, VContainer 等) |
| シーン内コントローラ(Level, UI など) | データ駆動設計を好む → ScriptableObject ベースのサービスロケータ |
| Domain Reload OFF 環境での安定動作が必要 | 小規模・プロトタイプ → FindAnyObjectByType を都度呼ぶ運用 |
| 明示的なライフサイクル制御が必要 | 状態を持たない処理 → 静的クラスで十分 |
|
|
|
|
| 項目 | 要件 |
|---|---|
| Unity | 2022.3+(Unity 6.3でテスト済み) |
| Enter Play Mode Options | Reload Domain ON/OFF 両対応 |
| 外部依存 | なし |
PolicyDrivenSingleton/フォルダを任意の場所へコピー 例:Assets/Plugins/PolicyDrivenSingleton/- 必要なら asmdef 名や namespace をプロジェクト方針に合わせて調整
- submodule / subtree 等で
PolicyDrivenSingleton/を取り込む運用も可能です (このリポジトリは UPM 前提ではありません)
using PolicyDrivenSingleton;
// 継承禁止 (sealed) を推奨します
public sealed class GameManager : GlobalSingleton<GameManager>
{
protected override void Awake()
{
base.Awake(); // 必須 - シングルトンを初期化します
// 初回のみの初期化
}
protected override void OnPlaySessionStart()
{
// Playセッションごとの再初期化(Domain Reload OFF を含む)
// 例:一時データ、イベント購読、キャッシュの再構築
}
}
// 利用例:
// GameManager.Instance.AddScore(10);using PolicyDrivenSingleton;
public sealed class LevelController : SceneSingleton<LevelController>
{
protected override void Awake()
{
base.Awake(); // 必須
}
}
// ⚠️ シーン配置必須(置き忘れは DEV/EDITOR/ASSERTIONS で fail-fast)private GameManager _gm;
private void Awake()
{
_gm = GameManager.Instance; // 起動時に確立してキャッシュ
}
private void Update()
{
if (_gm == null) return; // fail-soft 構成の保険
// ...
}| メンバー | 目的 | 自動生成 | 典型用途 |
|---|---|---|---|
T Instance { get; } |
必須経路:確立/取得 | global_only |
起動・初期化・ゲーム進行必須 |
bool TryGetInstance(out T instance) |
任意経路:あれば使う | never |
後片付け、解除、終了/中断経路 |
自動生成
global_only:GlobalSingleton<T>は未存在なら生成、SceneSingleton<T>は生成しないnever: 生成しない
| メンバー | 目的 | 典型用途 |
|---|---|---|
protected virtual void OnPlaySessionStart() |
Playセッション単位の再初期化 | Domain Reload OFF 対策、再購読、キャッシュ再構築 |
bool TryPostToMainThread(Action action) |
BG→メインへ委譲 | 非同期結果のUI反映、Unity API 呼び出し |
| 状態 | Instance |
TryGetInstance |
|---|---|---|
| Play 中 | 確立済みなら返す / 必要なら検索・(Globalは)生成 | 存在すれば返す(生成しない) |
| 終了処理中 | null |
false |
| Edit Mode | 検索のみ(生成しない・キャッシュ更新しない) | 検索のみ(キャッシュ更新しない) |
推奨:解除系(OnDisable/OnDestroy/OnApplicationPause等)は
TryGetInstanceを原則にする。
fail-fast / fail-soft の方針(詳細)
-
DEV/EDITOR/ASSERTIONS:誤用を早期に発見するため、以下は fail-fast(例外)になり得ます。
- 非アクティブなシングルトン検出(検索APIがinactiveを既定で見ないため、隠れ重複に繋がる)
- SceneSingleton の置き忘れ(自動生成しない契約)
-
Player:検証やログは
[Conditional]等でストリップされる前提のため、fail-soft(null/false)になり得ます。 -
したがって利用側は
null/falseを前提にハンドリングしてください(特に解除/終了経路)。
API Quick Reference(状態別の詳細)
| 状態 | GlobalSingleton | SceneSingleton |
|---|---|---|
| Play中(正常) | キャッシュ済み → 返す / なければ検索 → 自動生成 | キャッシュ済み → 返す / なければ検索のみ |
| 終了処理中 | null |
null |
| Edit Mode | 検索のみ(生成・キャッシュ更新なし) | 検索のみ |
| 非アクティブ/無効コンポーネント検出時 | DEV: 例外 / Player: null |
DEV: 例外 / Player: null |
| シーン未配置 | 自動生成 | DEV: 例外 / Player: null |
| 型不一致 | 拒否(DEV: Error ログ → 破棄) | 拒否(DEV: Error ログ → 破棄) |
| バックグラウンドスレッド | null(Error ログ出力) |
null(Error ログ出力) |
| 状態 | 振る舞い |
|---|---|
| 存在する | true + 有効な参照 |
| 存在しない | false + null(自動生成しない) |
| 終了処理中 | false + null |
| Edit Mode | 検索のみ(キャッシュ更新しない) |
| 非アクティブ/無効コンポーネント検出時 | DEV: 例外 / Player: false + null |
| バックグラウンドスレッド | false + null(Error ログ出力) |
補足:
TryGetInstanceはFindAnyObjectByTypeを使うため、非アクティブな GameObject は既定では検索対象外です(検出時のみ例外)。
| 条件 | 呼び出し |
|---|---|
| 初回 Play(Domain Reload ON) | Awake() → OnPlaySessionStart() |
| 2回目以降 Play(Domain Reload OFF) | OnPlaySessionStart() のみ(Awake() は呼ばれない) |
| シングルトン確立時 | 1 Play セッションにつき 1 回のみ |
| 状態 | 振る舞い |
|---|---|
| メインスレッド上 | 即座に実行、true を返す |
| バックグラウンドスレッド | SyncContext 経由で Post、true を返す |
| SyncContext 未キャプチャ | false を返す(fail-soft)、Error ログ出力 |
| null アクション | false を返す |
flowchart TB
subgraph PublicAPI["Public API"]
G["GlobalSingleton<T><br/>PersistentPolicy<br/>• DontDestroyOnLoad<br/>• Auto-create"]
S["SceneSingleton<T><br/>SceneScopedPolicy<br/>• Scene bound<br/>• No auto-create"]
end
subgraph Core["Core"]
B["SingletonBehaviour<T, TPolicy><br/>• Instance/TryGetInstance<br/>• TryPostToMainThread<br/>• Hooks: OnPlaySessionStart"]
end
subgraph Runtime["Runtime (internal)"]
R["SingletonRuntime<br/>• PlaySessionId<br/>• IsQuitting (best-effort)<br/>• Thread validation<br/>• Main thread posting"]
L["SingletonLogger<br/>• Conditional logs<br/>• Stripped in Player by design"]
end
subgraph Editor["Editor only"]
E["SingletonEditorHooks<br/>• Play Mode events<br/>• ClearQuittingFlag()"]
end
G --> B
S --> B
B --> R
B --> L
E --> R
Notes:
- Editor hooks の方向:
SingletonEditorHooks(Editor専用)がSingletonRuntime.ClearQuittingFlag()を呼び出し、Play Mode 境界で状態をリセット - internal クラス:
SingletonRuntime/SingletonLoggerはinternalであり、外部から直接呼び出し不可
- Domain Reload OFF でも安全:Playセッション開始ごとに
PlaySessionIdを更新し、型ごとの static キャッシュを無効化 → 再探索して同一インスタンスを掴み直す - Edit Mode 副作用ゼロ:エディタ拡張から呼んでも生成やキャッシュ更新を行わない
- 検索仕様に合わせた防御:Find系APIは既定で inactive を対象外にするため、非アクティブなシングルトンは「存在しても見つからない扱い → 自動生成 → 隠れ重複」になり得る。DEV/EDITOR/ASSERTIONS では強く検出する
Instance 取得フロー(図解)
flowchart TD
A[Instance 呼び出し] --> B{メインスレッド?}
B -->|No| Z1[null + Error ログ]
B -->|Yes| C{Edit Mode?}
C -->|Yes| D[検索のみ → 返す]
C -->|No| E{終了中?}
E -->|Yes| Z2[null]
E -->|No| F{キャッシュあり?}
F -->|Yes| G[キャッシュを返す]
F -->|No| H[Find で検索]
H --> I{見つかった?}
I -->|Yes| J{Active & Enabled?}
J -->|No| Z3[DEV: 例外 / Player: null]
J -->|Yes| K[初期化 → キャッシュ → 返す]
I -->|No| L{AutoCreate 許可?}
L -->|Yes| M[生成 → 初期化 → 返す]
L -->|No| Z4[DEV: 例外 / Player: null]
Play Session 境界のライフサイクル(Domain Reload OFF)
sequenceDiagram
participant U as Unity Runtime
participant R as SingletonRuntime
participant S as Singleton<T>
Note over U,S: Play Session 1 開始
U->>R: SubsystemRegistration
R->>R: PlaySessionId++
U->>S: Awake()
S->>S: InitializeForCurrentPlaySession
S->>S: OnPlaySessionStart()
Note over U,S: Play Session 1 終了
U->>R: Application.quitting
R->>R: IsQuitting = true
Note over U,S: Play Session 2 開始(Domain Reload OFF)
U->>R: SubsystemRegistration
R->>R: PlaySessionId++, IsQuitting = false
Note over S: Awake() は呼ばれない(オブジェクト生存中)
U->>S: Instance アクセス
S->>S: PlaySessionId 変更検出 → キャッシュ無効化
S->>S: 再検索 → 再確立
S->>S: OnPlaySessionStart()
PolicyDrivenSingleton/
├── Core/
│ ├── AssemblyInfo.cs # InternalsVisibleTo(テスト用)
│ ├── SingletonBehaviour.cs # コア実装
│ ├── SingletonLogger.cs # 条件付きロガー(Playerビルドで除去)
│ └── SingletonRuntime.cs # 内部ランタイム(Domain Reload対策)
├── Editor/
│ ├── SingletonEditorHooks.cs # Editorイベントフック(Play Mode状態)
│ └── PolicyDrivenSingleton.Editor.asmdef # Editor用 asmdef
├── Policy/
│ ├── ISingletonPolicy.cs # ポリシーインターフェース
│ ├── PersistentPolicy.cs # 永続ポリシー
│ └── SceneScopedPolicy.cs # シーンスコープポリシー
├── Tests/ # PlayMode & EditMode テスト
│ ├── Editor/
│ │ ├── Behaviours/ # EditMode: behaviour系
│ │ ├── Core/ # EditMode: runtime状態
│ │ ├── Logging/ # EditMode: ログ
│ │ ├── Policy/ # EditMode: ポリシー
│ │ └── PolicyDrivenSingleton.Editor.Tests.asmdef
│ ├── Runtime/
│ │ ├── Domain/
│ │ ├── Doubles/
│ │ ├── EdgeCases/
│ │ ├── GlobalSingleton/
│ │ ├── Hierarchy/
│ │ ├── Infrastructure/
│ │ ├── Lifecycle/
│ │ ├── Policy/
│ │ ├── Practical/
│ │ ├── SceneSingleton/
│ │ ├── Threading/
│ │ ├── Validation/
│ │ ├── PolicySingletonTestSetup.cs # SetUpFixture(ログカウンタ)
│ │ └── PolicyDrivenSingleton.Tests.asmdef
│ ├── Shared/ # EditMode用のテストダブル
│ │ ├── EditModeSingletonDoubles.cs
│ │ └── PolicyDrivenSingleton.Tests.Shared.asmdef
│ └── TestExtensions.cs # テスト用ヘルパー
├── GlobalSingleton.cs # Public API(永続・自動生成あり)
├── SceneSingleton.cs # Public API(シーン限定・自動生成なし)
└── PolicyDrivenSingleton.asmdef # Runtime asmdef
- Play中はメインスレッド前提(Unity APIを呼ぶため)
- 厳密な型一致:派生型など
Tと一致しない参照は拒否 - SceneSingleton はシーン配置必須(自動生成しない)
- DEVでは重複を全探索で検出(キャッシュ未確立時に fail-fast)
- Inactive/Disabled運用は避ける(隠れ重複の原因)
- 終了中の復活を避ける:終了経路は
TryGetInstanceを使う(Application.quittingはベストエフォート)
- 具象クラスは
sealed推奨(型不一致/拡張の事故を避ける) Awake/OnEnable/OnDestroyを override する場合は base 呼び出し必須- 頻繁アクセスする参照はキャッシュする(Updateで
Instanceを叩かない) - GlobalSingleton は root GameObject 推奨:
DontDestroyOnLoadは root にのみ有効。子オブジェクトの場合、本ライブラリが自動で root へ移動し Warning を出力
Domain Reload OFF 環境では static 状態が残ります。Awake は生存期間中1回のため、Playごとの再初期化は OnPlaySessionStart() で行ってください。
OnPlaySessionStart()は 冪等に書く(イベント購読は「解除 → 登録」など)
初期化順序を厳密に制御したい場合は DefaultExecutionOrder や Bootstrap で固定してください。
using UnityEngine;
using PolicyDrivenSingleton;
[DefaultExecutionOrder(-10000)]
public class Bootstrap : MonoBehaviour
{
void Awake()
{
_ = GameManager.Instance;
_ = AudioManager.Instance;
_ = InputManager.Instance;
}
}Unity API前提(要点)
- Domain Reload 無効:static フィールドと static event の購読が Play 間で残る
- Find系:既定で inactive は除外される/呼び出しごとに同一オブジェクトが返る保証はない
- DontDestroyOnLoad:root GameObject(またはroot上のComponent)に対してのみ有効
- Application.quitting:強制終了やクラッシュ等では発火しない場合がある/キャンセルできない局面で発火する
| 制限事項 | 説明 | 回避策 |
|---|---|---|
| 静的コンストラクタのタイミング | シングルトンクラスに静的コンストラクタがあると PlaySessionId 初期化前に実行される可能性 |
静的コンストラクタを避ける、または遅延初期化パターンを使用 |
| スレッドセーフティ | すべての操作はメインスレッドから呼び出す必要がある | バックグラウンド処理の結果は UnityMainThreadDispatcher 等でメインスレッドに戻す |
| シーン読み込み順序 | 複数シーンに同一シングルトン型がある場合、破棄順序は Unity のシーン読み込み順序に依存 | シングルトンは 1 シーンにのみ配置する |
| メモリリーク(Domain Reload OFF) | OnDestroy で静的イベント購読を解除しないとリークする |
OnPlaySessionStart で「解除 → 登録」パターンを使う |
| Find API の非決定性 | FindAnyObjectByType は呼び出しごとに同一オブジェクトを返す保証がない |
本ライブラリはキャッシュで吸収済み(利用側は意識不要) |
| Inactive の検出漏れ | FindAnyObjectByType(Exclude) は非アクティブを見ない |
シングルトンは常に Active にする。DEV では fail-fast で検出 |
PlayMode / EditMode テスト同梱(合計 79 テスト:PlayMode 58 / EditMode 21)
実行方法:Window → General → Test Runner → Run All
補足:
- EditMode テスト用のテストダブルは
Tests/Shared/に配置(Editor フォルダ外で AddComponent 可能にするため) - EditMode / PlayMode ともに機能別フォルダ + 1 TestFixture/1 ファイルで整理
- Runtime の
SetUpFixture(PolicySingletonTestSetup)でログカウンタを初期化(PolicyDrivenSingleton.Tests.Runtime配下に適用) - テスト数は増減するため、Test Runner の実行結果に合わせて更新
テストカバレッジ詳細
| カテゴリ | テスト数 | カバレッジ |
|---|---|---|
| GlobalSingleton | 7 | 自動生成、キャッシュ、重複検出 |
| SceneSingleton | 5 | 配置、自動生成なし、重複検出 |
| InactiveInstance | 3 | 非アクティブGameObject検出、無効コンポーネント |
| TypeMismatch | 2 | 派生クラス拒否 |
| ThreadSafety | 7 | バックグラウンドスレッド保護、メインスレッド検証 |
| TryPostToMainThread | 5 | メインスレッド実行、バックグラウンドPost、null処理 |
| Lifecycle | 2 | 破棄、再生成 |
| SoftReset | 1 | PlaySessionId 境界での Playごとの再初期化 |
| SceneSingletonEdgeCase | 2 | 未配置、自動生成なし |
| PracticalUsage | 6 | GameManager、LevelController、状態管理 |
| PolicyBehavior | 3 | ポリシー駆動挙動検証 |
| ResourceManagement | 3 | インスタンスライフサイクルとクリーンアップ |
| DomainReload | 6 | PlaySessionId境界、キャッシュ無効化、終了状態 |
| ParentHierarchy | 2 | DontDestroyOnLoad用のルート再配置 |
| BaseAwakeEnforcement | 1 | base.Awake() 呼び出し検出 |
| EdgeCase | 3 | 破棄インスタンスクリーンアップ、高速アクセス、配置タイミング |
| カテゴリ | テスト数 | カバレッジ |
|---|---|---|
| SingletonRuntimeEditMode | 2 | PlaySessionId、IsQuitting 検証 |
| Policy | 5 | Policy struct 検証、不変性、インターフェース準拠 |
| SingletonBehaviourEditMode | 5 | EditMode 挙動、キャッシュ分離 |
| SingletonLifecycleEditMode | 3 | 親階層、生成、Edit Modeでの共存 |
| SingletonRuntimeStateEditMode | 2 | NotifyQuitting、PlaySessionId一貫性 |
| SingletonLoggerEditMode | 4 | Log、LogWarning、LogError、ThrowInvalidOperation API |
ライブラリは以下のシンボルのいずれかが定義されている場合にデバッグログを出力します。それ以外では [Conditional] によりストリップされます。
UNITY_EDITORDEVELOPMENT_BUILDUNITY_ASSERTIONS
| レベル | メッセージ | トリガー |
|---|---|---|
| Log | OnPlaySessionStart invoked. |
シングルトンのセッション初期化実行時 |
| Log | Instance access blocked: application is quitting. |
終了中に Instance アクセス |
| Log | TryGetInstance blocked: application is quitting. |
終了中に TryGetInstance アクセス |
| Warning | Auto-created. |
GlobalSingleton の自動生成 |
| Warning | Duplicate detected. Existing='...', destroying '...' |
重複シングルトンの検出・破棄 |
| Warning | Reparented to root for DontDestroyOnLoad. |
永続化のため子オブジェクトをルートへ移動 |
| Error | base.Awake() was not called in ... |
サブクラスで base.Awake() 呼び出し忘れ |
| Error | Type mismatch. Expected='...', Actual='...' |
型不一致検出(派生型など) |
| Error | ... must be called from the main thread. |
バックグラウンドスレッドからのアクセス |
// シングルトンの状態確認
if (MySingleton.TryGetInstance(out var instance))
{
Debug.Log($"Singleton found: {instance.name}");
}
else
{
Debug.LogWarning("Singleton not available");
}- コンポーネントが Active & Enabled か?
- Play中に メインスレッド から呼んでいるか?
Awakeoverride 時にbase.Awake()を呼んでいるか?- SceneSingleton をシーンに置き忘れていないか?
FAQ
Q. Play Modeで Instance が null を返す
A. Active/Enabled、メインスレッド、base呼び出し、終了中ガードのいずれかを確認してください。
Q. 重複が検出される / 破棄される
A. 複数シーン・プレハブに同一型が混在している可能性があります。配置を整理してください。
Q. 例外が出る環境と出ない環境がある
A. DEV/EDITOR/ASSERTIONS の fail-fast と、Playerの fail-soft の差です。解除・後片付けは TryGetInstance を使ってください。
| トピック | リンク |
|---|---|
| GitHub Docs: Creating Mermaid diagrams | docs.github.com |
| Unity Manual: Domain Reloading | docs.unity3d.com |
| Unity API: Object.FindAnyObjectByType | docs.unity3d.com |
| Unity API: FindObjectsInactive | docs.unity3d.com |
| Unity API: Object.DontDestroyOnLoad | docs.unity3d.com |
| Unity API: Application.quitting | docs.unity3d.com |
| Unity API: DefaultExecutionOrder | docs.unity3d.com |
| Microsoft Docs: ConditionalAttribute | learn.microsoft.com |
MIT License. See LICENSE.