시간
Gameplay·UI·Background 세 개의 독립 클럭 위에서 흐르는 일시정지 가능한 게임 시간 — 클럭마다 자체 속도 제어 제공.
세 개의 독립 클럭 — Gameplay, UI, Background · 클럭별 타임 스케일과 중첩 안전 일시정지 · No-op 대체 구현:
NullTimeService
CGS는 고전적인 일시정지 메뉴 버그 — 음악이 끊기고, UI가 얼어붙고, 메뉴가 열려 있는데 게임플레이가 뒤에서 계속 돌아가는 문제 — 를 **ITimeService**로 방지합니다. Unity의 전역 Time.timeScale은 모든 것을 한꺼번에 멈춥니다. Time 서비스는 대신 세 개의 독립 클럭을 돌립니다:
| 클럭 | 대표 용도 | 일시정지 시 |
|---|---|---|
Clock.Gameplay | 이동, AI, 전투, 물리 인접 로직 | 메뉴가 열리면 이 클럭을 일시정지 |
Clock.UI | 메뉴·HUD 애니메이션 | 계속 흐르므로 메뉴 애니메이션 유지 |
Clock.Background | 음악, 앰비언트 오디오 | 계속 흐르므로 음악이 끊기지 않음 |
Time.deltaTime 대신 _time.DeltaTime(Clock.Gameplay)으로 시간을 읽으세요. _time.SetTimeScale(Clock.Gameplay, 0.3f)로 클럭 하나만 늦추면 시네마틱 슬로 모션이 됩니다 — UI와 음악은 원래 속도를 유지합니다. 일시정지는 안전하게 중첩됩니다: 세 시스템이 한 클럭을 일시정지하면, 셋 모두 재개할 때까지 클럭은 멈춰 있습니다.
서비스 가져오기
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를 살려 둔 채 계속 틱합니다: 세션이 끝날 때까지 사라지지 않는 두 번째 티커가 Hierarchy에 남습니다. 플레이어 빌드에는 이를 정리해 줄 teardown 단계도 없습니다.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 — 에 같은 규칙이 적용됩니다.
흔한 함정
-
메인 스레드 전용. Debug 빌드에서는 워커 스레드(
Task.Run, 스레드 풀 등)에서 Time 서비스를 호출하면InvalidOperationException을 던집니다. 메인 스레드에서 리졸브하고 인스턴스를 캐시하세요. -
Update()에서 쓰는 곳은 인스턴스를 캐시하세요.ServiceLocator.Resolve<T>()는 빠른 딕셔너리 조회지만 공짜는 아닙니다. 매 프레임 시간을 읽는다면Awake()에서_time을 캐시하세요. -
중첩 일시정지는 카운터 기반입니다.
Pause(Clock.Gameplay)를 두 번 호출했다면Resume()도 두 번 호출해야 합니다. 이것이 겹치는 일시정지 소스들이 서로를 밟지 않게 하는 장치입니다 — 다만 짝이 맞지 않는Resume()은 Debug 빌드에서 예외를 던지므로, 짝을 맞추세요. -
UnityEngine.Time.timeScale을 건드리지 마세요. Time 서비스는 Unity의 전역 타임 스케일이 1.0으로 유지된다고 가정하고Time.unscaledDeltaTime을 기준선으로 사용합니다.Time.timeScale을 직접 설정하면 클럭별 분리가 깨집니다. 전역 슬로 모션에는SetGlobalTimeScale()을 사용하세요. -
UnscaledTime은 항상 앞으로만 갑니다. 일시정지와 타임 스케일을 완전히 무시하며, 도메인 리로드 시에만 리셋됩니다. 경과 시간, 로딩 화면 타임아웃, 속도 제한 등 게임 시간이 측정에 영향을 주면 안 되는 곳에 사용하세요. 값은 모든 클럭에서 동일하며, 파라미터는 가독성을 위해 존재합니다.
관련 페이지
- Service Locator — 서비스 리졸브와 교체
- Scheduler — 이 클럭들을 따르는 타이머
- Tween — 일시정지 인식 값 애니메이션
- Tween Sequencing — 클럭 위에서의 트윈 체이닝