Time

37 min read

Pause-aware time with layered timescale modifiers and multi-clock support, giving you the substrate behind bullet-time, hit-stop, in-game calendars, and deterministic replays.

Summary

ScyllaTime is the framework’s pause-aware time source. It is created during ScyllaBootstrap.Awake and is the canonical IClockSource for every gameplay subsystem that needs to know about time, pause, slow-motion, or fast-forward. The singleton sits above one or more ScyllaClock instances; each clock has its own tick rate, calendar, optional pause behaviour, and modifier stack. The master clock optionally drives Unity’s Time.timeScale so animation, physics, and particle systems also respect the gameplay slow-motion. Read pause-aware time via ScyllaTime.Instance.DeltaTime (returns 0 when paused, scaled by every active modifier), and unscaled time via UnscaledDeltaTime (always running, suitable for UI animations that should ignore pause).

The pause model has three values: PauseMode.None, PauseMode.Hard, and PauseMode.Soft. Hard pause freezes every clock; soft pause keeps clocks that have opted out of the global pause via ClockDefinition.RespectsGlobalPause = false running. Your UI clock can use the soft variant so menus, network heartbeats, and async indicators keep updating while gameplay is paused. The Pause / Resume / TogglePause methods cover the common cases; Pause(PauseMode) selects the variant explicitly. RestoreTimeScaleOnResume (a configuration flag) controls whether the pre-pause time scale is restored on resume; turning it off is useful for “pause sets the scale to 1.0” patterns.

Time-scale changes flow through three paths. Direct set (SetTimeScale, SetNormalSpeed, SetSlowMotion, SetFastForward) updates the base scale immediately and persists. Transitions (TransitionTimeScale) ramp from the current scale to a target over a fixed number of real seconds with an optional easing function; the returned SmoothTimeScaleTransition exposes CurrentValue, IsComplete, and Cancel. Modifiers (PushModifier, PopModifier) layer time-scale changes on top of the base scale with a priority order and a duration; the stack uses TimeScaleBlendMode values (Override, Multiply, Add) to combine multiple active modifiers. Modifiers are the right shape for hit-stop (one-frame Override 0), slow-motion windows (multi-second Multiply 0.3), and stacking ability effects (each ability pushes its own modifier; the stack computes the combined effective scale per frame).

ScyllaTime also supports multiple independent clocks via RegisterClock(ClockDefinition). Each ScyllaClock has its own CalendarDefinition (custom months, seasons, weekdays, leap-year rules), its own tick rate (BaseTickRate, SpeedMultiplier), its own pause behaviour, and its own scheduling queue. The framework publishes typed events on every calendar boundary (ClockSecondChangedEvent, ClockMinuteChangedEvent, ClockHourChangedEvent, ClockDayChangedEvent, ClockWeekdayChangedEvent, ClockMonthChangedEvent, ClockYearChangedEvent, ClockSeasonChangedEvent, ClockEraChangedEvent); subscribe via the static ScyllaEvents.Listen<T> facade (see Event System). Each clock also exposes a fluent scheduler via extension methods (RunAfter, RunEvery, RunAtDate, WaitUntil) that survives time-scale changes and pause without drift.

Use ScyllaTime for:

  • Any gameplay code that should respect pause (movement, AI ticks, damage timers, ability cooldowns).
  • Slow-motion, fast-forward, hit-stop, and other time-distortion effects (via the time-scale or the modifier stack).
  • Game-time clocks that advance independently of real time (a 24-hour in-game day in 8 real minutes).
  • Calendar-driven gameplay (seasons, day/night cycles, weekly events, era transitions).
  • Scheduled callbacks that fire after an in-game delay or on a recurring in-game schedule.
  • Replays and saves that need to capture and restore the entire time state.

Use UnityEngine.Time only for input timing, UI animations that should ignore game-time effects, and frame-budgeting code that needs the raw frame interval. Mixing the two in the same gameplay system produces “pause stops the player but not the timer” bugs.

Features

  • Pause-aware time source. ScyllaTime.Instance.DeltaTime returns 0 when paused and scales by every active modifier. UnscaledDeltaTime always runs for UI animations and frame-budget code.
  • Three pause modes. Hard freezes every clock; Soft keeps clocks marked RespectsGlobalPause = false running (menus, network heartbeats); None is the steady state. Pause, Resume, TogglePause cover the common cases.
  • Optional Unity Time.timeScale integration. The master clock can drive Unity’s own time scale so animation, physics, and particle systems also respect gameplay slow-motion. One clock can be the master; the rest tick independently.
  • Three time-scale change paths. Direct (SetTimeScale, SetSlowMotion, SetFastForward); transitions (TransitionTimeScale over real seconds with optional easing); modifiers (a priority-ordered stack with blend modes and durations).
  • Modifier stack with three blend modes. Override replaces lower-priority entries, Multiply multiplies into the running scale, Add adds. Hit-stop, slow-motion windows, and stacking ability effects layer cleanly without manual scale arithmetic.
  • Convenience factories. TimeScaleModifier.HitStop(duration) and SlowMotion(scale, duration) produce the right priority/blend/duration combinations for the two most common cases.
  • Multiple independent clocks. RegisterClock(ClockDefinition) adds a clock with its own tick rate, calendar, pause behaviour, modifier stack, and scheduler. The right primitive for game-time clocks that advance faster than real time, or for separate UI / gameplay / network clocks.
  • Calendar-driven time. CalendarDefinition data assets define months, seasons, weekdays, eras, leap-year rules, and epoch offsets. GameDate is the calendar-aware point in time with Year, Month, Day, Hour, Minute, Second, WeekdayIndex, SeasonIndex, EraIndex.
  • Calendar boundary events. The framework publishes typed ClockSecondChangedEvent, ClockMinuteChanged, ClockHourChanged, ClockDayChanged, ClockWeekdayChanged, ClockMonthChanged, ClockYearChanged, ClockSeasonChanged, ClockEraChanged. Subscribe through ScyllaEvents.Listen<T> for time-driven gameplay (daily refreshes, season changes, sunrise events).
  • Per-clock fluent scheduler. RunAfter, RunEvery, RunAtDate, WaitUntil extension methods on ScyllaClock. Survives time-scale changes and pause without drift; returns ScheduledTickHandle for cancellation.
  • Smooth transitions. TransitionTimeScale ramps from the current scale to a target over a fixed number of real seconds with an optional easing. The returned SmoothTimeScaleTransition exposes CurrentValue, IsComplete, and Cancel.
  • Snapshot and restore. ScyllaTime.CaptureState() returns a MasterTimeState capturing every clock, every modifier, and the pause state. RestoreState applies it. The right primitive for replays, saves, and lockstep multiplayer.
  • ITimeListener polled-call interface. Alternative to event-bus subscription for simple state listeners. One method (OnTimeEvent(TimeEvent)) handles Paused, Resumed, TimeScaleChanged. Useful when a polled callback shape is more convenient than a managed subscription.

