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:
| Clock | Typical use | On pause |
|---|---|---|
Clock.Gameplay | Movement, AI, combat, physics-adjacent logic | Pause this one when a menu opens |
Clock.UI | Menu and HUD animations | Keeps flowing, so menus still animate |
Clock.Background | Music, ambient audio | Keeps 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.TimeServiceowns a[CGS] TimeService TickerGameObject (markedDontDestroyOnLoadin 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 TickerDispose 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
InvalidOperationExceptionif 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_timeinAwake(). -
Nested pause is counter-based. Calling
Pause(Clock.Gameplay)twice requires twoResume()calls. This is what keeps overlapping pause sources from stepping on each other — but a mismatchedResume()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 usesTime.unscaledDeltaTimeas its baseline. SettingTime.timeScaleyourself breaks the per-clock separation. UseSetGlobalTimeScale()for global slow motion instead. -
UnscaledTimealways 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.
Related pages
- Service Locator — resolving and replacing services
- Scheduler — timers that follow these clocks
- Tween — pause-aware value animation
- Tween Sequencing — chaining tweens on a clock