タイム

Gameplay・UI・Background の 3 つの独立したクロック上で動く、ポーズ可能なゲーム時間。各クロックが独自の速度制御を持ちます。

Gameplay・UI・Background の 3 つの独立したクロック · クロックごとのタイムスケールとネスト安全なポーズ · no-op 代替: NullTimeService

CGS は、ポーズメニューの定番バグ — 音楽が途切れる、UI が固まる、開いたメニューの裏でゲームプレイが動き続ける — を ITimeService で防ぎます。Unity のグローバルな Time.timeScaleすべてを一度に止めてしまいます。Time サービスは代わりに 3 つの独立したクロックを動かします:

クロック主な用途ポーズ時
Clock.Gameplay移動、AI、戦闘、物理関連ロジックメニューを開いたらこれをポーズする
Clock.UIメニューと HUD のアニメーション流れ続けるので、メニューはアニメーションし続ける
Clock.Background音楽、環境音流れ続けるので、音楽が途切れない

時間は Time.deltaTime の代わりに _time.DeltaTime(Clock.Gameplay) で読み取ります。_time.SetTimeScale(Clock.Gameplay, 0.3f) で 1 つのクロックだけを遅くすれば、シネマティックなスローモーションになります — UI と音楽は本来の速度を保ちます。ポーズは安全にネストします。3 つのシステムがクロックをポーズしたら、3 つすべてが再開するまでポーズされたままです。

サービスの取得

Awake() または Start() で解決し、参照をキャッシュします。Update() 内で解決してはいけません — 毎フレームの解決は無駄な処理です。

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class MyGameplay : MonoBehaviour
{
    private ITimeService _time;

    private void Awake()
    {
        _time = ServiceLocator.Resolve<ITimeService>();
    }

    private void Update()
    {
        float dt = _time.DeltaTime(Clock.Gameplay);
        // Use dt for movement, AI, and other game logic.
    }
}

API リファレンス

時間の読み取り(すべて O(1)、メインスレッド専用)

float DeltaTime(Clock clock)         // Scaled, pause-aware frame delta; 0 while paused
float UnscaledDeltaTime(Clock clock) // Raw frame delta; ignores pause and time scale
float FixedDeltaTime(Clock clock)    // Fixed-step delta, scaled and pause-aware
double Time(Clock clock)             // Accumulated game time (double precision;
                                     // affected by pause and time scale)
double UnscaledTime(Clock clock)     // Accumulated real time; always increases.
                                     // Use for elapsed-time measurement and timeouts.

タイムスケール制御

タイムスケールは速度の倍率です。1.0 が通常速度、0.5 が半分の速度で、0 はポーズと同様に振る舞います。

float GetTimeScale(Clock clock)             // The clock's current scale
void SetTimeScale(Clock clock, float scale) // Set one clock's scale (0 allowed)
void SetGlobalTimeScale(float scale)        // Set all three clocks at once

ポーズ制御(カウンター式、ネスト安全)

void Pause(Clock clock)        // Increment the clock's pause counter
void Resume(Clock clock)       // Decrement it; going below zero throws
bool IsPaused(Clock clock)     // true while the counter is above 0
int GetPauseCount(Clock clock) // The current counter value (diagnostic)
void PauseAll()                // Pause all three clocks
void ResumeAll()               // Resume all three; clocks already at zero are
                               // skipped, so this never throws

このカウンターが、重なり合うポーズ元を安全にします。ポーズメニュー、カットシーン、フォーカス喪失時のポーズハンドラーが互いを知らずにそれぞれ Pause(Clock.Gameplay) を呼んでも、最後のひとつが Resume を呼ぶまでゲームプレイは再開されません。

完全な例

ゲームプレイを凍結しつつ UI のアニメーションは続けるポーズメニュー:

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);
        gameObject.SetActive(true);
    }

    public void Close()
    {
        _time.Resume(Clock.Gameplay);
        gameObject.SetActive(false);
    }
}

ゲームプレイだけに適用されるシネマティックなスローモーション:

using CommonGameSystem.Core;
using UnityEngine;

