Frame-cached delta time, rolling frame rate statistics, countdown timers with events, and the formatting that turns a float of seconds into a clock a player can read.
Summary
Four classes cover the time work that sits outside the framework’s time subsystem: reading delta time without paying for the Unity call repeatedly, tracking frame rate over a rolling window, running a countdown that raises events, formatting seconds into something a player can read, and doing calendar arithmetic for daily rewards and login streaks.
TimeUtil is the one you touch most. Its DeltaTime and UnscaledDeltaTime cache per frame, keyed on Time.frameCount, so a system that reads delta time in a dozen places pays for one Unity call rather than twelve. It also keeps rolling frame rate statistics, converts seconds to a formatted clock string, and has a FrameNormalizedDelta for the specific case of porting fixed-rate logic that assumes a 60 FPS baseline. For timestamps that things are sorted by, UniqueUTCNow returns a UTC time that is always later than the one it returned before.
Timer is a countdown object rather than a coroutine. You create it, tick it, and it raises OnComplete when it finishes. It supports looping with a loop cap, runs on its own time scale, and can be paused and resumed. Being a plain object rather than a coroutine means it survives the thing that owns it being disabled, and it can be serialized, inspected and unit tested.
DateUtil handles wall-clock dates: parsing and formatting ISO 8601, adding days and months, finding the start of a week or a month, and measuring the gap between two dates. A daily-reward system, a login streak, or a timed event window all need exactly that.
One thing to settle before you read further. TimeUtil has Pause, Resume and SetTimeScale, and so does the framework’s Time subsystem. They are not the same thing and they do not know about each other. Use the subsystem for game pause; the section below explains why.
Use the time utilities for:
- Reading delta time in per-frame code without repeated Unity API calls.
- Showing a frame rate counter, or gating quality settings on a rolling average.
- Countdown timers for abilities, respawns, wave gaps and round clocks.
- Formatting a duration as a clock string for a HUD, a results screen or a leaderboard.
- Daily rewards, login streaks, and any feature that cares about the calendar rather than elapsed play time.
- Timestamping saves, log entries or events that are later ordered by time.
- Holding back an intro, a camera move or any other delta-driven sequence until a loading hitch has passed.
Features
- Frame-cached delta time.
DeltaTimeandUnscaledDeltaTimecompute once per frame and return the cached value afterwards, so repeated reads within a frame cost nothing extra. - Rolling frame rate statistics.
CurrentFPS,AverageFPS,MinFPSandMaxFPSover a sampled window, withSampleFPS,ResetFPSStatsandResetFPSMinMaxto drive and clear it. - Countdown timers as objects.
Timer.CreateandTimer.CreateLoopingproduce a tickable countdown with six events, a loop cap, and its own time scale independent of the game’s. - Fluent timer configuration. Every setter on
Timerreturns the timer, so construction and configuration chain into one statement. - Clock formatting.
FormatTimeandFormatTimeShortturn seconds into a readable string, with optional hours and milliseconds. - Calendar arithmetic.
DateUtiladds days, months and years, measures the gap between two dates, and finds the start or end of a day, week, month or year. - ISO 8601 in both directions.
ToISO8601,ToISO8601UTCandParseISO8601for anything that has to survive a save file or a server round trip. - Tolerant date parsing.
TryParseagainst a known format andTryParseAnywhen the shape of the input is not guaranteed. - Frame-rate-normalized delta.
FrameNormalizedDeltamultiplies delta time by the target frame rate, for porting logic written as “units per frame at 60 FPS”. - Strictly increasing UTC timestamps.
TimeUtil.UniqueUTCNow()returns the current UTC time, or one tick past the previous result when the system clock has not moved on, so two timestamps taken in quick succession are never equal. It is safe to call from any thread. - Settling wait.
WaitForSettledFramesis a coroutine wait that holds until a run of consecutive frames comes in under a threshold (by default 3 frames of at most 0.05 seconds each), with a timeout of 5 seconds of accumulated frame time.TimedOutreports which way it ended.
The four classes
Each class owns a different kind of time: frame time, elapsed time, wall-clock time, and the wait for a stable frame rate.
| Class | Deals with | Notes |
|---|---|---|
TimeUtil | Frame time and frame rate | Static, and the one used most. DeltaTime and UnscaledDeltaTime cache on Time.frameCount; every other property forwards to UnityEngine.Time uncached. The FPS statistics are driven by SampleFPS, so they only move if something calls it. Also holds the clock formatting, the strictly increasing UniqueUTCNow clock, and the game-speed controls that overlap the Time subsystem. Main-thread only, except UniqueUTCNow. |
Timer | A single countdown | An object you own and tick yourself with Update(deltaTime). Raises OnStart, OnPause, OnResume, OnStop, OnProgress and OnComplete. Supports looping with an optional maximum, its own SetTimeScale independent of the game’s, and reports ElapsedTime, RemainingTime, LoopCount and three state flags. |
DateUtil | Wall-clock dates | Static. Formatting and parsing including ISO 8601, arithmetic in days, months and years, boundary helpers for day, week, month and year, and the weekend and weekday tests. The week helpers take the first day of the week as a parameter, defaulting to Sunday. |
WaitForSettledFrames | Waiting for a stable frame rate | A CustomYieldInstruction that holds until several consecutive frames come in under a threshold, or until a timeout. What you yield on before starting anything whose timing would be ruined by a startup or loading hitch. Frames are measured unscaled, TimedOut and IsDone report the outcome, and ObserveFrame drives it without a coroutine. |
Where this ends and the Time subsystem begins
Both TimeUtil and the framework’s Time subsystem can pause the game, and only one of them should be doing it in a given project.
ScyllaTime is the subsystem. It owns pause as a piece of state: it tracks whether the game is paused, remembers the scale to restore, supports pause modes, and gives the rest of the framework something to consult. TimeUtil.Pause() is a convenience that sets UnityEngine.Time.timeScale to zero and nothing else.
The consequence is that they fight. Pausing through TimeUtil leaves ScyllaTime.IsPaused reporting false, because nothing told it. Resuming through TimeUtil sets the scale to exactly 1.0, discarding any custom scale the subsystem was managing, such as a slow-motion effect that was running when you paused.
So: use ScyllaTime for game pause and time scale. Reach for TimeUtil‘s versions only in a project that is not using the subsystem at all, or in a throwaway tool where the interaction cannot arise. Everything else on TimeUtil, the delta caching, the frame rate statistics and the formatting, is independent and safe to use alongside the subsystem.
Recipes
Every recipe stands alone here. Skip around based on whichever problem you’re solving right now.
Read delta time once per frame instead of a dozen times
Problem. Your movement, animation and cooldown code all read Time.deltaTime in the same frame, and each read is a call into Unity’s engine layer. Individually cheap, collectively pointless.
Solution. TimeUtil.DeltaTime computes once per frame and returns the cached value for every later read in that frame.
/* Cached on Time.frameCount, so the first read in a frame does the work and the rest
are field reads. Drop-in for Time.deltaTime. */
transform.position += velocity * TimeUtil.DeltaTime;
_cooldownRemaining -= TimeUtil.DeltaTime;
/* Unscaled for anything that must keep running while the game is paused: a UI
animation, a pause-menu transition, a network heartbeat. */
_menuFade += TimeUtil.UnscaledDeltaTime * fadeSpeed;
/* FrameNormalizedDelta is delta time multiplied by the target frame rate, which is
what you want when porting logic written as "move 2 units per frame at 60 FPS".
It scales with timeScale; the Unscaled variant does not. */
retroSprite.position += stepPerFrame * TimeUtil.FrameNormalizedDelta;
Show a frame rate counter that is not noise
Problem. An instantaneous frame rate readout jumps around so much it is unreadable. You want an average, and you want to know how bad the worst frame was.
Solution. The rolling statistics, driven by a SampleFPS call each frame.
private void Update()
{
/* The statistics only advance when something samples them. One call per frame,
from one place, is the intended shape. */
TimeUtil.SampleFPS();
/* Current is the instantaneous rate and jumps around. Average is the readable one. */
_fpsLabel.text = $"{TimeUtil.AverageFPS:F0} FPS";
/* Min is the honest number: a player feels the worst frame in the window as a
stutter, and an average of 60 hides a single 20ms spike completely. */
_worstLabel.text = $"min {TimeUtil.MinFPS:F0} max {TimeUtil.MaxFPS:F0}";
}
/* Clear the window when a measurement stops being comparable: after a scene load, on
entering a new area, or when the player changes a quality setting. */
TimeUtil.ResetFPSStats();
/* Or reset only the extremes, keeping the running average, which suits a benchmark
that reports the worst frame per section. */
TimeUtil.ResetFPSMinMax();
Run an ability cooldown without a coroutine
Problem. An ability has a cooldown that needs to pause when the game pauses, survive the caster being disabled, and tell the UI how far along it is. A coroutine stops when the object is disabled and cannot easily report progress.
Solution. A Timer is a plain object. You tick it, so you decide what stops it and what does not.
/* Create returns the timer; every setter returns it too, so configuration chains. */
private readonly Timer _dashCooldown = Timer.Create(3f)
.SetLooping(false);
private void Awake()
{
/* Six events cover the lifecycle. OnProgress carries a value from 0 to 1, ready to
drive a radial cooldown sweep on the HUD directly. */
_dashCooldown.OnComplete += () => _dashReady = true;
_dashCooldown.OnProgress += p => _cooldownRing.fillAmount = 1f - p;
}
private void Update()
{
/* You supply the delta, so this cooldown honours game pause. Passing
UnscaledDeltaTime instead would make it keep counting through a pause. */
_dashCooldown.Update(TimeUtil.DeltaTime);
}
public void Dash()
{
_dashReady = false;
_dashCooldown.Restart();
}
/* State is readable without tracking it yourself. */
var remaining = _dashCooldown.RemainingTime;
var isTicking = _dashCooldown.IsRunning;
Spawn waves on a loop with a hard stop
Problem. Your survival mode spawns a wave every twelve seconds for exactly ten waves, then stops and hands over to the boss.
Solution. A looping timer with a loop cap does both, without a counter you maintain alongside it.
/* CreateLooping restarts automatically on completion. SetMaxLoops stops it after a
fixed number, so the wave counter lives in the timer rather than beside it. */
private readonly Timer _waveTimer = Timer.CreateLooping(12f)
.SetMaxLoops(10);
private void Awake()
{
/* OnComplete fires on every loop, so this runs once per wave. */
_waveTimer.OnComplete += SpawnWave;
/* OnStop fires when the loop cap is reached, which is the handover point. */
_waveTimer.OnStop += BeginBossPhase;
_waveTimer.Start();
}
/* LoopCount reports how many have elapsed, for the "Wave 4 of 10" label. */
_waveLabel.text = $"Wave {_waveTimer.LoopCount + 1} of 10";
/* SetTimeScale on the timer itself speeds up the whole sequence without touching the
game's time scale, so a "fast forward" difficulty option only has to call this one
method. */
_waveTimer.SetTimeScale(1.5f);
Format a duration as a clock a player can read
Problem. You have 137.4 seconds and the HUD needs 2:17. The results screen needs 00:02:17.400. Writing that twice is how the two end up disagreeing.
Solution. FormatTime covers both shapes through its parameters.
/* includeHours is nullable and defaults to null, which means "include them only when
the duration is long enough to need them". A 2 minute run shows 2:17, a 90 minute
one shows 1:30:00, with no branch on your side. */
_timerLabel.text = TimeUtil.FormatTime(elapsedSeconds);
/* Milliseconds for a speedrun split or a results breakdown. */
_splitLabel.text = TimeUtil.FormatTime(elapsedSeconds, includeHours: true, includeMilliseconds: true);
/* The short form for a compact HUD where space is tight. */
_compactLabel.text = TimeUtil.FormatTimeShort(elapsedSeconds);
/* Conversion both ways when something else in the codebase speaks TimeSpan. */
var span = TimeUtil.SecondsToTimeSpan(elapsedSeconds);
var backToSeconds = TimeUtil.TimeSpanToSeconds(span);
var formatted = TimeUtil.FormatTimeSpan(span, includeMilliseconds: true);
Grant a daily reward exactly once per day
Problem. A daily reward must reset at midnight rather than twenty-four hours after the last claim, and a login streak has to notice a missed day.
Solution. DateUtil‘s boundary and difference helpers, with ISO 8601 for the stored value.
/* Store the claim as ISO 8601 so it survives a save file and a timezone change. */
var lastClaimed = DateUtil.ParseISO8601(save.LastDailyClaimISO);
/* Comparing the start of each day resets the reward exactly at midnight, rather than
twenty-four hours after the last claim. */
var today = DateUtil.GetStartOfDay(DateTime.Now);
var lastClaimDay = DateUtil.GetStartOfDay(lastClaimed);
if (today > lastClaimDay)
{
GrantDailyReward();
/* One day between them continues the streak; more than one breaks it. */
var gap = DateUtil.GetDaysBetween(lastClaimDay, today);
save.Streak = gap == 1 ? save.Streak + 1 : 1;
save.LastDailyClaimISO = DateUtil.ToISO8601(DateTime.Now);
}
/* A weekend event window, with the week starting on whichever day your audience
expects. The default is Sunday. */
var weekStart = DateUtil.GetStartOfWeek(DateTime.Now, DayOfWeek.Monday);
var weekEnd = DateUtil.GetEndOfWeek(DateTime.Now, DayOfWeek.Monday);
if (DateUtil.IsWeekend(DateTime.Now))
{
ApplyWeekendBonus();
}
Give quick successive saves distinct timestamps
Problem. The save slot list sorts by timestamp, and an autosave followed immediately by a quicksave sometimes shows up in the wrong order. On Windows, DateTime.UtcNow only advances about every 15.6 milliseconds, so two saves made inside the same step get the same time and the sort has nothing to go on.
Solution. TimeUtil.UniqueUTCNow() returns the system time when it has moved past the previous result, and the previous result plus one tick (100 nanoseconds) when it has not. Every result is later than the one before it, and none drifts more than one clock step from the real time.
/* Two saves in the same frame still get different, correctly ordered times. */
var autosaveTime = TimeUtil.UniqueUTCNow();
var quicksaveTime = TimeUtil.UniqueUTCNow(); /* always later than autosaveTime */
/* The result is a UTC DateTime, so it stores cleanly as ISO 8601. */
save.WrittenAtISO = DateUtil.ToISO8601UTC(quicksaveTime);
/* Safe to call from any thread, for example a background save task. */
Task.Run(() => WriteSave(slot, TimeUtil.UniqueUTCNow()));
Read a date whose format you cannot guarantee
Problem. A date is coming from a config file, an imported spreadsheet, or a save written by an older version of your game, and it is not always in the same shape.
Solution. TryParseAny attempts the formats DateUtil knows about, and TryParse pins it to one when you do know.
/* Known format: the strict form, and the one to use for your own data. */
if (!DateUtil.TryParse(row.ReleaseDate, out var releaseDate))
{
Log.Warning($"Unreadable release date '{row.ReleaseDate}'.", LogCategory.Data);
releaseDate = DateTime.MinValue;
}
/* Unknown shape: the tolerant form, for imported or legacy data. */
if (DateUtil.TryParseAny(legacySave.Timestamp, out var parsed))
{
MigrateSaveTimestamp(parsed);
}
/* Formatting for display, separate from the machine-readable storage form. */
_dateLabel.text = DateUtil.Format(releaseDate);
_todayLabel.text = DateUtil.TodayString();
Wait for the frame rate to settle before starting something timed
Problem. Your intro sequence is driven by delta time, and it starts during the loading hitch, so the first frame’s enormous delta skips a chunk of the animation before anyone sees it.
Solution. Yield on WaitForSettledFrames first. It holds until several consecutive frames come in under a threshold, or until a timeout, and the constructor takes all three values. The defaults are 3 consecutive frames of at most 0.05 seconds each, with a 5 second timeout, and they are exposed as DEFAULT_SETTLED_FRAMES, DEFAULT_SETTLED_FRAME_SECONDS and DEFAULT_TIMEOUT_SECONDS. Frame lengths are measured unscaled, so a paused game does not wait forever. The timeout counts accumulated frame time rather than wall-clock time, so it does not expire while the editor sits paused.
private IEnumerator PlayIntro()
{
/* Holds until the frame rate steadies, or until the timeout expires, so a slow
* machine still proceeds rather than hanging. Defaults cover the common case. */
var wait = new WaitForSettledFrames();
yield return wait;
/* TimedOut says the rate never settled. The intro still plays, but the first
* frames may be hitched, which is worth a note in a startup log. */
if (wait.TimedOut)
{
Log.Info("Intro started before the frame rate settled.", LogCategory.Core);
}
/* By here the first delta is a real frame rather than a loading spike, so the
* reveal plays at the speed it was authored for. */
_titleReveal.Play();
}
/* A stricter wait for a 60 FPS target: 5 frames of at most 20 ms, giving up after 3 s. */
var strictWait = new WaitForSettledFrames(settledFrameSeconds: 0.02f, settledFramesRequired: 5, timeoutSeconds: 3f);
Outside a coroutine, feed frame lengths to ObserveFrame from your own update loop and watch IsDone. That is also how the wait is tested without a running player loop.
private readonly WaitForSettledFrames _settle = new();
private void Update()
{
if (_settle.IsDone)
{
return;
}
_settle.ObserveFrame(Time.unscaledDeltaTime);
if (_settle.IsDone)
{
BeginCameraFlyby();
}
}
Pause the game the way the framework expects
Problem. You want a pause menu, and TimeUtil.Pause() is right there.
Solution. Use the Time subsystem instead, so the rest of the framework knows the game is paused.
/* ScyllaTime owns pause as state: it tracks IsPaused, remembers the scale to restore,
and supports pause modes. The framework consults it. */
ScyllaTime.Instance.Pause();
/* Resume restores the scale that was in effect before the pause, so a slow-motion
effect that was running survives the trip through the pause menu. */
ScyllaTime.Instance.Resume();
/* TimeUtil.Pause() sets UnityEngine.Time.timeScale to 0 and nothing else. It does not
update ScyllaTime.IsPaused, and TimeUtil.Resume() sets the scale to exactly 1.0,
discarding whatever custom scale was in play. Reach for these only in a project
that is not using the Time subsystem. */
API Sketch
TimeUtil, DateUtil and Timer live in Scylla.Core.Util, alongside WaitForSettledFrames. The sketch below lists the members without their static modifiers.
/* ---------------------------------------------------------------- */
/* TimeUtil: frame time, frame rate, formatting */
/* ---------------------------------------------------------------- */
/* Cached once per frame on Time.frameCount. */
float DeltaTime { get; }
float UnscaledDeltaTime { get; }
/* Forwarded to UnityEngine.Time uncached. */
float ScaledDeltaTime, SmoothDeltaTime, FixedDeltaTime { get; }
float GameTime, UnscaledGameTime, RealtimeSinceStartup { get; }
float FrameNormalizedDelta, FrameNormalizedDeltaUnscaled { get; }
int FrameCount { get; }
float TimeScale { get; }
bool IsPaused { get; } // reads back Time.timeScale, not ScyllaTime state
/* Frame rate, driven by SampleFPS. */
float CurrentFPS, AverageFPS, MinFPS, MaxFPS { get; }
void SampleFPS();
void ResetFPSStats();
void ResetFPSMinMax();
/* Formatting and conversion. */
string FormatTime(float totalSeconds, bool? includeHours = null, bool includeMilliseconds = false);
string FormatTimeShort(float totalSeconds);
string FormatTimeSpan(TimeSpan timeSpan, bool includeMilliseconds = false);
TimeSpan SecondsToTimeSpan(float seconds);
float TimeSpanToSeconds(TimeSpan timeSpan);
/* Strictly increasing UTC clock. Safe from any thread. */
DateTime UniqueUTCNow();
/* Interpolation helpers. */
float GetInterpolationDelta(float duration, bool useUnscaled = false);
float GetSmoothingFactor(float smoothTime, bool useUnscaled = false);
/* Game speed. Prefer the Time subsystem for these; see the section above. */
void Pause();
void Resume();
void SetTimeScale(float scale);
/* ---------------------------------------------------------------- */
/* Timer: a countdown object you tick yourself */
/* ---------------------------------------------------------------- */
static Timer Create(float duration, Action onComplete = null);
static Timer CreateLooping(float duration, Action onComplete = null);
/* Control. All return the timer, so calls chain. */
Timer Start(); Timer Stop(); Timer Pause();
Timer Resume(); Timer Reset(); Timer Restart();
Timer SetElapsedTime(float time);
Timer SetDuration(float duration);
Timer SetLooping(bool isLooping);
Timer SetMaxLoops(int maxLoops);
Timer SetTimeScale(float scale); // independent of the game's time scale
void Update(float deltaTime); // you supply the delta, so you choose the clock
/* State. */
float ElapsedTime, RemainingTime { get; }
bool IsRunning, IsPaused, IsComplete { get; }
int LoopCount { get; }
/* Events. */
event Action OnStart, OnPause, OnResume, OnStop, OnComplete;
event Action<float> OnProgress; // 0..1
/* ---------------------------------------------------------------- */
/* DateUtil: wall-clock dates */
/* ---------------------------------------------------------------- */
string TodayString();
string TodayString(string format);
string NowString(string format = FORMAT_GENERAL_LONG);
string Format(DateTime dateTime, string format = FORMAT_GENERAL_LONG);
string ToISO8601(DateTime dateTime, bool includeTime = true);
string ToISO8601UTC(DateTime dateTime);
DateTime Parse(string dateString, string format = FORMAT_ISO_DATE);
bool TryParse(string dateString, out DateTime result, string format = FORMAT_ISO_DATE);
bool TryParseAny(string dateString, out DateTime result);
DateTime ParseISO8601(string isoString);
DateTime AddDays(DateTime dateTime, int days);
DateTime AddMonths(DateTime dateTime, int months);
DateTime AddYears(DateTime dateTime, int years);
int GetDaysBetween(DateTime start, DateTime end);
int GetMonthsBetween(DateTime start, DateTime end);
int GetYearsBetween(DateTime start, DateTime end);
DateTime GetStartOfDay(DateTime dateTime); DateTime GetEndOfDay(DateTime dateTime);
DateTime GetStartOfWeek(DateTime dateTime, DayOfWeek startOfWeek = DayOfWeek.Sunday);
DateTime GetEndOfWeek(DateTime dateTime, DayOfWeek startOfWeek = DayOfWeek.Sunday);
DateTime GetStartOfMonth(DateTime dateTime); DateTime GetEndOfMonth(DateTime dateTime);
DateTime GetStartOfYear(DateTime dateTime); DateTime GetEndOfYear(DateTime dateTime);
bool IsWeekend(DateTime dateTime); bool IsWeekday(DateTime dateTime);
/* ---------------------------------------------------------------- */
/* WaitForSettledFrames: a CustomYieldInstruction */
/* ---------------------------------------------------------------- */
const float DEFAULT_SETTLED_FRAME_SECONDS = 0.05f;
const int DEFAULT_SETTLED_FRAMES = 3;
const float DEFAULT_TIMEOUT_SECONDS = 5f;
WaitForSettledFrames(float settledFrameSeconds = DEFAULT_SETTLED_FRAME_SECONDS,
int settledFramesRequired = DEFAULT_SETTLED_FRAMES,
float timeoutSeconds = DEFAULT_TIMEOUT_SECONDS); // non-positive times fall back to the defaults; the frame count is at least 1
float SettledFrameSeconds { get; }
int SettledFramesRequired { get; }
float TimeoutSeconds { get; }
bool TimedOut { get; } // ended on the timeout rather than on a settled run
bool IsDone { get; } // ended for either reason
void ObserveFrame(float frameSeconds); // drive it manually without a coroutine
void Reset();
See the API reference for full signatures, remarks, and exception contracts on every member.
Best Practices
- Use
TimeUtil.DeltaTimeas your default delta. It is a drop-in forTime.deltaTimeand the caching pays for itself the moment more than one system reads it in a frame. - Pick the clock at the call site.
Timer.Updatetakes the delta you hand it, so passingDeltaTimeorUnscaledDeltaTimedecides whether that timer freezes with the game. That is the whole mechanism, and it is per timer. - Pause through the Time subsystem, not through
TimeUtil.ScyllaTimeowns pause as state that the rest of the framework reads;TimeUtil.Pauseonly writesTime.timeScale. - Report minimum frame rate alongside the average. An average of 60 hides a single stutter completely, even though the player felt exactly that stutter.
- Call
SampleFPSfrom exactly one place. The statistics only advance when something samples them, and sampling from two systems double-counts the window. - Reset the frame rate window when the measurement changes meaning. After a scene load or a quality change, the old samples describe a different game.
- Prefer a
Timerto a coroutine for anything with state you need to read. A coroutine cannot tell you its remaining time, cannot be paused without extra machinery, and dies with the object that started it. - Chain timer configuration rather than assigning in steps. Every setter returns the timer, so creation and setup read as one statement and there is no half-configured timer in between.
- Store dates as ISO 8601, and prefer UTC when anything is shared. Local time is fine for a single-device save and wrong the moment a server or a second device is involved.
- Compare day boundaries, not elapsed hours, for daily features.
GetStartOfDayresets a daily reward at midnight instead of on a rolling window from the last claim. - Use
TryParseAnyonly for data you do not control. For your own formats the strictTryParsecatches the typo instead of guessing around it. - Timestamp ordered records with
UniqueUTCNow.DateTime.UtcNowcan return the same value twice in quick succession, especially on Windows, which turns “newest” into a tie. - Yield on
WaitForSettledFramesbefore anything timing-sensitive. A loading hitch produces a delta far larger than a normal frame, and delta-driven sequences started in that frame visibly skip.
Pitfalls
Related
- 01 Scylla Core
- 13 Core Systems – Time
- 35 Core Utilities – String
- 34 Core Utilities – Unit
- API reference:
Scylla.Core.Util.TimeUtil - API reference:
Scylla.Core.Util.DateUtil - API reference:
Scylla.Core.Util.Timer - API reference:
Scylla.Core.Util.WaitForSettledFrames