Stupid Enemy

A wall-bouncing enemy is the simplest moving threat: each frame, advance by moveX/moveY; if the next position hits a solid tile, reverse direction. That’s the entire AI.

Loading editor…

Why simple enemies work

Pac-Man’s ghosts use simple chase and scatter rules. Mario’s Goombas walk in straight lines. Space Invaders move in formation. None of these are complex behaviours, but all of them create spatial and timing challenges for the player.

Predictable patterns are fair — players can learn them and improve. Simple movement is also cheap to compute, so you can run large numbers of enemies without performance concerns. The challenge in enemy design comes almost entirely from placement, not from the AI itself.

Enemy type definitions

Store enemy properties in a lookup object, one entry per type:

const ENEMY_TYPES = {
    HORIZONTAL_PATROL: {
        color: 0x8A2BE2,
        moveX: 1,   // Moves right initially
        moveY: 0,
        speed: 1,
        size: 10
    },
    VERTICAL_PATROL: {
        color: 0x00CED1,
        moveX: 0,
        moveY: 1,   // Moves down initially
        speed: 1,
        size: 10
    },
    FAST_HORIZONTAL: {
        color: 0x32CD32,
        moveX: 1,
        moveY: 0,
        speed: 2,
        size: 8
    }
};

Store spawn positions separately — one per-level array describing which type appears at which tile:

const levelEnemies = {
    1: [
        { type: 'HORIZONTAL_PATROL', tileX: 2, tileY: 1 },
        { type: 'VERTICAL_PATROL',   tileX: 8, tileY: 1 },
        { type: 'FAST_HORIZONTAL',   tileX: 1, tileY: 5 }
    ]
};

Spawning enemies

Convert spawn data to live enemy objects and add them to the stage:

const enemies = [];

function createEnemy(typeName, tileX, tileY) {
    const type = ENEMY_TYPES[typeName];
    if (!type) return;

    const sprite = new PIXI.Graphics()
        .rect(0, 0, type.size, type.size)
        .fill(type.color);

    const enemy = {
        sprite,
        type: typeName,
        x: tileX * TILE_SIZE + (TILE_SIZE - type.size) / 2,
        y: tileY * TILE_SIZE + (TILE_SIZE - type.size) / 2,
        width:  type.size,
        height: type.size,
        moveX: type.moveX,
        moveY: type.moveY,
        speed: type.speed
    };

    sprite.x = enemy.x;
    sprite.y = enemy.y;
    app.stage.addChild(sprite);
    enemies.push(enemy);
}

Enemy movement

Each frame: calculate next position, check for a wall, reverse direction if blocked, otherwise move:

function updateEnemies() {
    enemies.forEach(enemy => {
        const nextX = enemy.x + enemy.moveX * enemy.speed;
        const nextY = enemy.y + enemy.moveY * enemy.speed;

        if (wouldHitWall(nextX, nextY, enemy.width, enemy.height)) {
            // Reverse on the same axis
            enemy.moveX = -enemy.moveX;
            enemy.moveY = -enemy.moveY;
        } else {
            enemy.x = nextX;
            enemy.y = nextY;
        }

        enemy.sprite.x = enemy.x;
        enemy.sprite.y = enemy.y;
    });
}

The direction vector (moveX, moveY) starts at (1, 0) for a horizontal patroller. On hitting a wall it becomes (-1, 0), then (1, 0) again. The enemy oscillates between its two boundary walls indefinitely.

Player collision

Check AABB overlap between each enemy and the player after positions are updated:

function checkEnemyPlayerCollision(enemy) {
    if (!player.alive) return;

    const hit = enemy.x < player.x + player.width  &&
                enemy.x + enemy.width  > player.x  &&
                enemy.y < player.y + player.height &&
                enemy.y + enemy.height > player.y;

    if (hit) handlePlayerHit();
}

function handlePlayerHit() {
    player.alive = false;
    player.sprite.tint = 0x666666;

    setTimeout(() => {
        player.alive = true;
        player.sprite.tint = 0xFFFFFF;
        player.x = 60;
        player.y = 180;
    }, 1500);
}

AABB (axis-aligned bounding box) tests whether two rectangles overlap. It’s more accurate for rectangular sprites than a circular distance check and avoids the Math.sqrt call.

Game loop integration

function gameLoop() {
    handleInput();
    updateEnemies();
    updatePhysics();
}

app.ticker.add(gameLoop);

What you built:

  • Enemy type definitions with movement direction and speed
  • A spawning function that converts tile coordinates to pixel positions
  • Wall-reversal movement: advance, check for solid tile, reverse if blocked
  • AABB collision detection between enemies and the player

Next: Bringing it Together