Concepts

A few terms appear repeatedly. Pause-aware means a value reflects whether the framework is paused: pausing zeroes DeltaTime but leaves UnscaledDeltaTime running. Time scale is the global multiplier applied to DeltaTime (and to Time.timeScale for the master clock). Modifier is a time-scale layer on the stack with a priority, a blend mode, and a duration. Clock is one ScyllaClock instance with its own rate, calendar, and scheduling queue. Master clock is the clock whose ClockDefinition.IsMasterClock = true; the framework allows one master at a time. Calendar is the data-driven rule set that maps tick counts onto years / months / days / hours / minutes / seconds.

ConceptTypeNotes
ScyllaTimesealed singletonThe framework-wide time source. Instance is the singleton; created during ScyllaBootstrap.Awake. Owns the master clock plus any additional clocks registered via RegisterClock. Implements IClockSource.
ScyllaClocksealed classOne independent clock with its own tick rate, calendar, scheduler, pause state, and modifier stack. Created by ScyllaTime.RegisterClock(ClockDefinition). Implements IClockSource.
ClockDefinitionScriptableObjectData asset describing a clock: ClockID, DisplayName, BaseTickRate, Calendar, IsMasterClock, RespectsGlobalPause. Bound to ScyllaTimeConfiguration.Clocks for startup registration; passable to RegisterClock at runtime.
PauseModeenumThree values: None, Hard (every clock freezes), Soft (clocks with RespectsGlobalPause = false keep ticking). Pause() defaults to Hard.
TimeScaleModifierstructOne entry on the modifier stack. Fields: ID, Priority, Scale, Mode (TimeScaleBlendMode), RemainingDuration, SourceID. Factories: Create, HitStop(duration), SlowMotion(scale, duration).
TimeScaleBlendModeenumThree values: Override (the modifier replaces every lower-priority entry), Multiply (multiplies into the running effective scale), Add (adds to the running effective scale).
TimeScaleModifierHandlereadonly structReturned by PushModifier. Pass to PopModifier to remove the modifier. IsValid reports whether the handle still references an active modifier.
SmoothTimeScaleTransitionsealed classReturned by TransitionTimeScale. CurrentValue, IsComplete, WasCancelled, Cancel(). The framework updates it every frame on UnscaledDeltaTime; you do not call Update directly.
GameDatereadonly structA point in game time, calendar-aware. Fields: Year, Month, Day, Hour, Minute, Second, Millisecond, WeekdayIndex, SeasonIndex, EraIndex, CalendarHash. Constructed by the framework; you read it but rarely construct it directly.
CalendarDefinitionScriptableObjectData asset defining the rule set of a calendar: months, seasons, weekdays, eras, leap-year rule, epoch offset, time-base ratios. Bound to a ClockDefinition.
ScheduleExtensionsstatic classExtension methods on ScyllaClock for scheduling: RunAfter(seconds), RunAfter(TimeUnit), RunEvery(seconds), RunEvery(TimeUnit), RunAtDate(GameDate), WaitUntil(predicate), Cancel(handle). Returns ScheduledTickHandle.
ClockEvent familyabstract ScyllaEvent plus subclassesPer-boundary events: ClockTickEvent, ClockSecondChangedEvent, ClockMinuteChangedEvent, ClockHourChangedEvent, ClockDayChangedEvent, ClockWeekdayChangedEvent, ClockMonthChangedEvent, ClockYearChangedEvent, ClockSeasonChangedEvent, ClockEraChangedEvent, ClockPausedEvent, ClockResumedEvent, ClockTimeScaleChangedEvent, ClockRegisteredEvent, ClockUnregisteredEvent.
ITimeListenerinterfacePolled-call listener that the framework invokes on TimeEvent values (Paused, Resumed, TimeScaleChanged). One method: OnTimeEvent(TimeEvent). Alternative to event-bus subscription for simple polled-state code.
MasterTimeStatesealed classFull snapshot of ScyllaTime state (every clock, every modifier, the pause state). Captured via ScyllaTime.CaptureState(); restored via RestoreState. Used by replays and saves.

The framework uses the master clock to optionally drive Unity’s own Time.timeScale so the engine’s animation, physics, and particle systems also slow / pause when gameplay does. Set ClockDefinition.IsMasterClock = true on at most one clock; that clock’s effective rate becomes the gameplay scale. Non-master clocks tick independently and do not affect Unity’s Time.timeScale.

Recipes

These recipes cover every public surface on ScyllaTime and ScyllaClock: time reading, pause, time-scale control, modifier stacks, clocks, calendars, the scheduler, snapshots, and event subscriptions. Each one is self-contained. Start with “Read pause-aware time” if you are new to the API; the others build naturally from there.

Read pause-aware time

Problem. You want gameplay code (movement, AI ticks, ability cooldowns, damage timers) to automatically freeze when the game is paused and slow down during bullet-time, without touching every system individually. UnityEngine.Time.deltaTime will not do it; you need the framework’s pause-aware version.

Solution. Read time through ScyllaTime.Instance. It exposes the same six time properties Unity does but routes every one of them through the framework’s pause and time-scale pipeline. The pause-aware properties (DeltaTime, TimeSinceStartup) return 0 when paused and are scaled by the global time scale plus any active modifiers. The unscaled properties (UnscaledDeltaTime, UnscaledTimeSinceStartup, RealTimeSinceStartup) ignore pause and time scale; use them for UI animations and tools that should keep running.

using Scylla.Core.Time;

/* Pause-aware deltas. Returns 0 when the framework is paused, scaled by
 * TimeScale and any active modifiers. Use for gameplay code. */
float dt   = ScyllaTime.Instance.DeltaTime;
float fixedDt = ScyllaTime.Instance.FixedDeltaTime;

/* Unscaled: ignore pause and scale. Use for UI animations and tooling
 * that should keep ticking through pause. */
float realDt = ScyllaTime.Instance.UnscaledDeltaTime;
float realT  = ScyllaTime.Instance.RealTimeSinceStartup;

