Game Loop and Input in PixiJS
The game loop reads input, updates game state, and triggers rendering on every frame. In PixiJS, the Ticker class provides the frame callback, while the EventSystem handles pointer and touch input on display objects. Keyboard input uses standard DOM listeners. Combining these systems into a well-structured update loop creates responsive, consistent gameplay across varying frame rates and devices.
PixiJS renders your scene automatically on every frame, but rendering alone does not make a game. The game loop is where you read the player's input, update positions and states, check for collisions, advance animations, and prepare everything for the next render. Getting this loop right determines whether your game feels smooth and responsive or sluggish and unpredictable.
Step 1: Understand the PixiJS Ticker
The Ticker is PixiJS's built-in frame loop manager. It calls registered functions on every animation frame, synchronized with the browser's requestAnimationFrame API. At 60 FPS, your callback runs approximately every 16.67 milliseconds. At 144 FPS on a high-refresh monitor, it runs roughly every 6.9 milliseconds.
Register a callback with app.ticker.add((ticker) => { update(ticker); }). The ticker object passed to your callback provides deltaTime, a multiplier representing how much time has passed relative to the expected frame duration. At a steady 60 FPS, deltaTime is 1.0. If a frame takes twice as long (30 FPS), deltaTime is 2.0. If the display runs at 120 FPS, deltaTime is approximately 0.5.
Always multiply movement and animation values by deltaTime to achieve frame-rate independence. Writing sprite.x += 5 * ticker.deltaTime moves the sprite at the same real-world speed whether the game runs at 30, 60, or 144 FPS. Without delta time, your game would run twice as fast on a 120 Hz display compared to a 60 Hz display.
The Ticker also provides elapsedMS, the actual elapsed time in milliseconds since the last frame, which is useful when you need precise timing in real units rather than the normalized delta multiplier. You can set app.ticker.maxFPS to cap the frame rate, which is useful for reducing battery consumption on mobile devices or ensuring consistent behavior during development.
Multiple callbacks can be registered with the Ticker, and they execute in the order they were added unless you specify a priority. Lower priority numbers run first, so you can ensure that input processing runs before game logic, and game logic runs before visual effect updates, by assigning appropriate priorities.
Step 2: Build a Fixed-Timestep Game Loop
For games with physics, collision detection, or any simulation that needs deterministic behavior, a fixed-timestep loop provides more reliable results than the variable-rate Ticker alone. The core idea is to update game logic at a constant rate (such as 60 times per second) regardless of how fast the display renders.
The implementation uses an accumulator pattern within the Ticker callback. On each frame, add the elapsed time to an accumulator variable. Then, while the accumulator holds enough time for a simulation step (typically 1/60th of a second, or about 16.67ms), subtract the step size and run one physics update at that fixed interval. After draining the accumulator, render the scene at its current visual state.
This approach solves two problems. First, physics calculations produce identical results regardless of frame rate, because the simulation always advances in exact, equal increments. A character jumping at 30 FPS reaches the same height and lands at the same time as one jumping at 144 FPS. Second, temporary frame rate drops do not cause objects to pass through walls or skip collision checks, because multiple fixed-size simulation steps run to catch up rather than one large variable step.
For visual smoothness between fixed update steps, apply interpolation. Store each object's previous position and current position, then render at a blend between the two based on how far the accumulator has progressed toward the next step. This eliminates the slight jitter that can occur when the rendering frame rate and simulation rate are not exact multiples of each other.
Fixed-timestep loops add complexity, so they are most valuable for games where physics precision matters, such as platformers with precise jumping, multiplayer games where determinism enables replay or lockstep networking, and simulations where frame rate variation would produce different outcomes.
Step 3: Handle Pointer and Touch Input
PixiJS v8's EventSystem provides unified pointer events that work identically for mouse and touch input. Any display object can receive pointer events once its eventMode property is configured.
Set sprite.eventMode = 'static' for objects that do not move (UI buttons, menu items) or 'dynamic' for objects that change position (draggable items, game characters). The difference is performance-related: static objects cache their hit area, while dynamic objects recalculate it each frame. Use static for anything that stays in place and dynamic only when the object moves and needs continued hit testing accuracy.
Listen for events with the standard on() method: sprite.on('pointerdown', (event) => { ... }). The event object provides global coordinates (position relative to the canvas), getLocalPosition(sprite) for coordinates relative to a specific display object, and pointerId for tracking individual fingers in multi-touch scenarios.
Common pointer events include pointerdown (mouse button pressed or touch started), pointerup (released), pointermove (cursor or finger moved), pointerover and pointerout (hover enter and leave), and pointertap (a quick press and release on the same object). For drag operations, listen for pointerdown on the draggable object, then pointermove on the stage (to track movement even if the cursor leaves the object), and pointerup on the stage to end the drag.
Hit areas can be customized with the hitArea property. By default, PixiJS uses the sprite's bounding rectangle. You can set a custom Rectangle, Circle, or Polygon shape for more precise click detection, such as making a circular button respond only to clicks within its round boundary rather than its rectangular bounding box.
Step 4: Handle Keyboard Input
PixiJS does not provide a built-in keyboard system because standard DOM event listeners handle keyboard input well. The recommended pattern is to track which keys are currently pressed in a state object, then read that state in the game loop to determine player actions.
Create a keys object that maps key names to boolean pressed states. On the keydown event, set the corresponding key to true. On keyup, set it to false. In your Ticker callback, check these flags to control movement: if keys.ArrowLeft is true, move the player left. This decouples input detection (which happens asynchronously via DOM events) from input processing (which happens synchronously in the game loop).
Use event.code rather than event.key for movement controls. The code property identifies the physical key regardless of keyboard layout, so "KeyW" always means the W key position even on AZERTY or Dvorak keyboards. The key property returns the character produced, which varies by layout and is less reliable for game controls.
Prevent default browser behavior for keys your game uses by calling event.preventDefault() in the keydown handler. Without this, arrow keys scroll the page, space bar scrolls down, and other keys trigger browser shortcuts that interfere with gameplay. Be selective about which keys you prevent, since blocking all keyboard input would make the page inaccessible.
For more complex input needs, consider tracking key press timing to distinguish taps from holds, implementing a key binding system that lets players remap controls, and supporting gamepad input through the Gamepad API (navigator.getGamepads()) which provides analog stick and button state that can be read in the ticker callback alongside keyboard state.
Step 5: Structure the Update Loop
A well-organized game loop processes each frame in distinct phases, each building on the results of the previous phase. This structure keeps the code maintainable as the game grows in complexity.
Phase 1: Read input. Sample the current state of all input devices. Copy keyboard key states, read the latest pointer position, check gamepad axes and buttons. Store these in a clean input state object. This snapshot ensures that input is consistent throughout the frame even if DOM events fire during processing.
Phase 2: Update game state. Use the input snapshot and delta time to update game logic. Move the player based on input, update enemy AI, advance timers, process game rules, and handle state transitions (player enters a door, game over condition met). This phase changes the abstract game state without touching any display objects.
Phase 3: Run physics. If using a fixed-timestep physics simulation, run the accumulator loop here. Apply forces, integrate velocities, detect and resolve collisions. Physics operates on its own data (positions, velocities, collision shapes) independent of display objects.
Phase 4: Synchronize visuals. Copy the updated game state and physics positions to PixiJS display objects. Set sprite positions to match physics body positions, update animation states based on game state (idle, running, attacking), apply visual effects (damage flash, screen shake), and update UI elements (score display, health bar).
PixiJS renders the scene automatically after all Ticker callbacks complete. You do not need to call a render function manually. Once your callback updates the display object properties, PixiJS draws the scene with the new values on the next render pass.
This phased approach, separating input, logic, physics, and visuals, makes each system testable in isolation and prevents the kind of tangled code where display objects and game logic are mixed together in ways that become impossible to debug as the project grows.
Use the Ticker with delta time for frame-rate-independent updates, implement a fixed timestep for physics-critical games, track keyboard state with DOM listeners, use the EventSystem for pointer interaction with display objects, and organize your update loop into input, logic, physics, and visual phases for maintainable game code.