How to Use Fixed Timestep Physics with Pixi.js Ticker

Integrating a physics engine with Pixi.js requires reconciling Pixi’s variable rendering frame rate with the rigid, constant intervals needed for stable physics. This article explains how to implement the standard fixed timestep accumulator pattern inside the Pixi.js Ticker, ensuring smooth rendering and deterministic physics simulation regardless of device performance or screen refresh rates.

The Problem with Variable Delta Time

By default, the PIXI.Ticker runs on requestAnimationFrame, meaning the elapsed time between frames (delta time) fluctuates. If you pass this variable delta time directly to a physics engine (like Matter.js, Box2D, or a custom engine), it leads to inconsistent collision detection, clipping through walls, and unpredictable object behavior.

To maintain physical consistency, physics calculations must occur at a strict, fixed interval (e.g., exactly 60 times per second), independent of how fast the screen renders.

The industry-standard approach is to accumulate the elapsed time provided by Pixi’s ticker and execute the physics engine in fixed steps until the accumulated time is consumed.

Here is the implementation of this pattern using Pixi.js:

import * as PIXI from 'pixi.js';

// 1. Define the fixed timestep (e.g., 60Hz = ~16.67ms per step)
const FIXED_TIMESTEP = 1 / 60; 
let accumulator = 0;

const app = new PIXI.Application();
await app.init({ width: 800, height: 600 });
document.body.appendChild(app.canvas);

// 2. Add the update loop to Pixi's Ticker
app.ticker.add((ticker) => {
    // Convert elapsedMS to seconds
    let elapsedSeconds = ticker.elapsedMS / 1000;

    // Prevent the "spiral of death" by capping the maximum frame time
    if (elapsedSeconds > 0.25) {
        elapsedSeconds = 0.25;
    }

    accumulator += elapsedSeconds;

    // Run physics updates in fixed increments
    while (accumulator >= FIXED_TIMESTEP) {
        physicsEngine.update(FIXED_TIMESTEP);
        accumulator -= FIXED_TIMESTEP;
    }

    // Render step: Update Pixi sprite positions
    updateVisuals();
});

Key Components of this Method

1. The Accumulator

The accumulator variable stores the leftover time from previous frames. If a frame takes 20ms and your fixed timestep is 16.6ms, the physics engine runs once, and the remaining 3.4ms is saved in the accumulator to be processed in the next frame.

2. Preventing the “Spiral of Death”

If the game lags heavily, the elapsed time can become very large. Without a cap, the while loop would run dozens of times in a single frame to catch up, causing even more lag. Capping elapsedSeconds at 0.25 (250ms) ensures the game slows down rather than crashing during heavy lag spikes.

3. Handling Micro-Stutter (Interpolation)

Because the rendering rate rarely aligns perfectly with the physics updates, you may notice a slight visual judder. To achieve perfect visual smoothness, interpolate your Pixi sprite positions between the current physics state and the previous physics state.

You can calculate the interpolation fraction (often called alpha) using the remaining time in the accumulator:

const alpha = accumulator / FIXED_TIMESTEP;

// Example interpolation:
sprite.x = currentPhysicsBody.x * alpha + previousPhysicsBody.x * (1 - alpha);
sprite.y = currentPhysicsBody.y * alpha + previousPhysicsBody.y * (1 - alpha);

Applying this interpolation ensures that even if a user has a 144Hz monitor, the visuals will render with maximum smoothness while the underlying physics simulation continues to run at a stable 60Hz.