[DefaultExecutionOrder(100)]
public class BossIntro : MonoBehaviour
{
    private ITimeService _time;

    private void Awake() => _time = ServiceLocator.Resolve<ITimeService>();

    public void BeginSlowMotion() => _time.SetTimeScale(Clock.Gameplay, 0.3f);

    public void EndSlowMotion() => _time.SetTimeScale(Clock.Gameplay, 1.0f);
}

クロックの分離はデモシーン Demo/MotionLab.unity で実際に確認できます。Gameplay タイムスケールのスライダーが SetTimeScale(Clock.Gameplay, value) でアニメーションギャラリーを減速・停止させる一方、Clock.UI 上のスピナーは影響を受けずに回り続けます。

無効化する

ServiceLocator.Replace<ITimeService>(new NullTimeService());

NullTimeService は Unity 自身の Time の値をそのまま通します。デルタと時間のクエリはすべて UnityEngine.Time の値を返し、ポーズとタイムスケールの呼び出しは何もしません。クロックの分離が不要なゲーム — プロトタイプや、Time.timeScale を直接管理するプロジェクト — で使ってください。

差し替えた古いインスタンスは Dispose する。 ServiceLocator.Replace<T> はレジストリのエントリーを入れ替えるだけで、それ以外は何もしません — 置き換えられたインスタンスへの参照をあなたがまだ保持しているかどうかを知りようがないため、Dispose はしません。TimeService[CGS] TimeService Ticker GameObject(Play モードでは DontDestroyOnLoad 指定)を所有しているため、差し替え後に Dispose されなかったインスタンスはその GameObject を生かし続け、tick も続けます。Hierarchy に、セッションが終わるまで決して消えない 2 つ目の ticker が残ることになります。プレイヤービルドには、これを片付けるティアダウン処理もありません。

var previous = ServiceLocator.Resolve<ITimeService>() as System.IDisposable;
ServiceLocator.Replace<ITimeService>(new NullTimeService());
previous?.Dispose();   // destroys the old [CGS] TimeService Ticker

Dispose は入れ替えのに行ってください。そうすれば、その間に Dispose 済みのインスタンスが解決されることはありません。同じルールは、内部に [CGS] … GameObject を所有するすべてのサービス — Object Pool、Audio、UI Framework、Scheduler、Tween、Tween Sequencing — に当てはまります。

よくある落とし穴

  • メインスレッド専用。 ワーカースレッド(Task.Run、スレッドプールなど)から Time サービスを呼ぶと、Debug ビルドでは InvalidOperationException がスローされます。メインスレッドで解決してインスタンスをキャッシュしてください。

  • Update() から使うならインスタンスをキャッシュする。 ServiceLocator.Resolve<T>() は高速なディクショナリ検索ですが、タダではありません。毎フレーム時間を読むなら、Awake()_time をキャッシュしてください。

  • ネストしたポーズはカウンター式。 Pause(Clock.Gameplay) を 2 回呼んだら、Resume() も 2 回必要です。これが重なり合うポーズ元同士の干渉を防ぐ仕組みですが、対応の取れていない Resume() は Debug ビルドで例外をスローします。ペアの対応を揃えてください。

  • UnityEngine.Time.timeScale に触らない。 Time サービスは Unity のグローバルタイムスケールが 1.0 のままであることを前提とし、Time.unscaledDeltaTime を基準に使います。自分で Time.timeScale を設定するとクロックごとの分離が壊れます。全体のスローモーションには代わりに SetGlobalTimeScale() を使ってください。

  • UnscaledTime は常に前へ進む。 ポーズとタイムスケールを完全に無視し、ドメインリロード時にのみリセットされます。経過時間、ロード画面のタイムアウト、レートリミットなど、ゲーム時間が計測に影響してはならない場面で使ってください。値はどのクロックでも同じで、パラメーターは可読性のために存在します。

関連ページ

  • Service Locator — サービスの解決と差し替え
  • Scheduler — これらのクロックに従うタイマー
  • Tween — ポーズ対応の値アニメーション
  • Tween Sequencing — クロック上でのトゥイーンのチェーン