Time

Pausable game time on three independent clocks — Gameplay, UI, and Background — each with its own speed control.

Three independent clocks — Gameplay, UI, Background · Per-clock time scale and nesting-safe pause · No-op replacement: NullTimeService

CGS prevents the classic pause-menu bugs — music cutting out, UI freezing, gameplay running behind an open menu — with ITimeService. Unity's global Time.timeScale stops everything at once. The Time service instead runs three independent clocks:

ClockTypical useOn pause
Clock.GameplayMovement, AI, combat, physics-adjacent logicPause this one when a menu opens
Clock.UIMenu and HUD animationsKeeps flowing, so menus still animate
Clock.BackgroundMusic, ambient audioKeeps flowing, so music never stutters

Read time with _time.DeltaTime(Clock.Gameplay) instead of Time.deltaTime. Slow one clock with _time.SetTimeScale(Clock.Gameplay, 0.3f) for cinematic slow motion — UI and music keep their natural speed. Pausing nests safely: if three systems pause a clock, it stays paused until all three resume it.

Getting the service

Resolve in Awake() or Start() and cache the reference. Do not resolve inside Update() — resolving every frame wastes work.

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 reference

Reading time (all O(1), main thread only)

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.

Time-scale control

A time scale is a speed multiplier: 1.0 is normal speed, 0.5 is half speed, 0 behaves like a pause.

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

Pause control (counter-based, safe to nest)

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

The counter is what makes overlapping pause sources safe: a pause menu, a cutscene, and a pause-on-focus-loss handler can each call Pause(Clock.Gameplay) without knowing about one another, and gameplay resumes only when the last one calls Resume.

Full example

A pause menu that freezes gameplay but keeps the UI animating:

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);
    }
}

Cinematic slow motion that applies to gameplay only:

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);
}

You can see the clock separation live in the demo scene, Demo/MotionLab.unity: its Gameplay time-scale slider slows and freezes the animation gallery with SetTimeScale(Clock.Gameplay, value) while a spinner on Clock.UI keeps turning, unaffected.

Turning it off

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

NullTimeService passes Unity's own Time values straight through. All delta and time queries return UnityEngine.Time values, and pause and time-scale calls do nothing. Use it if your game does not need separate clocks — prototypes, or projects that manage Time.timeScale directly.

Dispose the instance you replace. ServiceLocator.Replace<T> swaps the registry entry and nothing else — it does not dispose the instance it replaces, because it cannot know whether you still hold a reference to it. TimeService owns a [CGS] TimeService Ticker GameObject (marked DontDestroyOnLoad in Play mode), so a replaced-but-undisposed instance keeps that GameObject alive and keeps ticking: you get a second ticker in the Hierarchy that never goes away for the rest of the session. Player builds have no teardown pass to clean it up either.

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

Dispose after the swap, so nothing can resolve an already-disposed instance in between. The same rule applies to every service that owns an internal [CGS] … GameObject — Object Pool, Audio, UI Framework, Scheduler, Tween, and Tween Sequencing.

Common pitfalls

  • Main thread only. Debug builds throw InvalidOperationException if you call the Time service from a worker thread (Task.Run, the thread pool, and so on). Resolve on the main thread and cache the instance.

  • Cache the instance in Update() callers. ServiceLocator.Resolve<T>() is a fast dictionary lookup, but it is not free. If you read time every frame, cache _time in Awake().

  • Nested pause is counter-based. Calling Pause(Clock.Gameplay) twice requires two Resume() calls. This is what keeps overlapping pause sources from stepping on each other — but a mismatched Resume() throws an exception in Debug builds, so keep your pairs balanced.

  • Do not touch UnityEngine.Time.timeScale. The Time service assumes Unity's global time scale stays at 1.0 and uses Time.unscaledDeltaTime as its baseline. Setting Time.timeScale yourself breaks the per-clock separation. Use SetGlobalTimeScale() for global slow motion instead.

  • UnscaledTime always moves forward. It ignores pause and time scale entirely and only resets on a domain reload. Use it for elapsed time, loading-screen timeouts, and rate limits — anywhere game time should not affect the measurement. The value is the same for every clock; the parameter exists for readability.