タイム
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 TickerGameObject(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 TickerDispose は入れ替えの後に行ってください。そうすれば、その間に 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 — クロック上でのトゥイーンのチェーン