PixiJS Game Development
Intro
PixiJS renders 2D scenes through a display-object tree (Application
holds a stage Container, which holds Sprites, Graphics, and
nested Containers). Drive updates from the Ticker, load assets
through Assets, and reach for ParticleContainer only when raw
batch performance matters.
Overview
Application setup
Create an Application, await app.init({...}), then append
app.canvas to the DOM. Pass resizeTo: window for a responsive
canvas, antialias: true for smoother edges, and
resolution: window.devicePixelRatio plus autoDensity: true for
sharp HiDPI rendering. PixiJS picks WebGL by default and falls back
to Canvas automatically.
Sprites and textures
Load textures via Assets.load('hero.png') (returns a promise) and
construct sprites with new Sprite(texture) or Sprite.from(texture).
Set anchor.set(0.5) to center the origin so rotation and scaling
behave intuitively. Assets caches by URL — loading the same source
twice is free. Use Texture.EMPTY as a placeholder while assets are
still loading.
Display hierarchy
Container is the base group: children inherit position, scale,
rotation, and alpha from their parent. Set sortableChildren = true
on a container and assign zIndex to children to control draw order.
Remove off-screen objects from the stage rather than just hiding them.
Use ParticleContainer for thousands of identical sprites — it
trades features for raw throughput.
Animation and the ticker
Drive everything from app.ticker.add((ticker) => { ... }). The
ticker.deltaTime value is normalized to 1.0 at the target FPS so
movement stays frame-rate independent. Use AnimatedSprite for
spritesheet animations and control animationSpeed independently of
the main ticker. For complex tweening, integrate GSAP rather than
hand-rolling easing.
Interaction and events
Set eventMode = 'static' on any display object that should receive
pointer events, then attach handlers with .on('pointerdown', ...).
The same events fire for mouse and touch. Custom hit regions go on
hitArea (Circle, Rectangle, Polygon). The drag pattern is
pointerdown on the sprite → pointermove on the stage → pointerup
on the stage. Disable interaction on non-interactive children to keep
hit-testing fast.
Graphics and effects
new Graphics() exposes vector primitives — rect, circle,
moveTo/lineTo, roundRect — with chainable fill() and
stroke() calls. For static shapes that you'll reuse, render once to
a texture with app.renderer.generateTexture(). Apply filters via
displayObject.filters = [new BlurFilter(...)]; chain filters by
adding them to the array in order. Filters are GPU-intensive — use
them sparingly on mobile.
Asset loading
Use Assets.add({ alias, src }) then Assets.load(alias) for named
assets, or define bundles with Assets.addBundle('game', {...}) and
await Assets.loadBundle('game'). Bundle loading reports progress
via a callback. Supported formats: PNG, JPG, WebP, SVG, JSON
(spritesheets), and audio via plugins. Preload critical assets before
showing the game; lazy-load secondary assets in the background.
Gotchas
Agent-specific failure modes — provider-neutral pause-and-self-check items:
- Accessing
app.canvasorapp.stagebeforeawait app.init()resolves. PixiJS v8 requires the async init to complete before the renderer is ready. Accessing canvas or stage properties before init resolves throws an error. Alwaysawait app.init({...})before any further setup. - Multiplying movement by raw frame deltas instead of
ticker.deltaTime. Raw frame time varies with device refresh rate — objects move twice as fast on a 120Hz display if you use raw milliseconds.ticker.deltaTimeis normalized to 1.0 at the target FPS, making movement frame-rate independent. Always multiply speeds bydeltaTime. - Constructing sprites by passing a URL string directly instead of using
Assets.load(). Bypassing theAssetscache reloads the file on every construction call. Useawait Assets.load('hero.png')once and construct sprites from the returned texture to benefit from caching. - Leaving off-screen objects in the display tree instead of removing them. Hidden or off-screen objects that remain in the scene still receive render calls. For games with many projectiles or particles, accumulated off-screen objects are a primary cause of frame rate degradation. Remove from parent when no longer needed.
- Setting
eventMode = 'static'on containers without disabling it on non-interactive children. Every object with event mode enabled participates in hit-testing on each pointer event. Enabling it on containers whose children need no interaction inflates hit-testing cost. Only enable interaction on objects that actually handle pointer events. - Applying the same filter to many individual objects instead of to a shared parent container. Each filter application is a separate GPU pass. A blur applied individually to 50 sprites costs 50 GPU passes; the same blur applied once to their parent container costs one pass. Group objects and apply filters at the container level.
- Showing the game scene before critical assets have finished loading. Displaying a scene before its textures are loaded results in sprites rendering as white squares or as invisible objects. Preload all assets required by the first visible scene before transitioning out of the loading screen.
Full reference
Application boilerplate
import { Application, Assets, Sprite, Container, Graphics, Text } from 'pixi.js';
const app = new Application();
await app.init({
width: 800,
height: 600,
background: '#1a1a2e',
antialias: true,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
});
document.body.appendChild(app.canvas);
Core classes
| Class | Purpose | Key properties |
|---|---|---|
Application |
Entry point, manages renderer and ticker | stage, ticker, screen, canvas |
Sprite |
Displays a texture | texture, anchor, tint, alpha |
Container |
Groups display objects | children, sortableChildren, zIndex |
Graphics |
Vector drawing | rect(), circle(), fill(), stroke() |
Text |
Renders text | text, style, resolution |
AnimatedSprite |
Frame-based animation | textures, animationSpeed, currentFrame |
ParticleContainer |
High-performance sprite batch | maxSize, properties |
Ticker |
Game loop manager | deltaTime, FPS, speed |
Asset loading patterns
// Single asset
const texture = await Assets.load('hero.png');
const sprite = new Sprite(texture);
// Bundled assets
Assets.addBundle('level1', {
background: 'bg.png',
tileset: 'tiles.json',
music: 'theme.mp3',
});
await Assets.loadBundle('level1');
// With progress tracking
Assets.loadBundle('level1', (progress) => {
console.log(`Loading: ${Math.round(progress * 100)}%`);
});
Sprite operations
const sprite = Sprite.from(texture);
sprite.anchor.set(0.5); // Center origin
sprite.position.set(400, 300); // Set position
sprite.scale.set(2); // Uniform scale
sprite.rotation = Math.PI / 4; // Rotate 45 degrees
sprite.alpha = 0.8; // Semi-transparent
sprite.tint = 0xff6b6b; // Color tint
sprite.eventMode = 'static'; // Enable interaction
sprite.cursor = 'pointer'; // Pointer cursor
Game loop
app.ticker.add((ticker) => {
// ticker.deltaTime = 1.0 at 60fps, scales with frame time
sprite.x += speed * ticker.deltaTime;
sprite.rotation += 0.01 * ticker.deltaTime;
});
// Control ticker
app.ticker.speed = 0.5; // Half speed
app.ticker.stop(); // Pause
app.ticker.start(); // Resume
Drag pattern
let dragging = false;
sprite.eventMode = 'static';
sprite.on('pointerdown', () => { dragging = true; });
app.stage.eventMode = 'static';
app.stage.hitArea = app.screen;
app.stage.on('pointermove', (e) => {
if (dragging) sprite.position.copyFrom(e.global);
});
app.stage.on('pointerup', () => { dragging = false; });
Graphics drawing
const g = new Graphics();
g.rect(0, 0, 200, 100).fill({ color: 0x3498db, alpha: 0.8 });
g.circle(100, 100, 50).fill(0xe74c3c).stroke({ color: 0xffffff, width: 2 });
g.moveTo(0, 0).lineTo(200, 200).stroke({ color: 0x2ecc71, width: 3 });
g.roundRect(10, 10, 180, 80, 12).fill(0x9b59b6);
Text rendering
const text = new Text({
text: 'Score: 0',
style: {
fontFamily: 'Arial',
fontSize: 36,
fontWeight: 'bold',
fill: 0xffffff,
stroke: { color: 0x000000, width: 4 },
dropShadow: { color: 0x000000, blur: 4, distance: 2 },
},
});
text.anchor.set(0.5);
AnimatedSprite from spritesheet
import { AnimatedSprite, Spritesheet } from 'pixi.js';
const sheet = await Assets.load('character.json');
const walkFrames = sheet.animations['walk']; // array of textures
const character = new AnimatedSprite(walkFrames);
character.animationSpeed = 0.15;
character.play();
// Control
character.stop();
character.gotoAndPlay(0);
character.loop = true;
character.onComplete = () => { /* animation finished */ };
Object pooling
const pool = [];
function getSprite(texture) {
const sprite = pool.pop() || new Sprite(texture);
sprite.visible = true;
return sprite;
}
function releaseSprite(sprite) {
sprite.visible = false;
pool.push(sprite);
}
Scene management
const scenes = { menu: new Container(), game: new Container() };
function switchScene(name) {
Object.values(scenes).forEach(s => s.visible = false);
scenes[name].visible = true;
}
app.stage.addChild(scenes.menu, scenes.game);
switchScene('menu');
Performance tips
- Use
ParticleContainerfor 10k+ identical sprites - Each filter forces a render-texture pass — limit them
- Pool and reuse objects rather than creating/destroying every frame
- Texture atlases reduce draw calls (PixiJS batches automatically)
- Always call
destroy()on removed display objects to free GPU memory - Avoid frequent
blendModechanges — they break batching - Profile with
app.ticker.FPSand the browser DevTools Performance tab