5. コアコンセプト
すべての CGS サービスの背後にある 5 つの考え方 — ブートストラップ、サービスロケーター、クロック、イベント、Null 実装 — と用語集。
フレームワーク全体は 5 つの考え方で説明できます。これらを知れば、23 のサービスすべてが同じように動くことが分かります。この章では各アイデアを平易な言葉で、プロジェクトに貼り付けられる短いコードとともに扱います。章末の用語集で、このマニュアルで使うすべての技術用語を定義します。
5.1 ブートストラップ: すべては自動で始まる
Common Game System には、シーンにドラッグするプレハブも、マネージャーオブジェクトも、初期化呼び出しもありません。Play を押すと、最初のシーンがロードされる前に、Unity がフレームワークの起動コードを自動で実行します。この起動ステップをブートストラップと呼びます。ブートストラップは 23 のサービスすべてを正しい順序で生成し、あなたのコードが見つけられるように 1 つずつ登録します。
正しく動作したことは、Play 後に Console を見れば確認できます。起動の最後の行にはこう書かれています:
bootstrap complete (v2.1.0, 23 services)
ブートストラップをあなたが呼ぶことは決してありません。Play セッションごとに正確に 1 回実行され、2 回目の呼び出しは意図的にエラーを投げます。あなたのシーンのどの Awake() が走る頃には、すべてのサービスがすでに生きて待機しています。完全なリファレンス: Bootstrap。
5.2 サービスロケーター: 一度だけ尋ねて、答えを持ち続ける
サービスとは、1 つの自己完結した機能 — セーブ、オーディオ、タイマー、入力 — で、単一の C# インターフェイスを通じてアクセスできるものです。サービスロケーターは、それらすべてを収めた電話帳です。インターフェイスを指定してサービスを尋ねると、実行中のインスタンスが返ってきます:
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)] // run after the framework has started
public class GameSetup : MonoBehaviour
{
private ITimeService _time;
private ISaveService _save;
private void Awake()
{
// Ask once, keep the answer.
_time = ServiceLocator.Resolve<ITimeService>();
_save = ServiceLocator.Resolve<ISaveService>();
}
}
これを高速かつ安全に保つ習慣は 2 つです:
Awake()かStart()で一度だけ解決し、結果をフィールドに保存する。 各ルックアップは辞書検索です。毎フレーム呼ぶのは、何の得もなく時間を浪費するだけです。Awake()でサービスを解決するクラスには[DefaultExecutionOrder(100)]をつける。 これにより Unity はあなたのスクリプトをフレームワーク自身のオブジェクトの後に実行するので、すべてのサービスが準備済みになります。
サービスが存在しない可能性がある場合 — 例えば任意の Unity パッケージを削除した場合 — は、代わりに ServiceLocator.TryResolve<T>(out var service) を使ってください。例外を投げる代わりに false を返します。完全なリファレンス: Service Locator。

