Skip to content

The Time Manager

Lucky Engine's Time Manager is the conductor of the simulation. It decides what runs, when, and at what rate, so every physics solver, script callback, and engine subsystem in a scene advances on one shared clock instead of fighting each other.

One instance lives on every Scene.

In short

The Time Manager runs up to eight fixed-rate clocks on one timeline, sequences five execution phases per simulation step, and steers the whole thing inside a CPU budget so the UI stays responsive. The script-side view is at Time Runners.

What it does

  • Advances the simulation. Each rendered frame, the Time Manager runs zero or more simulation steps until the work for that frame is done.
  • Coordinates multiple rates. Different subsystems often want different rates: control at 50 Hz, recording at 30 Hz, drone sensors at 500 Hz, with lidar faster still. Each binds to a runner; runners share one timeline so their boundaries line up.
  • Enforces phase order. Within a step, work is divided into five fixed phases. Callbacks register with a phase and a priority; the order they run in is decided once and replayed every step.

Time modes

  • Sim Realtime (deterministic, capped)

    Steps every runner deterministically and never runs ahead of the wall clock. Slows down rather than skipping steps under load. The default.

  • Sim High Performance (deterministic, uncapped)

    Steps every runner deterministically and as fast as the hardware allows, without tracking real time. For headless replay, training, and benchmarks.

  • Game Realtime (non-deterministic)

    Follows the real-time clock and skips simulation time when the machine cannot keep up. The legacy game-loop behaviour.

A deterministic mode produces the same sequence of steps regardless of frame rate, which is what makes reproducible simulation and data collection possible.

Default runners

A new scene starts with two runners. Up to eight can be added.

  • Robot

    Runner 0, 50 Hz, 20 ms step.

    Every script callback and built-in subsystem runs on Robot by default.

  • Capture

    Runner 1, 30 Hz, 33.3 ms step.

    Available for slower work like data recording or low-rate sensor exports.

For frequencies that do not divide evenly into nanoseconds (30 Hz, 60 Hz), the engine internally cycles the step duration so long-term timing stays exact. Shared boundaries between runners therefore coincide to the nanosecond.

Phases

Every simulation step walks through five phases in order:

graph LR
    A([1. Acquisition]):::p1 --> B([2. Control]):::p2 --> C([3. Physics]):::p3 --> D([4. Validation]):::p4 --> E([5. Export]):::p5
    classDef p1 fill:#2d3a4f,stroke:#4a5e7e,color:#ffffff;
    classDef p2 fill:#2d4f3a,stroke:#4a7e5e,color:#ffffff;
    classDef p3 fill:#4f2d2d,stroke:#7e4a4a,color:#ffffff;
    classDef p4 fill:#4f4a2d,stroke:#7e764a,color:#ffffff;
    classDef p5 fill:#3a2d4f,stroke:#5e4a7e,color:#ffffff;
Phase What runs
1. Acquisition Sensor reads, data recorder, sync points, OnPreUpdate
2. Control OnUpdate, robot controllers, motion-graph IK
3. Physics Jolt, MuJoCo, Box2D, OnPhysicsUpdate
4. Validation OnLateUpdate, animation
5. Export OnPostUpdate, gRPC step capture, audio scene subsystem

A callback is bound to exactly one phase. Multiple callbacks in the same phase run in priority order, lowest number first, with ties broken stably by registration order.

What you can rely on

The Time Manager makes four guarantees about every simulation step:

  • All runners share one timeline. Sim time advances by the same delta on every runner, every step. Runners cannot drift apart.
  • Only runners that crossed a boundary fire callbacks. A runner whose accumulator is still short of its next step does nothing this iteration.
  • Phases always run in order. Acquisition first, Export last, every step, every scene.
  • At a shared step, callbacks interleave by phase, then by priority. Never one whole runner after the other.

Shared steps

When two runners' boundaries fall on the same instant, the Time Manager runs them together. Phases still drive the order; the runners' callbacks merge within each phase in priority order.

For the default Robot (50 Hz) and Capture (30 Hz), the shared boundary rate is the GCD of their frequencies: 10 Hz. Every 100 ms both runners land together:

graph LR
    T0["0 ms<br/>Robot + Capture"]:::shared --> T1["20 ms<br/>Robot"] --> T2["33 ms<br/>Capture"] --> T3["40 ms<br/>Robot"] --> T4["60 ms<br/>Robot"] --> T5["67 ms<br/>Capture"] --> T6["80 ms<br/>Robot"] --> T7["100 ms<br/>Robot + Capture"]:::shared
    classDef shared fill:#f5503d,stroke:#f5503d,color:#ffffff;

This is the cadence at which the engine's sync-point subsystem latches consistent snapshots across rates.

A familiar analogy

The multi-rate, nanosecond-aligned behaviour matches synchronised clock domains in digital hardware: several clocks derived from one base clock realign at their common beat and advance edge to edge. A multi-rate fixed-step solver, such as Simulink, schedules different sample rates on a shared base step the same way.

The step context

Every callback receives a StepContext with three pieces of information:

struct StepContext
{
    std::chrono::nanoseconds dt;     // this runner's fixed step
    RunnerID                 id;     // which runner is firing the callback
    const std::array<RunnerState, k_MaxRunners>* Swarm;  // every runner's live state
};

The swarm is the bridge between rates. Each entry carries one runner's current fixed step and its forward-looking alpha, an accumulator fraction in [0, 1] toward the next boundary. A callback can read another runner's state to interpolate between fixed steps or align a slower read with a faster producer:

double rendererAlpha  = ctx.GetAlpha(physicsRunnerID);   // interpolate between physics steps
auto   controllerStep = ctx.GetStep(controlRunnerID);    // align with the controller's clock

Free updates

Two phases sit outside the simulation clock: OnFreePreUpdate runs before any simulation step in a frame, and OnFreePostUpdate runs after. Their ts is the rendered-frame delta, not a fixed step. They are disabled by default and exist for work that genuinely belongs on the refresh rate, typically renderer hooks or UI bookkeeping.

For the script-author's view of the same system, including runner names, phase ordering as the script sees it, and the scene-settings editor pages, see Time Runners in the Scripting guide.