Lesson 05

Enemy AI and objectives: how a canvas arena becomes a real game.

A player, a sword and a blank arena are not enough. A game becomes interesting when enemies have roles, levels have objectives, and the player understands what to do next.

IntermediateAction gamesAI30-45 minutes

1. Use configuration before writing new code

Level 33 hack-and-slash games share an action core, but each game can feel different because configuration changes theme, enemy roles, objectives and rewards. This is better than copying twenty nearly identical games and editing random lines.

const gameConfig = {
    title: "Vampire Crypt",
    hero: "Dawn Hunter",
    enemyTypes: ["bat", "thrall", "vampire", "ancient"],
    objectiveType: "purify",
    bossName: "Count Nocturne",
    palette: {
        floor: "#1b1024",
        wall: "#3a1735",
        accent: "#ff4d8d"
    }
};

The exact names differ by game, but the principle stays the same: theme belongs in data. Shared movement, collision and combat can live in a core file.

2. Give enemies roles, not just hit points

An enemy with more health is not automatically more interesting. In Supagames action games, enemy traits can change behavior: ranged enemies shoot, splitters spawn smaller threats, vampires drain, undead return slowly, bosses change pressure.

function spawnEnemy(type, x, y) {
    const traits = ENEMY_TRAITS[type] || {};

    return {
        type,
        x,
        y,
        w: traits.size || 28,
        h: traits.size || 28,
        hp: traits.hp || 3,
        speed: traits.speed || 1.2,
        damage: traits.damage || 1,
        ranged: !!traits.ranged,
        drain: !!traits.drain,
        split: !!traits.split,
        boss: !!traits.boss,
        cooldown: 0
    };
}

Source pattern: Level 33 action core.

Once enemies have roles, the player can learn. A bat rushes. A cultist shoots. A vampire must be kited. A boss needs patience. That is already more gameplay than ten enemies with different colors.

3. Projectiles should know who fired them

Projectiles become easier to manage when they include an owner field. The same array can contain player shots and enemy shots, but collision rules stay clear.

projectiles.push({
    owner: "player",
    x: player.x,
    y: player.y - 8,
    vx: dx / distance * 7,
    vy: dy / distance * 7,
    dmg: damage,
    color: "#ffd93d",
    size: 6,
    life: 90
});

Then update code can say: if owner is player, test enemies; if owner is enemy, test player. That is much clearer than having separate arrays that drift out of sync.

4. Objectives make maps feel purposeful

A dungeon where you only kill everything can work for one level. After that, objectives create variety. Protect a shrine, break seals, collect relics, survive a ritual, escort a courier, open a gate or defeat a named boss.

function objectiveComplete(state) {
    if (state.objective.type === "clear") {
        return state.enemies.length === 0;
    }

    if (state.objective.type === "collect") {
        return state.collectedRelics >= state.objective.required;
    }

    if (state.objective.type === "purify") {
        return state.purifiedShrines >= state.objective.required;
    }

    return false;
}

The code is simple, but the result is big: the player knows what progress means. It also gives each game theme a way to matter. Vampire games can purify coffins. Robot games can disable cores. Demon games can seal gates.

5. Theme the objective, not only the title

If a game is called Vampire Crypt, the player should do vampire-related things. They might carry sunlight charges, break blood altars, avoid mist-form enemies and fight a count who summons bats. If a game is called Robot Foundry, the verbs should change: overload generators, dodge conveyor belts, disable turrets and escape before the reactor vents.

This is how a shared engine avoids feeling repetitive. The code can still use common objective types, but the labels, map props, enemy roles and final encounter should match the fantasy.

const objectiveLabels = {
    vampire: "Purify the blood altars",
    robot: "Disable the foundry cores",
    demon: "Seal the ember gates",
    forest: "Wake the guardian roots"
};

function objectiveLabel(theme) {
    return objectiveLabels[theme] || "Complete the mission";
}

Players notice when a name promises one thing and the mechanics deliver another. Good theming is not just art. It is the connection between title, objective, enemy behavior and reward.

6. Damage needs rhythm

One bug we often watch for is enemies touching the player and dealing damage every frame. At 60 frames per second, that can delete the player instantly. Use invulnerability time or enemy attack cooldowns.

if (rectHit(player, enemy) && player.invulnerableTime <= 0) {
    player.hp -= enemy.damage;
    player.invulnerableTime = 45;
    knockBack(player, enemy);
}

This makes contact dangerous without feeling broken. The player gets feedback, a moment to recover and a chance to move.

7. Progression should change decisions

Adding +1 damage after every level is easy, but not always interesting. A stronger progression system gives the player choices that change behavior. For example: a wider slash is safer against swarms, a faster dash helps against ranged enemies, and a pierce projectile rewards lining enemies up.

const upgrades = [
    { id: "wideSlash", label: "Wider slash arc", apply: p => p.arc += 0.35 },
    { id: "dash", label: "Short dash cooldown", apply: p => p.dashCooldown *= 0.75 },
    { id: "pierce", label: "Projectiles pierce once", apply: p => p.pierce += 1 }
];

When upgrades change decisions, a 10-minute game can stretch toward a 30-minute session because the player wants to try another build. This is the same reason tower defense, roguelikes and card battlers can stay engaging with relatively simple graphics.

Design checklist for a 15-30 minute browser game

  • At least three enemy roles with visible behavior differences.
  • Objectives that match the title and theme, not generic filler.
  • A short tutorial or first encounter that teaches controls safely.
  • Readable upgrades that change how the player plays, not only bigger numbers.
  • A final encounter or epilogue so the session feels complete.
  • Mobile controls that do not hide enemies, objectives or health.

The core idea: variety should come from meaningful rules, not from renaming the same enemy ten times.

Play: A level 33 action example. Back: Return to the full learning path.