Refactoring Enemy AI for a Top-Down Shooter

Summary

The development team encountered a logic architectural failure when attempting to transition a side-scrolling game engine to a top-down shooter. While the core loop and collision detection were functional, the Enemy class lacked the necessary spatial awareness to interact with the player’s coordinates. The engine was hardcoded for linear, one-dimensional movement (X-axis only), making complex AI behaviors like “chasing” impossible without a fundamental refactor of the update logic.

Root Cause

The failure stemmed from two primary issues:

  • Hardcoded Movement Vectors: The Enemy.update() method only modified this.x using a constant this.speed. This assumes all enemies move in a straight horizontal line, which is a side-scroller paradigm.
  • Lack of Dependency Injection for Target Data: The Enemy class had access to the game instance, but the update() method was not designed to perform vector mathematics or reference the player object’s position to calculate a direction.
  • Missing Trigonometry: To move toward a target in a 2D space, an entity requires angular velocity or at least a normalized direction vector derived from the difference between the target’s position and its own.

Why This Happens in Real Systems

In large-scale software engineering, this is known as Rigid Design.

  • Domain Overfitting: The code was “overfitted” to the tutorial’s specific use case (side-scrolling). When the domain changed (top-down), the underlying logic became obsolete.
  • Violation of the Open/Closed Principle: The Enemy class was not “open for extension.” To change behavior, one had to rewrite the core update loop rather than extending it with new movement strategies.
  • Implicit Assumptions: Developers often write code that works for “Scenario A” and forget to define the mathematical boundaries required for “Scenario B.”

Real-World Impact

  • Technical Debt: Attempting to “patch” the movement by adding if/else statements inside the existing update method creates spaghetti code.
  • Scalability Bottlenecks: As more enemy types are added (e.g., flying enemies, patrolling enemies), the Enemy class becomes a God Object, handling too many responsibilities and becoming impossible to maintain.
  • Performance Degradation: Calculating complex paths for hundreds of entities using inefficient conditional logic can lead to frame drops in the main loop.

Example or Code (if necessary and relevant)

class Enemy {
    constructor(game) {
        this.game = game;
        this.x = this.game.width;
        this.y = Math.random() * this.game.height;
        this.speed = 1.5;
        this.markedForDeletion = false;
        this.lives = 5;
        this.score = this.lives;
    }

    update() {
        // Calculate direction vector towards player
        const dx = this.game.player.x - this.x;
        const dy = this.game.player.y - this.y;
        const distance = Math.sqrt(dx * dx + dy * dy);

        if (distance > 0) {
            // Normalize the vector and apply speed
            this.x += (dx / distance) * this.speed;
            this.y += (dy / distance) * this.speed;
        }

        // Cleanup if enemy leaves bounds
        if (this.x  this.game.width || this.y  this.game.height) {
            this.markedForDeletion = true;
        }
    }

    draw(context) {
        context.fillStyle = 'red';
        context.fillRect(this.x, this.y, this.width, this.height);
        context.fillStyle = 'black';
        context.font = '20px Helvetica';
        context.fillText(this.lives, this.x, this.y);
    }
}

How Senior Engineers Fix It

A senior engineer would implement a Strategy Pattern or a Component-Based Architecture to decouple movement from the entity itself.

  • Decouple Movement Logic: Instead of hardcoding movement in Enemy.update(), we would pass a MovementStrategy object to the enemy. One strategy could be LinearMovement, another ChaseMovement.
  • Vector Math Library: Use (or implement) a small vector utility to handle addition, subtraction, and normalization. This prevents manual calculation errors and makes the code readable.
  • State Machines: Implement a Finite State Machine (FSM). An enemy could have states like IDLE, CHASE, and ATTACK. The update method would simply delegate to the current state’s logic.
  • Dependency Injection: Ensure the update method receives the necessary environmental context (like the player position) explicitly, rather than relying on side effects of the game object.

Why Juniors Miss It

  • Focus on “What” instead of “How”: Juniors often focus on the immediate goal (“Make it move toward the player”) by adding localized hacks, rather than asking “How should movement be architected in a 2D plane?”
  • Lack of Mathematical Foundation: Many beginners overlook the necessity of Vector Normalization. They might try to do this.x += player.x - this.x, which causes the enemy to accelerate infinitely toward the player rather than moving at a constant speed.
  • Tutorial Trap: They follow the logic of a specific implementation (the tutorial) as a universal law rather than a specific instance of a broader pattern.

Leave a Comment