/* Game time and frame count. */
float gameTime  = ScyllaTime.Instance.TimeSinceStartup;        /* scaled */
float realTimeT = ScyllaTime.Instance.UnscaledTimeSinceStartup; /* unscaled */
int   frame     = ScyllaTime.Instance.FrameCount;

/* Internal tick counter, useful for debug overlays and replay scrubbing. */
long ticks = ScyllaTime.Instance.CurrentTickMilliseconds;

/* Convert between scaled (game) and real (wall-clock) seconds. */
float gameSeconds = ScyllaTime.Instance.ConvertToScaledTime(realSeconds: 1.0f);
float realSeconds = ScyllaTime.Instance.ConvertToRealTime(scaledTime: 0.3f);

/* Read the current pause and scale state. */
bool      paused = ScyllaTime.Instance.IsPaused;
PauseMode mode   = ScyllaTime.Instance.CurrentPauseMode;
float     scale  = ScyllaTime.Instance.TimeScale;
float     eff    = ScyllaTime.Instance.EffectiveTimeScale; /* scale * modifiers */

Pause and resume gameplay

Problem. You want to freeze gameplay when the player opens the pause menu, but you need the menu’s own animations and any network heartbeats to keep running. A hard freeze of Time.timeScale = 0 is too blunt; it stops everything, and undoing it mid-session while keeping the framework’s state consistent is error-prone.

Solution. Call Pause() and Resume() on ScyllaTime.Instance. Sometimes the right pause is hard (the entire game freezes), and sometimes it is soft (gameplay freezes but menus and network heartbeats keep running). Pause() / Resume() / TogglePause() cover the common case (hard pause). Pause(PauseMode) selects the variant: Hard freezes every clock; Soft lets clocks with RespectsGlobalPause = false keep running. Inspect CurrentPauseMode to read the active mode; IsPaused is true for both Hard and Soft.

/* Default pause: PauseMode.Hard. Every clock freezes. */
ScyllaTime.Instance.Pause();

/* Soft pause: clocks with RespectsGlobalPause=false keep ticking. Useful
 * when the UI clock should advance during a gameplay pause (network
 * timers, animated menus, async indicators). */
ScyllaTime.Instance.Pause(PauseMode.Soft);

/* Resume. The framework restores TimeScale to its pre-pause value when
 * RestoreTimeScaleOnResume is true (the default). */
ScyllaTime.Instance.Resume();

/* Toggle: pauses when running, resumes when paused. Useful for a
 * single-button debug-pause toggle. */
ScyllaTime.Instance.TogglePause();

/* State inspection. */
bool      paused  = ScyllaTime.Instance.IsPaused;
PauseMode current = ScyllaTime.Instance.CurrentPauseMode; /* None, Hard, Soft */

if (current == PauseMode.Soft)
{
    /* Some clocks may still be ticking. */
}

Apply slow-motion, fast-forward, and smooth time transitions

Problem. There is a moment in almost every action game when slowing time changes the entire reading of a scene: a dodge, a critical hit, the camera lingering on a boss reveal. You want to set, transition, and restore the time scale cleanly – without doing the math yourself and without leaving the framework’s state inconsistent.

Solution. SetNormalSpeed, SetSlowMotion, SetFastForward, and SetTimeScale write directly to the base scale. The default slow-motion and fast-forward scales come from ScyllaTimeConfiguration (DefaultSlowMotionScale = 0.3, DefaultFastForwardScale = 2.0). TransitionTimeScale ramps from the current scale to a target over a number of real (unscaled) seconds with an optional easing function; the returned SmoothTimeScaleTransition lets you observe progress and cancel.

/* Read or set the base scale directly. The setter clamps to MaxTimeScale. */
ScyllaTime.Instance.TimeScale = 0.5f;

/* Or use the equivalent method. */
ScyllaTime.Instance.SetTimeScale(0.5f);

/* Presets that pull their values from ScyllaTimeConfiguration. The optional
 * scale argument overrides the configured default. */
ScyllaTime.Instance.SetNormalSpeed();        /* 1.0 */
ScyllaTime.Instance.SetSlowMotion();          /* uses DefaultSlowMotionScale (0.3) */
ScyllaTime.Instance.SetSlowMotion(scale: 0.1f); /* explicit override */
ScyllaTime.Instance.SetFastForward();         /* uses DefaultFastForwardScale (2.0) */
ScyllaTime.Instance.SetFastForward(scale: 4f); /* explicit override */

/* Smooth transition: ramp from the current scale to a target over a number
 * of real seconds. The easing parameter is a Func<float, float> from the
 * Ease class (see Tween) or a custom delegate. */
using Scylla.Core.Util.Tween;

SmoothTimeScaleTransition transition = ScyllaTime.Instance.TransitionTimeScale(
    targetScale: 0.3f,
    realSeconds: 0.5f,
    onComplete:  () => Log.Info("Slow motion engaged", LogCategory.Time),
    easing:      Ease.OutQuad /* custom easing function */
);

/* Observe progress and cancel. The framework updates the transition every
 * frame; you do not call Update directly. */
float currentScale = transition.CurrentValue;
bool  done         = transition.IsComplete;
transition.Cancel();
bool cancelled = transition.WasCancelled;

/* The transition fires onComplete when the target is reached (and the
 * transition's IsComplete flips to true). */

Stack hit-stop, slow-motion, and ability time effects

Problem. Say you are building a slow-motion-plus-hit-stop combo: the world freezes briefly on impact, then resumes at half speed, then snaps back to normal as the screen settles. A single TimeScale variable is not enough – these effects need to stack with priorities, and you need them to clean up automatically when they expire or when the source ability ends.

Solution. The modifier stack lets multiple time-scale changes coexist with priority, blend mode, and duration. PushModifier adds a modifier and returns a handle; PopModifier(handle) removes it; RemoveModifiersBySourceID(id) removes every modifier tagged with a specific source. The stack is recomputed every frame; EffectiveTimeScale is the resulting product. TimeScaleModifier has three named factories for common shapes (HitStop, SlowMotion, Create).

using Scylla.Core.Time;

/* Hit-stop: one-frame freeze. Override mode with priority 100 means it
 * dominates any lower-priority modifier; duration 0.05 means it expires
 * after 50 ms of real time. */
TimeScaleModifierHandle hitStop = ScyllaTime.Instance.PushModifier(
    TimeScaleModifier.HitStop(duration: 0.05f, sourceID: "ImpactHit")
);

