This document is about: QUANTUM 3
SWITCH TO

Frame Timer and Game Timer

Quantum has two built-in timer structs to implement gameplay based on a timer, like cooldowns for skills, respawns or periodic checks in the simulation:

  • FrameTimer counts simulation frames and always runs at the simulation rate.
  • GameTimer counts game ticks (Frame.GameTick), which advance by Frame.GameTimeScale each frame, so the timer pauses and slows down with the game.

Which Timer to Use

FrameTimer GameTimer
Counts Frames (Frame.Number) Game ticks (Frame.GameTick)
Frame.GameTimeScale Ignored Paused at 0, slower below 1, faster above 1
Stored as 2 x int 2 x FP
Expiry check HasStoppedThisFrame HasStoppedThisTick
Use for Time that must pass regardless of pause or slow motion, e.g. a match countdown, an AFK kick or a reconnect grace period Gameplay that pauses or slows down with the game, e.g. cooldowns, buffs, respawns or spawn waves

If the game never changes GameTimeScale both timers behave the same.

FrameTimer

Start a new timer from seconds or from frames.

C#

FrameTimer timerA = FrameTimer.FromFrames(frame, 60)
FrameTimer timerB = FrameTimer.FromSeconds(frame, 2)
FrameTimer timerC = default(FrameTimer);

The timers timerA and timerB will be in running state, while timerC is not set, yet.

Checking the state of a FrameTimer is exemplified below, considering the sample timers from above:

C#

timerA.IsRunning(frame); // returns TRUE
timerB.IsRunning(frame); // returns TRUE
timerC.IsRunning(frame); // returns FALSE

timerA.IsSet; // returns TRUE
timerB.IsSet; // returns TRUE
timerC.IsSet; // returns FALSE

The FrameTimer keeps a memory of the target frame, which can be used to query if a timer stopped at an exact time. It can also return the elapsed and remaining time which can be handy to be displayed in the game UI for example.

C#

if (timer.HasStoppedThisFrame(frame)) {
    // Only if the timer ran out this exact frame.
}
else {
    var ticksRemaining = timer.RemainingFrames(frame);
    var secondsRemaining = timer.RemainingSeconds(frame);

    var ticksElapsed = timer.ElapsedFrames(frame);
    var elapsedSeconds = timer.ElapsedSeconds(frame);
}

Seconds are converted with the simulation rate (Frame.UpdateRate), so whole seconds are exact whole frames at any rate. FromSeconds rounds up to whole frames.

Duration returns the length of the timer in frames and DurationNormalized its progress between 0 (just started) and 1 (expired), e.g. to fill a cooldown icon in the UI.

C#

var progress = timer.DurationNormalized(frame);

FrameTimers can be added to components in the DSL or to the Frame's global variables, e.g. for a match countdown that keeps running while the game is paused.

Qtn

component Match {
    FrameTimer Countdown;
}

global {
    FrameTimer MatchTimer;
}

GameTimer

The GameTimer has the same API as the FrameTimer, but counts game ticks instead of frames. The game tick starts at 0 and advances by Frame.GameTimeScale at the start of each frame. A time scale of 1 (default) advances one tick per frame, 0 pauses the game time and 0.5 runs it at half speed. Changes to the time scale are applied from the next frame on.

C#

frame.GameTimeScale = FP._0_50;

GameTimer timerA = GameTimer.FromTicks(frame, 60);
GameTimer timerB = GameTimer.FromSeconds(frame, 2); // lasts 4 seconds at half speed

GameTimers can be used in the DSL and with prototypes (Ticks or Seconds mode) the same way as FrameTimers.

Qtn

component Character {
    GameTimer SkillCooldown;
}

This example uses the GameTimer on the component to control a skill cooldown, which pauses and slows down with the game.

C#

namespace Quantum
{
  using Photon.Deterministic;
  using UnityEngine.Scripting;
  
  [Preserve]
  public unsafe class CharacterSystem : SystemMainThreadFilter<CharacterSystem.Filter> {
    public struct Filter {
      public EntityRef Entity;
      public Character* Character;
    }

    public override void Update(Frame frame, ref Filter filter) {
        var character = filter.Character;
        if (character->SkillCooldown.IsRunning(frame) == false) {
            // Execute skill and reset the cooldown

            // Reset the cooldown by creating a new timer
            character->SkillCooldown = GameTimer.FromSeconds(frame, 2);

            // Reset the cooldown by restarting the timer.
            // BUT this requires that the timer was set at a previous time, for example when adding the component.
            character->SkillCooldown.Restart(frame);
        }
    }
  }
}

Checking the timer and querying the time works as with the FrameTimer. Seconds are seconds of game time, converted with the simulation rate:

C#

if (timer.HasStoppedThisTick(frame)) {
    // Only if the timer ran out during this frame.
}
else {
    var ticksRemaining = timer.RemainingTicks(frame);
    var secondsRemaining = timer.RemainingSeconds(frame);
    var progress = timer.DurationNormalized(frame);
}

To get real seconds divide the seconds by Frame.GameTimeScale (if it is above 0), which is only exact if the time scale doesn't change until the timer expires.

Use power of two fractions for the time scale (e.g. FP._0_50, FP._0_25, FP._1 * 3 / 8), so the game tick lands exactly on whole ticks. FP can't store most decimals exactly (e.g. FP._0_10).

Repeating Timers

Since the game tick doesn't advance in whole steps when slowed down, it rarely lands exactly on the target tick. OvershootTicks and OvershootSeconds return how far the game tick has passed the target, e.g. to place a spawn at the exact expiry point.

To restart a repeating timer without drifting, pass keepOvershoot: true to Restart, so the new interval starts at the previous target instead of the current game tick. With a time scale above 1 the game tick can pass more than one interval per frame, so restart in a loop to handle every interval:

C#

while (spawner->Interval.HasStoppedThisTick(frame)) {
    Spawn(frame, spawner->Interval.OvershootSeconds(frame));
    spawner->Interval.Restart(frame, keepOvershoot: true);
}

Delta Time

Frame.DeltaTime follows the time scale as well (GameTimeScale / UpdateRate), so systems that integrate with DeltaTime pause and slow down with the game. Frame.UnscaledDeltaTime returns the delta time ignoring the time scale. Setting DeltaTime sets the time scale instead.

To keep DeltaTime independent of the time scale and only scale the game tick, disable SimulationConfig.GameTimeScaleAffectsDeltaTime.

Last updated on

Back to top