| name | timing |
| description | Timing in FlatRedBall2. Use when working with cooldowns, timers, delays, entity lifetimes, self-destruct, repeating events, or FrameTime.DeltaSeconds. Covers cooldown gates, repeating timers, and entity lifetime patterns. |
Timing in FlatRedBall2
The engine provides FrameTime to every CustomActivity call. Use time.DeltaSeconds (a float) to drive all time-based logic — cooldowns, repeating events, and entity lifetimes.
FrameTime.SinceGameStart is a TimeSpan (use .TotalSeconds for a float). It's useful for absolute timestamps but the countdown pattern below is simpler for most cases.
Scaled vs. Unscaled Delta
FrameTime carries two per-frame deltas:
Delta / DeltaSeconds — multiplied by Engine.Time.TimeScale. This is what you want 99% of the time. If the player triggers slow-mo by setting TimeScale = 0.5f, gameplay using DeltaSeconds slows down with it.
UnscaledDelta / UnscaledDeltaSeconds — the raw wall-clock delta, ignoring TimeScale. Use for things that should run at real-world speed regardless of slow-mo or fast-forward: UI animations, debug overlays, screen-shake decay, anything player-facing that would feel wrong if it slowed down with the game world.
"Unscaled" means not multiplied by TimeScale. It does not mean "ignores pause" — both Delta and UnscaledDelta keep advancing while a screen is paused; the difference is purely the scale multiplication. (Pausing is handled separately — see "Pause and Entity.PauseMode" below.)
For cumulative wall-clock time across the whole game, read Engine.Time.UnscaledTimeSinceStart (a TimeSpan).
Pause and Entity.PauseMode
Screen.PauseThisScreen() suppresses entity physics, entity CustomActivity, and collision processing for the screen. Screen.CustomActivity, Gum UI, and input keep running — pause-menu logic still works.
By default, every entity is PauseMode.Pausable and freezes with the screen. To opt an individual entity out of pause suppression — e.g., a cursor entity, parallax background, or animated menu spinner that should keep ticking even while gameplay is frozen — set entity.PauseMode = PauseMode.Always; (typically in CustomInitialize). Its PhysicsUpdate and CustomActivity will run every frame regardless of Screen.IsPaused.
Note: collision processing for Always entities is still gated by screen pause — they move independently but won't interact with other entities until the screen unpauses. For UI-style entities that just need to animate or follow input, this is exactly what you want.
TimeSpan Convention
Engine-wide rule: public time-state and duration parameters use TimeSpan (Engine.Time.CurrentScreenTime, Engine.Time.UnscaledTimeSinceStart, Engine.Time.Delay(TimeSpan), Tween duration:, AnimationFrame.FrameLength, etc.). The deliberate exception is FrameTime.DeltaSeconds, which stays float because per-frame physics math (Position += Velocity * dt) would be hostile if it required (float)Delta.TotalSeconds at every call site.
For Engine.Time.Delay, a convenience overload DelaySeconds(double) is also available so call sites with literal seconds don't need TimeSpan.FromSeconds(...) ceremony — both forms are valid:
await Engine.Time.Delay(TimeSpan.FromMilliseconds(500), Token);
await Engine.Time.DelaySeconds(0.5, Token);
Cooldown Gate
Lets an action fire at most once per interval, only when triggered by input or a condition:
private float _fireCooldown = 0f;
_fireCooldown -= time.DeltaSeconds;
if (_fireCooldown <= 0f && firePressed)
{
SpawnBullet();
_fireCooldown = 0.25f;
}
Repeating Timer
Fires unconditionally every N seconds — useful for AI actions, spawners, scripted beats:
private float _shootTimer = 2f;
_shootTimer -= time.DeltaSeconds;
if (_shootTimer <= 0f)
{
SpawnBullet();
_shootTimer = 2f;
}
Entity Lifetime (Self-Destruct)
Entities that should expire after a fixed duration track their own remaining time and destroy themselves:
public class Explosion : Entity
{
private float _lifetime = 0.5f;
public override void CustomActivity(FrameTime time)
{
_lifetime -= time.DeltaSeconds;
if (_lifetime <= 0f)
Destroy();
}
}
To make the lifetime configurable at spawn time, expose it as a method or property:
public class Particle : Entity
{
private float _lifetime;
public void Launch(float lifetimeSeconds) => _lifetime = lifetimeSeconds;
public override void CustomActivity(FrameTime time)
{
_lifetime -= time.DeltaSeconds;
if (_lifetime <= 0f)
Destroy();
}
}
var p = factory.Create();
p.X = X; p.Y = Y;
p.Launch(Engine.Random.Between(0.3f, 0.8f));
All three patterns are the same mechanism — a float field decremented by DeltaSeconds each frame — applied to different use cases.
Async Delay APIs
Engine.Time exposes three awaitable helpers for sequencing game logic without manual state machines.
DelaySeconds — pause-aware
public override async void CustomInitialize()
{
await Engine.Time.DelaySeconds(2.0, Token);
MoveToScreen<NextScreen>();
}
- Counts screen time, which freezes while
Screen.IsPaused is true.
- Respects
Engine.Time.TimeScale (slow-mo / fast-forward).
- Resets to zero on screen transition.
- Always pass
Token (the screen's CancellationToken) so the task cancels automatically on screen change rather than completing against the new screen.
DelayUntil — condition-based
await Engine.Time.DelayUntil(() => _boss.IsDefeated, Token);
ShowVictoryUI();
Checked once per frame. Useful when the duration is unknown. Not pause-aware by design — the predicate is evaluated every frame regardless of pause state.
DelayFrames — frame-count-based
await Engine.Time.DelayFrames(2);
Frame count always advances, even while paused — useful for UI sequencing that should not be affected by game pause.
Waiting for Player Input
Use DelayUntil to bridge an event-driven UI action (e.g., a button click) into an async game loop, instead of polling with a flag in CustomActivity:
BattleAction? _choice = null;
attackButton.Click += (_, _) => _choice = BattleAction.Attack;
fleeButton.Click += (_, _) => _choice = BattleAction.Flee;
await Engine.Time.DelayUntil(() => _choice.HasValue, Token);
var chosen = _choice.Value;
Lifetime warning: If the screen transitions before the player clicks, Token fires and the await cancels cleanly. Do not use TaskCompletionSource.TrySetResult from a button handler that may outlive the screen — the continuation would run against the new screen.
Gotchas
- Always pass
Token to DelaySeconds and DelayUntil. Without it, a task created on one screen can complete after the screen has been destroyed and fire code against the new screen.
DelayFrames has no cancellation token but is still cancelled on screen transition — continuations guarded by Token are not needed, but keep frame waits short regardless.
async void only at the top level — use async void for CustomInitialize/event callbacks. Internal helpers should return Task and be awaited.
DeltaSeconds is not zero while paused — timer fields decremented by DeltaSeconds in entity CustomActivity won't tick while paused because entity CustomActivity is skipped. But screen CustomActivity always runs, so timer fields there do still count down. Use if (!IsPaused) guards in screen code if needed.
DelaySeconds freezes with the screen — DelaySeconds counts screen time, which is paused by PauseThisScreen() and resumes on UnpauseThisScreen(). This is the right behavior for game logic ("wait 2 s, then spawn the boss" should not advance while the player is paused at a menu). For UI flows that should keep advancing during pause — paced dialog, animated menu reveals — use DelayFrames instead, which always ticks regardless of pause.
Simulation Clock (Pauseable, Variable Speed)
Strategy and city-builder games need a game-wide clock that fires discrete "sim ticks" at a rate controlled by the player (pause / 1× / 2× / 3×). This is different from per-entity timers and from DelaySeconds (which counts real screen time and can't be sped up).
private float _simAccumulator;
private float _timeScale = 1f;
private const float TickInterval = 2f;
_simAccumulator += time.DeltaSeconds * _timeScale;
while (_simAccumulator >= TickInterval)
{
_simAccumulator -= TickInterval;
RunSimTick();
}
Why a while loop instead of if? At high time scales, multiple ticks can accumulate in a single frame. Using while processes all of them before proceeding, which keeps the simulation deterministic.
Changing speed mid-game: Update _timeScale at any time — the accumulator continues from wherever it left off. Pausing is _timeScale = 0f; the accumulator freezes until resumed.
Running ticks on the screen (not an entity) is important: entity CustomActivity is skipped when the screen is paused, but screen CustomActivity always runs. Keep the accumulator and RunSimTick() call on the screen so you can gate it yourself with _timeScale.
Pause Gate — Awaiting Player Input While Paused
To show a dialog and freeze the game until the player responds (NPC dialogue, skill selection, confirmation), pause the screen, display the UI, and await DelayUntil for the player's choice, then resume. DelayUntil evaluates its predicate every frame regardless of pause state, so it resolves the moment the player acts:
PauseThisScreen();
_dialogPanel.IsVisible = true;
await Engine.Time.DelayUntil(() => _playerChose, Token);
_dialogPanel.IsVisible = false;
UnpauseThisScreen();
DelaySeconds does not work here — it counts screen time, which is frozen while paused.