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 Recommended Solution: The Accumulator Pattern
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.