/* Slow-motion window: Multiply mode at 0.3 for 2 seconds, priority 50. */
TimeScaleModifierHandle slowMo = ScyllaTime.Instance.PushModifier(
    TimeScaleModifier.SlowMotion(scale: 0.3f, duration: 2.0f, sourceID: "BulletTime")
);

/* Custom modifier via the Create factory. */
TimeScaleModifierHandle custom = ScyllaTime.Instance.PushModifier(
    TimeScaleModifier.Create(
        scale:     0.5f,
        mode:      TimeScaleBlendMode.Multiply,
        priority:  75,
        duration:  TimeScaleModifier.DURATION_INFINITE, /* until explicitly removed */
        sourceID:  "WadingThroughWater"
    )
);

/* Remove individually. */
bool removed = ScyllaTime.Instance.PopModifier(hitStop);

/* Remove every modifier tagged with a source ID. Useful when an entity
 * dies or an ability ends; one call clears every effect that entity owned. */
int count = ScyllaTime.Instance.RemoveModifiersBySourceID("WadingThroughWater");

/* Clear the entire stack. Resets the effective scale to just TimeScale. */
ScyllaTime.Instance.ClearModifiers();

/* Inspect the stack. */
float multiplier = ScyllaTime.Instance.GetEffectiveMultiplier();
IReadOnlyList<TimeScaleModifier> active = ScyllaTime.Instance.GetActiveModifiers();

foreach (var modifier in active)
{
    /* ID, Priority, Scale, Mode, RemainingDuration, SourceID. */
}

Register and query independent clocks

Problem. Most projects need only one clock, but the ones that need more (a UI clock that ignores pause, a game-time clock that runs faster than wall-clock for a passing-of-days mechanic, a per-save calendar that ticks differently in different runs) need them quite badly. You need to set up multiple clocks with separate tick rates and pause behaviours, and query them individually at runtime.

Solution. ScyllaTime supports multiple independent clocks via RegisterClock(ClockDefinition). The framework auto-registers any clocks bound to ScyllaTimeConfiguration.Clocks at startup; runtime registration is for clocks added after the fact (for example, a per-save calendar that loads with the save). One clock can be designated the master via ClockDefinition.IsMasterClock; the master clock’s effective rate drives Unity’s Time.timeScale.

using Scylla.Core.Time;
using Scylla.Core.Time.Calendar;

/* Register a clock at runtime. The ClockDefinition is a ScriptableObject
 * asset created via Assets > Create > Scylla > Time > Clock Definition.
 * Throws ArgumentException when ClockID is empty or when registering a
 * second master while one already exists. */
ScyllaClock gameClock = ScyllaTime.Instance.RegisterClock(gameClockDefinition);
ScyllaClock uiClock   = ScyllaTime.Instance.RegisterClock(uiClockDefinition);

/* Unregister at runtime. */
bool removed = ScyllaTime.Instance.UnregisterClock(gameClockDefinition.ClockID);

/* Look up a clock by ID. Returns null when missing. */
ScyllaClock found = ScyllaTime.Instance.GetClock("Game");

/* Enumerate every registered clock. */
IReadOnlyList<ScyllaClock> all = ScyllaTime.Instance.GetAllClocks();
int total = ScyllaTime.Instance.ClockCount;

/* Access the master clock. */
ScyllaClock master = ScyllaTime.Instance.MasterClock;

/* Inspect a clock's properties. */
string clockID    = gameClock.ClockID;
string displayName = gameClock.DisplayName;
float  rate        = gameClock.BaseTickRate;       /* ticks per second */
float  multiplier  = gameClock.SpeedMultiplier;    /* extra per-clock speed */
float  effective   = gameClock.EffectiveRate;      /* final tick rate */
bool   isMaster    = gameClock.IsMasterClock;
bool   reversed    = gameClock.IsReversed;
bool   ownPaused   = gameClock.IsPausedOwn;        /* clock-local pause */
bool   anyPaused   = gameClock.IsPaused;           /* clock-local or global */

/* Read the calendar-aware current date. */
GameDate now = gameClock.CurrentDate;
int year   = now.Year;
int month  = now.Month;
int day    = now.Day;
int hour   = now.Hour;
int minute = now.Minute;
int weekday = now.WeekdayIndex;
int season  = now.SeasonIndex;
int era     = now.EraIndex;

/* Per-clock controls. */
gameClock.Pause();
gameClock.Resume();
gameClock.TogglePause();
gameClock.SetSpeedMultiplier(2.0f);          /* this clock runs twice as fast */
gameClock.SetCurrentDate(new GameDate(/* ... */)); /* warp to a specific date */

Read and configure a custom in-game calendar

Problem. Your fantasy RPG has a 10-month calendar with a five-day week, two seasonal festivals, and an epoch that began 500 years before the story. The Gregorian calendar is the wrong shape for any of that, and you want the framework to compute CurrentDate correctly without you doing the tick-to-date math by hand.

Solution. Each ScyllaClock is paired with a CalendarDefinition (a ScriptableObject asset). The calendar describes the time-base ratios (hours per day, minutes per hour, seconds per minute, milliseconds per second), the list of months with day counts, the list of seasons with month spans, the list of weekdays, the list of eras with year boundaries, and a leap-year rule. The framework computes GameDate values from the clock’s tick count using the calendar’s rules; your code reads the resulting date and rarely touches the calendar directly.

/* Read the calendar paired with a clock. */
CalendarDefinition calendar = gameClock.Calendar;

/* Time-base ratios. */
int hoursPerDay      = calendar.HoursPerDay;        /* default 24 */
int minutesPerHour   = calendar.MinutesPerHour;     /* default 60 */
int secondsPerMinute = calendar.SecondsPerMinute;   /* default 60 */
int msPerSecond      = calendar.MillisecondsPerSecond; /* default 1000 */

/* Lists of structural definitions. */
IReadOnlyList<MonthDefinition>   months    = calendar.Months;
IReadOnlyList<SeasonDefinition>  seasons   = calendar.Seasons;
IReadOnlyList<WeekdayDefinition> weekdays  = calendar.Weekdays;
IReadOnlyList<EraDefinition>     eras      = calendar.Eras;

/* Year-shape helpers. */
int daysInJanuary = calendar.GetDaysInMonth(year: 2026, monthIndex: 0);
int daysInYear    = calendar.GetDaysInYear(year: 2026);

/* Epoch and leap-year rules. */
string  epochName  = calendar.EpochName;          /* e.g. "AD" */
int     epochYear  = calendar.EpochYearOffset;
LeapYearRule leap  = calendar.LeapRule;

Schedule callbacks on game-time, not wall-clock time

