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.

Loading editor…

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