Service Debugger ウィンドウ(Tools > Common Game System > Service Debugger)は、プレイ中に登録済みのすべてのサービスを一覧します。期待するサービスがここにない場合、その任意 Unity パッケージが未インストールの可能性が高いです — 第 7.2 章を参照してください。表示される数は 23 より多いことがあります — フレームワークは 23 の公開サービスに加えて、いくつかの内部ヘルパーを登録するためです。
5.3 クロック: 3 つの速度で流れる時間
クロックとは、独立した時間の流れです。フレームワークは 3 つを動かしています: Clock.Gameplay、Clock.UI、Clock.Background です。それぞれは、他の 2 つに触れることなくポーズやスローダウンができます。これは古典的なバグを解決します: ゲームをポーズしたら、メニューのアニメーションまで凍りつき、音楽も止まってしまう、というあれです。
- Gameplay — あなたのワールド: キャラクター、物理反応、クールダウン。ポーズメニューはこのクロックを止めます。
- UI — メニューとオーバーレイ。ゲームプレイのポーズ中も動き続けるので、ボタンはアニメーションし続けます。
- Background — 音楽など、決して止まってはいけないもの。
ポーズメニューは 2 行になります:
using CommonGameSystem.Core;
using UnityEngine;
[DefaultExecutionOrder(100)]
public class PauseMenu : MonoBehaviour
{
private ITimeService _time;
private void Awake() => _time = ServiceLocator.Resolve<ITimeService>();
public void Open() => _time.Pause(Clock.Gameplay); // world freezes
public void Close() => _time.Resume(Clock.Gameplay); // world resumes
}
時間は Unity の Time.deltaTime の代わりに、クロック単位で _time.DeltaTime(Clock.Gameplay) で読み取ります。スローモーションは 1 回の呼び出しです: _time.SetTimeScale(Clock.Gameplay, 0.3f) — UI と音楽は通常速度のままです。スケジューラーのタイマーとトゥイーンサービスのアニメーションはそれぞれクロックにバインドされるので、追加コードなしで正しくポーズ・スローダウンします。ルールは 1 つ: Unity のグローバルな Time.timeScale を自分で設定しないこと。クロック単位のシステムと衝突してしまいます。完全なリファレンス: Time。
5.4 イベント: 購読し、ハンドルを持ち続け、破棄する
イベントバスを使うと、システム同士が互いを知らずに対話できます。あるスクリプトがイベントオブジェクトをパブリッシュすると、そのイベント型のすべてのサブスクライバーが、購読した順に即座に呼び出されます。購読すると小さなハンドルが返ります。そのハンドルを破棄すると購読解除になります。破棄を忘れると、ハンドラーは永久に登録されたままです — それはメモリリークです。
using System;
using CommonGameSystem.Core;
using UnityEngine;
// Events are plain classes you define yourself.
public class ScoreChanged
{
public int NewScore;
}
[DefaultExecutionOrder(100)]
public class ScoreLabel : MonoBehaviour
{
private IEventBus _events;
private IDisposable _subscription;
private void Awake() => _events = ServiceLocator.Resolve<IEventBus>();
private void OnEnable() => _subscription = _events.Subscribe<ScoreChanged>(OnScore);
private void OnDisable() => _subscription?.Dispose(); // always unsubscribe
private void OnScore(ScoreChanged e) => Debug.Log($"Score: {e.NewScore}");
}
パブリッシュはどこからでも 1 行です: _events.Publish(new ScoreChanged { NewScore = 100 });。知っておくべき詳細が 2 つあります: イベント型はクラスでなければなりません(構造体は不可)。そして、例外を投げたサブスクライバーが他のサブスクライバーをブロックすることはありません — エラーはログに記録され、配信は続行されます。完全なリファレンス: Event Bus。
5.5 Null 実装: すべてのサービスにオフスイッチがある
すべてのサービスには、対応する Null 実装が同梱されています — あらゆる呼び出しを受け入れて何もしないバージョンです。これを登録すると、呼び出し側を 1 つも変更することなく、そのサブシステムをオフにできます。全体のオーディオをどこでもミュートするのは 1 行です:
ServiceLocator.Replace<IAudioService>(new NullAudio());
プロジェクト内のすべての PlayMusic と PlaySfx 呼び出しは、変わらずコンパイルされ、変わらず実行されます — ただ何もしないだけです。テスト、ヘッドレスサーバー、ボイス収録セッション、あるいはサブシステムを独自ソリューションに置き換えるときに便利です。第 7.4 章に 23 サービスすべてのオフスイッチの一覧と、セッション中に特定のサービスを差し替える際に追加すべきクリーンアップの 1 行が載っています。
5.6 用語集
| 用語 | 意味 |
|---|---|
| サービス | 自己完結したフレームワーク機能(セーブ、オーディオ、タイマー)の 1 つ。単一の C# インターフェイスを通じて使用します。 |
| Resolve(解決) | サービスロケーターに、インターフェイスを指定してサービスを尋ねること。一度だけ行い、参照をフィールドに保持します。 |
| ブートストラップ | 最初のシーンがロードされる前に、23 のサービスすべてを生成・登録する自動起動ステップ。あなたが呼ぶことはありません。 |
| クロック | 3 つの独立した時間の流れ(Gameplay、UI、Background)の 1 つ。それぞれ個別にポーズやスローダウンができます。 |
| トゥイーン | 値を開始点から終了点まで、指定した時間をかけて滑らかに動かす短いアニメーション。形はイージングカーブで決まります。 |
| アクションマップ | Unity Input System アセット内の、名前つき入力アクションのグループ("Gameplay" や "Menu" など)。有効化されたマップだけがプレイヤーに反応します。 |
| Addressables | 直接参照の代わりにテキストのアドレスで、アセットをオンデマンドにロードするための Unity の任意パッケージ。 |
| IL2CPP | C# をネイティブコードに変換する Unity のビルドモード。未使用に見えるクラスを除去するため、セーブ用クラスには link.xml のエントリが必要です(第 6.3 章)。 |