Problem. You want a poison effect to tick every three game-seconds, a quest objective to open at the next in-game sunrise, and a wandering merchant to arrive in the village every game-week. Invoke and coroutines with WaitForSeconds use real time; they do not pause when the game pauses and do not slow down during bullet-time.

Solution. Each ScyllaClock exposes a fluent scheduler via extension methods in ScheduleExtensions. RunAfter schedules a callback to fire after a delay (in game-time seconds or TimeUnit); RunEvery runs the callback on a recurring interval; RunAtDate fires when the clock’s CurrentDate reaches a specific GameDate; WaitUntil polls a predicate on every tick and fires the callback when it returns true. The returned ScheduledTickHandle cancels the schedule when passed to Cancel.

using Scylla.Core.Time;
using Scylla.Core.Time.Scheduling;
using Scylla.Core.Util.Units;

/* RunAfter: fire once after N game-time seconds. */
ScheduledTickHandle h1 = gameClock.RunAfter(
    scaledSeconds: 5.0f,
    callback:      () => Log.Info("Five game-seconds passed", LogCategory.Time)
);

/* RunAfter with a TimeUnit value. Uses the unit's millisecond conversion. */
ScheduledTickHandle h2 = gameClock.RunAfter(
    delay:    TimeUnit.OneSecond,
    callback: () => SpawnEnemy()
);

/* RunEvery: fire on a recurring interval. Each call returns a fresh handle;
 * the recurring schedule continues until Cancel is called. */
ScheduledTickHandle h3 = gameClock.RunEvery(
    scaledSeconds: 60.0f,
    callback:      () => SaveAutosave()
);

ScheduledTickHandle h4 = gameClock.RunEvery(
    interval:      TimeUnit.OneMinute,
    callback:      () => UpdateWeather()
);

/* RunAtDate: fire when the clock's CurrentDate reaches the target GameDate.
 * Useful for scripted events (the dragon attacks on Year 5, Month 6, Day 1). */
var attackDate = new GameDate(year: 5, month: 6, day: 1, hour: 0, /* ... */);
ScheduledTickHandle h5 = gameClock.RunAtDate(attackDate, () => DragonAttack());

/* WaitUntil: poll a predicate every tick; fire the callback when true. */
ScheduledTickHandle h6 = gameClock.WaitUntil(
    predicate: () => playerHealth < 25,
    callback:  () => TriggerLowHealthEvent()
);

/* Cancel a scheduled call. Returns true when the schedule was active and
 * has now been removed; false when the handle is invalid or already fired. */
bool cancelled = gameClock.Cancel(h1);

/* Or call CancelScheduled directly on the clock with the same handle. */
gameClock.CancelScheduled(h2);

/* Clear every scheduled call on the clock. Useful at scene transitions. */
gameClock.ClearScheduled();

/* Inspect pending count. */
int pending = gameClock.PendingScheduledCount;

Apply time effects to one clock without affecting others

Problem. You want bullet-time to slow the gameplay clock while UI animations and network ticks keep running at normal speed. Pushing a modifier onto the global ScyllaTime stack would slow every clock at once, which is not what you want.

Solution. Each ScyllaClock has its own modifier stack independent of the global ScyllaTime stack. Push a modifier onto the per-clock stack to slow or speed up that specific clock without affecting the rest of the framework. A slow-motion camera effect pushes onto the gameplay clock’s stack; the UI clock stays at normal speed. The per-clock methods mirror the global ones.

/* Push a modifier onto the per-clock stack. */
TimeScaleModifierHandle handle = gameClock.PushModifier(
    TimeScaleModifier.SlowMotion(scale: 0.5f, duration: 2.0f, sourceID: "FocusMode")
);

/* Pop, remove by source, or clear. Same shape as the global methods. */
gameClock.PopModifier(handle);
gameClock.RemoveModifiersBySourceID("FocusMode");
gameClock.ClearModifiers();

/* Per-clock listener registration. The clock fires events on calendar
 * boundaries; the listener receives every one. Alternative to event-bus
 * subscription for code that wants polled callbacks. */
gameClock.RegisterListener(myListener);
gameClock.UnregisterListener(myListener);

Snapshot and restore the full time state for saves and replays

Problem. You want replays and lockstep multiplayer to reproduce exactly: capture the full time state at frame N, restore it at frame N+M, and have everything match. That means every clock, every active modifier, and the current pause mode, not just the base time scale.

Solution. ScyllaTime.CaptureState() returns a MasterTimeState describing the entire time state: every clock, every modifier on every stack, the pause state, and the time-scale. RestoreState(state) applies the snapshot back; the framework reconstructs the clocks, the modifiers, and the pause state. Use for replays (capture, then deterministically replay) and saves (capture before save, restore after load).

using Scylla.Core.Time.State;

/* Snapshot the current state. */
MasterTimeState snapshot = ScyllaTime.Instance.CaptureState();

/* Serialize via Scylla's serialization system if persisting to disk. The
 * MasterTimeState carries every clock state and modifier; it is the right
 * thing to embed in a save file. */
string json = ScyllaSerialization.ToJSON(snapshot).Value;

/* Restore. The framework reconstructs the entire time state from the snapshot. */
ScyllaTime.Instance.RestoreState(snapshot);

/* Debug-info accessor for editor tooling. Returns a struct with the
 * snapshot fields plus per-clock debug info. */
ScyllaTimeDebugInfo debug = ScyllaTime.Instance.GetDebugInfo();
ClockDebugInfo[] perClock = /* allocated lazily */;
ScyllaTimeDebugInfo debug2 = ScyllaTime.Instance.GetDebugInfo(ref perClock);

React to pause and time-scale changes via ITimeListener

Problem. You have a HUD that needs to show a pause overlay when the game pauses and update a vignette when slow-motion kicks in. You want simple callbacks without wiring up the full event bus, because your HUD only cares about three transitions.

Solution. ITimeListener (the polled-callback interface) lets code react to TimeEvent values (Paused, Resumed, TimeScaleChanged) without using the Event System. Register via ScyllaTime.Instance.RegisterListener(listener); the framework invokes OnTimeEvent(TimeEvent) for each transition. Use the event bus (subscribe to ClockPausedEvent, ClockResumedEvent, ClockTimeScaleChangedEvent) when you need the typed payload; use ITimeListener for simple polled-state code.

using Scylla.Core.Time;

public sealed class PauseAwareHUD : MonoBehaviour, ITimeListener
{
    private void OnEnable()
    {
        ScyllaTime.Instance.RegisterListener(this);
    }

