Table of Contents

Simulation / Presentation Split

Core rule: game-state mutations happen in the Simulation loop (FixedUpdate). Rendering, VFX, and UI happen in the Presentation loop (Update / LateUpdate). Never mix the two.


Three-Loop Architecture

Loop Unity Callback Update Groups Typical Work
Simulation FixedUpdate perTimeStepUpdateSystem (every tick), perThreeTimeStepsUpdateSystem (every 3rd tick) Physics, game logic, state mutations, network tick processing
Presentation Update perFrameUpdateSystem (every frame), perSecondUpdateSystem (once/sec) Rendering, animations, VFX, UI, audio, interpolated visuals
Late Presentation LateUpdate perFrameUpdateSystem (every late frame), perSecondUpdateSystem (once/sec late) Camera follow, final visual adjustments

Timing Diagram

gantt
    title Three-Loop Timing
    dateFormat X
    axisFormat %s

    section Simulation
    FixedUpdate tick :a1, 0, 2
    FixedUpdate tick :a2, 4, 6
    FixedUpdate tick :a3, 8, 10
    FixedUpdate tick :a4, 12, 14

    section Presentation
    Update (render) :b1, 2, 8
    Update (render) :b2, 10, 16

    section Late Presentation
    LateUpdate :c1, 8, 10
    LateUpdate :c2, 16, 18

Interpolation Between Ticks

Simulation runs at a fixed rate; presentation runs at the display refresh rate. Bridge the gap with interpolation.

// Alpha value: how far we are between the last tick and the next
public static float updateInterpolationAlpha
    => Mathf.Clamp01(accumulatedDeltaTime / FixedDeltaTime);
// In Presentation loop — smooth between two simulation snapshots
float alpha = SimulationTime.updateInterpolationAlpha;

var renderPosition = Vector3.Lerp(previousPosition, currentPosition, alpha);
var renderRotation = Quaternion.Slerp(previousRotation, currentRotation, alpha);
Do Don't
Store previousPosition at the start of each tick Read transform.position directly in Update for game logic
Lerp/Slerp in Presentation only Mutate authoritative state in Update
Use updateInterpolationAlpha for smooth visuals Assume fixed and variable dt are the same

ExecutionMode Flags

Controls which code paths run for a given entity based on its role in the network topology.

namespace PixelEngine.Simulation
{
    [Flags]
    public enum ExecutionMode
    {
        None         = 1 << 0,
        Input        = 1 << 1,   // Collects player input
        Simulation   = 1 << 2,   // Runs game-state logic
        Presentation = 1 << 3,   // Renders visuals / audio
    }
}

Per-Role Execution Table

Role Input Simulation Presentation Notes
Local Player Yes Yes Yes Full authority — predicts, simulates, and renders
Remote Player No No Yes Presentation only — interpolates replicated state
Hosted Player No Yes Yes Server-side entity that also renders (listen server)
Dedicated Server No Yes No Headless — no rendering, no audio

Usage Example

public void Tick(ExecutionMode mode)
{
    if (mode.HasFlag(ExecutionMode.Input))
        CollectInput();

    if (mode.HasFlag(ExecutionMode.Simulation))
        RunSimulation();

    if (mode.HasFlag(ExecutionMode.Presentation))
        UpdateVisuals();
}

Burst-Compiled Tick Jobs

Performance-critical simulation work (physics queries, spatial hashing, state rollback) should use Unity's Burst compiler via IJobEntity or IJobChunk. Keep Burst jobs in the Simulation loop only — Presentation code typically does not need Burst.

[BurstCompile]
public partial struct MovementTickJob : IJobEntity
{
    public float DeltaTime;

    public void Execute(ref Position pos, in Velocity vel)
    {
        pos.Value += vel.Value * DeltaTime;
    }
}

Quick Rules

# Rule
1 Never mutate game state in Update or LateUpdate.
2 Never read Time.deltaTime in FixedUpdate — use Time.fixedDeltaTime.
3 Always interpolate between ticks for smooth visuals.
4 Always guard code paths with ExecutionMode flags.
5 Always store previous-tick state before advancing simulation.
6 Use Burst for hot-path simulation jobs.
7 Keep Presentation code free of side-effects on authoritative state.

Unity Documentation