Shoot Him
Projectiles are a common interaction mechanic — the player fires in the direction they last moved, and bullets that reach an enemy remove it from the level. This tutorial adds a bullet system to the side-scrolling platformer from previous tutorials.
Bullet Data Structure
A bullet is a plain object with a position, a velocity, a size, and a sprite. The size matters because collision is AABB (axis-aligned bounding box), not circular distance — two rectangles overlap when neither axis is separated:
// A bullet object
const bullet = {
sprite: new PIXI.Graphics().rect(0, 0, 4, 4).fill(0xFFFF00),
x: startX,
y: startY,
vx: direction.x * BULLET_SPEED,
vy: direction.y * BULLET_SPEED,
width: 4,
height: 4
};
app.stage.addChild(bullet.sprite);
bullets.push(bullet);
Object Pooling
Creating and destroying sprites every shot causes garbage collection pauses. Object pooling keeps a fixed set of bullet objects and recycles them:
const bulletPool = [];
function getBullet() {
// Reuse a pooled bullet, or create a new one
const bullet = bulletPool.length > 0 ? bulletPool.pop() : {
sprite: new PIXI.Graphics().rect(0, 0, 4, 4).fill(0xFFFF00),
width: 4, height: 4
};
bullet.sprite.visible = true;
app.stage.addChild(bullet.sprite);
return bullet;
}
function returnBullet(bullet) {
bullet.sprite.visible = false;
bulletPool.push(bullet);
}
Shoot Cooldown
A lastShot timestamp enforces minimum time between shots. Check Date.now() - player.lastShot > player.shootCooldown before creating a bullet, then update player.lastShot = Date.now() when firing.
Bullet–Enemy AABB Collision
The demo uses rectangle overlap rather than circular distance, which is more accurate for rectangular sprites:
// Returns true when rect A overlaps rect B
function rectsOverlap(ax, ay, aw, ah, bx, by, bw, bh) {
return ax < bx + bw && ax + aw > bx &&
ay < by + bh && ay + ah > by;
}
Iterate bullets and enemies in reverse order (length - 1 to 0) when splicing, so earlier indices stay valid after each removal.
Particle Effects
Spawn short-lived graphics objects on enemy death and animate them through the PIXI ticker — not requestAnimationFrame, which runs outside PixiJS’s render loop:
function spawnParticles(cx, cy) {
for (let i = 0; i < 6; i++) {
const p = new PIXI.Graphics().rect(0, 0, 3, 3).fill(0xFF6600);
p.x = cx; p.y = cy;
const angle = (i / 6) * Math.PI * 2;
const speed = 2 + Math.random() * 2;
const pvx = Math.cos(angle) * speed;
const pvy = Math.sin(angle) * speed;
app.stage.addChild(p);
// Animate via ticker, remove when faded
const tick = () => {
p.x += pvx; p.y += pvy; p.alpha -= 0.06;
if (p.alpha <= 0) { app.stage.removeChild(p); app.ticker.remove(tick); }
};
app.ticker.add(tick);
}
}
Next: Depth