    private void OnDisable()
    {
        ScyllaTime.Instance.UnregisterListener(this);
    }

    /* The framework calls OnTimeEvent for every transition. The TimeEvent
     * enum has three values: Paused, Resumed, TimeScaleChanged. */
    public void OnTimeEvent(TimeEvent timeEvent)
    {
        switch (timeEvent)
        {
            case TimeEvent.Paused:
                ShowPauseOverlay();
                break;
            case TimeEvent.Resumed:
                HidePauseOverlay();
                break;
            case TimeEvent.TimeScaleChanged:
                UpdateSlowMotionVignette();
                break;
        }
    }

    private void ShowPauseOverlay()        { /* ... */ }
    private void HidePauseOverlay()        { /* ... */ }
    private void UpdateSlowMotionVignette() { /* ... */ }
}

Subscribe to calendar boundary events for time-driven gameplay

Problem. There is always a moment in time-driven gameplay when the clock crosses a calendar boundary: a new day begins, a season turns, a year ticks over. You want your shop inventory to refresh daily, your seasonal events to trigger on the correct month change, and your weather system to update on the hour – without polling CurrentDate every frame.

Solution. The framework publishes typed events on every such boundary. Subscribe via the static ScyllaEvents.Listen<T> facade (the correct entry point for the Event System). Each event carries ClockID, Before (the previous GameDate), After (the new date), IsReversing (true when the clock is running backwards), and Kind. The event names map directly onto the calendar boundaries: second, minute, hour, day, weekday, month, year, season, era.

using Scylla.Core.Events;
using Scylla.Core.Time.Events;

/* Subscribe to per-hour rollovers. The handler receives a typed event with
 * the previous and new GameDate values. */
ScyllaEvents.Listen<ClockHourChangedEvent>(
    subscriber: this,
    handler:    evt =>
    {
        Log.Info($"[{evt.ClockID}] Hour: {evt.Before.Hour:00} -> {evt.After.Hour:00}",
            LogCategory.Time);
    },
    priority: ScyllaEventPriority.Medium
);

/* Per-day rollover. Useful for "day passes" gameplay (new quests appear,
 * shop inventory refreshes, weather changes). */
ScyllaEvents.Listen<ClockDayChangedEvent>(
    subscriber: this,
    handler:    evt => StartNewDay(evt.After)
);

/* Season changes. The After.SeasonIndex carries the new season; index into
 * the calendar's Seasons list for the season definition. */
ScyllaEvents.Listen<ClockSeasonChangedEvent>(
    subscriber: this,
    handler:    evt =>
    {
        var season = gameClock.Calendar.Seasons[evt.After.SeasonIndex];
        ApplySeasonalGameplay(season);
    }
);

/* Per-clock lifecycle events. Fire when the clock pauses, resumes, or
 * changes its effective time scale. */
ScyllaEvents.Listen<ClockPausedEvent>      (this, evt => { /* ... */ });
ScyllaEvents.Listen<ClockResumedEvent>     (this, evt => { /* ... */ });
ScyllaEvents.Listen<ClockTimeScaleChangedEvent>(this, evt => { /* ... */ });

/* Clock registration / unregistration events. Useful when external code
 * needs to react to a clock being added or removed at runtime. */
ScyllaEvents.Listen<ClockRegisteredEvent>   (this, evt => { /* ... */ });
ScyllaEvents.Listen<ClockUnregisteredEvent> (this, evt => { /* ... */ });

/* Tick event: fires on every clock tick. High frequency; subscribe only
 * when per-tick processing is genuinely needed. */
ScyllaEvents.Listen<ClockTickEvent>(this, evt =>
{
    /* Runs every tick on every clock. Filter by evt.ClockID. */
});

API Sketch

The public surface for the time system spans the singleton, the clock, the modifier stack, the scheduler extensions, the calendar types, the events, and the configuration asset.

namespace Scylla.Core.Time
{
    /* The framework-wide singleton. */
    public sealed class ScyllaTime : IClockSource
    {
        public static ScyllaTime Instance { get; }

        /* Configuration and state. */
        public ScyllaTimeConfiguration Configuration { get; }
        public bool      IsPaused           { get; }
        public PauseMode CurrentPauseMode   { get; }
        public float     TimeScale          { get; set; }
        public float     EffectiveTimeScale { get; }

        /* Time properties (mirror UnityEngine.Time). */
        public float DeltaTime              { get; }
        public float UnscaledDeltaTime      { get; }
        public float FixedDeltaTime         { get; }
        public float TimeSinceStartup       { get; }
        public float UnscaledTimeSinceStartup { get; }
        public float RealTimeSinceStartup   { get; }
        public int   FrameCount             { get; }
        public long  CurrentTickMilliseconds { get; }

        /* Modifier stack and clock registry. */
        public TimeScaleModifierStack ModifierStack { get; }
        public ScyllaClock            MasterClock   { get; }
        public int                    ClockCount    { get; }

        /* Pause / resume. */
        public void Pause();
        public void Pause(PauseMode mode);
        public void Resume();
        public void TogglePause();

        /* Time-scale set and transitions. */
        public void                       SetNormalSpeed();
        public void                       SetSlowMotion (float scale = -1f);
        public void                       SetFastForward(float scale = -1f);
        public void                       SetTimeScale  (float scale);
        public SmoothTimeScaleTransition  TransitionTimeScale(float targetScale, float realSeconds,
            Action onComplete = null, Func<float, float> easing = null);

        /* Global modifier stack. */
        public TimeScaleModifierHandle PushModifier(TimeScaleModifier modifier);
        public bool                    PopModifier (TimeScaleModifierHandle handle);
        public int                     RemoveModifiersBySourceID(string sourceID);
        public void                    ClearModifiers();
        public float                   GetEffectiveMultiplier();
        public IReadOnlyList<TimeScaleModifier> GetActiveModifiers();

        /* Clock registration and queries. */
        public ScyllaClock RegisterClock  (ClockDefinition definition);
        public bool        UnregisterClock(string clockID);
        public ScyllaClock GetClock       (string clockID);
        public IReadOnlyList<ScyllaClock> GetAllClocks();

        /* Snapshot and restore. */
        public MasterTimeState CaptureState();
        public void            RestoreState(MasterTimeState state);

        /* Polled listeners. */
        public void RegisterListener  (ITimeListener listener);
        public void UnregisterListener(ITimeListener listener);

        /* Time conversion. */
        public float ConvertToScaledTime(float realTime);
        public float ConvertToRealTime  (float scaledTime);

        /* Diagnostic helpers. */
        public string             GetTimeInfo();
        public ScyllaTimeDebugInfo GetDebugInfo();
        public ScyllaTimeDebugInfo GetDebugInfo(ref ClockDebugInfo[] buffer);
    }

    /* Per-clock surface. */
    public sealed class ScyllaClock : IClockSource
    {
        public string             ClockID         { get; }
        public string             DisplayName     { get; }
        public CalendarDefinition Calendar        { get; }
        public ClockDefinition    Definition      { get; }
        public bool               IsMasterClock   { get; }
        public float              BaseTickRate    { get; }
        public float              SpeedMultiplier { get; }
        public float              EffectiveRate   { get; }
        public bool               IsReversed      { get; }
        public bool               IsPaused        { get; }
        public bool               IsPausedOwn     { get; }
        public PauseMode          CurrentPauseMode { get; }
        public float              UnscaledDeltaTime { get; }
        public float              TimeScale       { get; }
        public long               CurrentTickMilliseconds { get; }
        public GameDate           CurrentDate     { get; }
        public TimeScaleModifierStack ModifierStack { get; }
        public int                PendingScheduledCount { get; }

        /* Pause / resume per clock. */
        public void Pause();
        public void Resume();
        public void TogglePause();

        /* Per-clock listener registration. */
        public void RegisterListener  (IClockListener listener);
        public void UnregisterListener(IClockListener listener);

        /* Clock speed and date control. */
        public void SetSpeedMultiplier(float multiplier);
        public void SetCurrentDate    (GameDate date);

        /* Per-clock modifier stack. */
        public TimeScaleModifierHandle PushModifier(TimeScaleModifier modifier);
        public bool                    PopModifier (TimeScaleModifierHandle handle);
        public int                     RemoveModifiersBySourceID(string sourceID);
        public void                    ClearModifiers();

        /* Scheduled tick management. */
        public bool CancelScheduled(ScheduledTickHandle handle);
        public void ClearScheduled();
    }

    /* Three pause modes. */
    public enum PauseMode { None = 0, Hard = 1, Soft = 2 }

    /* Three blend modes for the modifier stack. */
    public enum TimeScaleBlendMode { Override = 0, Multiply = 1, Add = 2 }

    /* One modifier entry. */
    public struct TimeScaleModifier
    {
        public const float DURATION_INFINITE = -1f;
        public int    ID;
        public int    Priority;
        public float  Scale;
        public TimeScaleBlendMode Mode;
        public float  RemainingDuration;
        public string SourceID;

        public static TimeScaleModifier Create   (float scale, TimeScaleBlendMode mode = TimeScaleBlendMode.Multiply,
            int priority = 0, float duration = DURATION_INFINITE, string sourceID = null);
        public static TimeScaleModifier HitStop  (float duration, int priority = 100, string sourceID = null);
        public static TimeScaleModifier SlowMotion(float scale, float duration = DURATION_INFINITE,
            int priority = 50, string sourceID = null);
    }

    public readonly struct TimeScaleModifierHandle : IEquatable<TimeScaleModifierHandle>
    {
        public int  ID      { get; }
        public bool IsValid { get; }
    }

    public sealed class SmoothTimeScaleTransition
    {
        public float CurrentValue { get; }
        public bool  IsComplete   { get; }
        public bool  WasCancelled { get; }
        public void  Cancel();
    }

    /* Polled-callback listener interface. */
    public interface ITimeListener
    {
        void OnTimeEvent(TimeEvent timeEvent);
    }

    public enum TimeEvent { Paused, Resumed, TimeScaleChanged }
}

namespace Scylla.Core.Time.Scheduling
{
    /* Extension methods on ScyllaClock. */
    public static class ScheduleExtensions
    {
        public static ScheduledTickHandle RunAfter (this ScyllaClock clock, float scaledSeconds, Action callback);
        public static ScheduledTickHandle RunEvery (this ScyllaClock clock, float scaledSeconds, Action callback);
        public static ScheduledTickHandle RunAfter (this ScyllaClock clock, TimeUnit delay, Action callback);
        public static ScheduledTickHandle RunEvery (this ScyllaClock clock, TimeUnit interval, Action callback);
        public static ScheduledTickHandle RunAtDate(this ScyllaClock clock, GameDate date, Action callback);
        public static ScheduledTickHandle WaitUntil(this ScyllaClock clock, Func<bool> predicate, Action callback);
        public static bool                Cancel   (this ScyllaClock clock, ScheduledTickHandle handle);
    }
}

namespace Scylla.Core.Time.Calendar
{
    /* Calendar-aware date. */
    public readonly struct GameDate : IEquatable<GameDate>
    {
        public readonly int Year, Month, Day, Hour, Minute, Second, Millisecond;
        public readonly int WeekdayIndex, SeasonIndex, EraIndex;
        public readonly int CalendarHash;
    }

    /* Calendar definition asset. */
    public sealed class CalendarDefinition : ScriptableObject
    {
        public int   HoursPerDay           { get; }
        public int   MinutesPerHour        { get; }
        public int   SecondsPerMinute      { get; }
        public int   MillisecondsPerSecond { get; }
        public IReadOnlyList<MonthDefinition>   Months   { get; }
        public IReadOnlyList<SeasonDefinition>  Seasons  { get; }
        public IReadOnlyList<WeekdayDefinition> Weekdays { get; }
        public IReadOnlyList<EraDefinition>     Eras     { get; }
        public int  FirstWeekdayIndex      { get; }
        public string EpochName            { get; }
        public int    EpochYearOffset      { get; }
        public LeapYearRule LeapRule       { get; }

        public int GetDaysInMonth(int year, int monthIndex);
        public int GetDaysInYear (int year);
    }
}

namespace Scylla.Core.Time.Events
{
    /* Boundary events. */
    public abstract class ClockEvent : ScyllaEvent
    {
        public string         ClockID     { get; }
        public GameDate       Before      { get; }
        public GameDate       After       { get; }
        public bool           IsReversing { get; }
        public abstract ClockEventKind Kind { get; }
    }

    public sealed class ClockTickEvent              : ClockEvent { /* fires every tick */ }
    public sealed class ClockSecondChangedEvent     : ClockEvent { /* one per second */ }
    public sealed class ClockMinuteChangedEvent     : ClockEvent { /* one per minute */ }
    public sealed class ClockHourChangedEvent       : ClockEvent { /* one per hour */ }
    public sealed class ClockDayChangedEvent        : ClockEvent { /* one per day */ }
    public sealed class ClockWeekdayChangedEvent    : ClockEvent { /* one per weekday transition */ }
    public sealed class ClockMonthChangedEvent      : ClockEvent { /* one per month */ }
    public sealed class ClockYearChangedEvent       : ClockEvent { /* one per year */ }
    public sealed class ClockSeasonChangedEvent     : ClockEvent { /* one per season transition */ }
    public sealed class ClockEraChangedEvent        : ClockEvent { /* one per era transition */ }
    public sealed class ClockPausedEvent            : ClockEvent { /* one per pause */ }
    public sealed class ClockResumedEvent           : ClockEvent { /* one per resume */ }
    public sealed class ClockTimeScaleChangedEvent  : ClockEvent { /* one per scale change */ }
    public sealed class ClockRegisteredEvent        : ClockEvent { /* one per registration */ }
    public sealed class ClockUnregisteredEvent      : ClockEvent { /* one per unregistration */ }
}

namespace Scylla.Core.Config
{
    /* Configuration asset bound to ScyllaBootstrap.TimeConfiguration. */
    public sealed class ScyllaTimeConfiguration : ScyllaConfiguration
    {
        public const float MIN_TIME_SCALE     = 0f;
        public const float MAX_TIME_SCALE     = 10f;
        public const float DEFAULT_TIME_SCALE = 1f;

        public float InitialTimeScale          { get; }
        public float MaxTimeScale              { get; }
        public float DefaultSlowMotionScale    { get; }
        public float DefaultFastForwardScale   { get; }
        public bool  AllowPause                { get; }
        public bool  RestoreTimeScaleOnResume  { get; }
        public bool  AdjustFixedTimestep       { get; }
        public float BaseFixedTimestep         { get; }
        public IReadOnlyList<ClockDefinition> Clocks { get; }
        public bool  PauseOnApplicationFocusLoss { get; }
        public bool  ResumeOnFocusGain         { get; }
        public bool  PauseOnApplicationPause   { get; }
        public bool  ResumeOnApplicationResume { get; }
    }
}

See the API reference for full signatures, remarks, and exception contracts on every member.

Settings reference

ScyllaTimeConfiguration is bound to ScyllaBootstrap.TimeConfiguration. You edit it in the Inspector and override it at runtime via the JSON config-file system (see Configuration).

GroupPropertyTypeDefaultEffect
Time ScaleInitialTimeScalefloat1.0Starting time scale at framework Awake. Range 0..10.
Time ScaleMaxTimeScalefloat10.0Hard ceiling on the time scale. Setter clamps; values above are silently capped.
Time ScaleDefaultSlowMotionScalefloat0.3Used by SetSlowMotion() when no explicit scale is passed.
Time ScaleDefaultFastForwardScalefloat2.0Used by SetFastForward() when no explicit scale is passed.
PauseAllowPausebooltrueMaster switch for pause calls. When false, Pause() is a no-op.
PauseRestoreTimeScaleOnResumebooltrueWhether Resume() restores the pre-pause time scale (true) or leaves the post-pause scale in effect (false).
Fixed TimestepAdjustFixedTimestepbooltrueWhen true, Time.fixedDeltaTime is scaled by the master clock’s effective rate so physics also slows.
Fixed TimestepBaseFixedTimestepfloat0.02Reference value (in seconds) for the fixed timestep scaling computation.
ClocksClocksIReadOnlyList<ClockDefinition>emptyList of ClockDefinition assets registered automatically at framework startup. Add via the Inspector list.
FocusPauseOnApplicationFocusLossboolfalseAuto-pause when the Unity window loses focus.
FocusResumeOnFocusGainboolfalseAuto-resume when the Unity window regains focus.
FocusPauseOnApplicationPausebooltrueAuto-pause on OS-level pause (mobile sleep, alt-tab on console). Recommended true for shipped builds.
FocusResumeOnApplicationResumebooltrueAuto-resume on OS-level resume.

Best Practices

  • Use ScyllaTime.Instance.DeltaTime for gameplay code; reserve UnityEngine.Time.deltaTime for input timing. The framework’s deltas are pause-aware and respect the time-scale; Unity’s raw deltas are not.
  • Pause via the framework, not by setting Time.timeScale = 0. Setting Time.timeScale = 0 bypasses the framework’s pause path; the framework’s clocks and Unity engine state get out of sync. Use ScyllaTime.Instance.Pause().
  • Use the modifier stack for time-bounded effects, the TimeScale setter for global state changes. Modifiers expire on their own and stack with priorities; TimeScale persists until the next set.
  • Tag every modifier with a SourceID. When an effect ends, RemoveModifiersBySourceID(id) cleans up every modifier the effect pushed. Without a source ID, the only way to remove a modifier is by handle, and that requires storing the handle for the lifetime of the effect.
  • Use the per-clock modifier stack for system-local effects. A bullet-time effect that should slow gameplay but not the UI pushes onto the gameplay clock’s stack, not the global one. Each clock has its own stack; the global stack affects every clock at once.
  • Subscribe to boundary events for time-based gameplay. ClockHourChangedEvent and ClockDayChangedEvent are more efficient than polling CurrentDate.Hour every frame. The framework fires them exactly once per boundary; your handler runs once.
  • Use the scheduler for in-game delays. gameClock.RunAfter(5f, Method) respects pause and time scale; Invoke(nameof(Method), 5f) does not. The scheduler also handles time-scale changes mid-delay without drift.
  • Wire Pause to the game’s UI pause and Resume to the unpause event. Without this, gameplay time keeps running while the rest of the simulation is “paused”. The RestoreTimeScaleOnResume flag in Configuration controls whether the pre-pause scale is restored on resume.
  • Use multiple clocks when systems have independent time needs. A 24-hour game day in 8 real minutes is a separate clock from the UI clock at real time. The framework supports any number of clocks; pick one per logical time domain.
  • Snapshot and restore the master state for replays and saves. CaptureState() returns the complete time state including every modifier; RestoreState reconstructs it. Embedding the snapshot in a save file makes the time state reproducible.
  • Use ITimeListener for simple state polling, the event bus for typed payloads. The listener interface delivers TimeEvent enum values; the bus delivers typed event objects. Pick based on whether your handler needs to inspect the typed payload.
  • Tune AdjustFixedTimestep based on physics needs. Default true means physics also slows during slow motion; for games where physics should run at a fixed rate regardless of game speed, set to false. The latter shape is right for puzzle games where physics is the simulation, not a gameplay-modulated layer.

Pitfalls