Build a Tower Defense Game using C++, Raylib & A* Pathfinding

Lesson 5 of 8

Enemies: Following the Path

findPath returns grid cells, but an enemy needs to move smoothly through pixel space, not teleport from cell center to cell center. Convert the path once, then walk it gradually every frame.

From grid path to pixel waypoints

std::vector<Vector2> pathToWaypoints(const std::vector<GridPos>& path) {
    std::vector<Vector2> waypoints;
    for (const GridPos& pos : path) {
        waypoints.push_back(gridToPixel(pos));
    }
    return waypoints;
}

Do this once, right after findPath returns, not every frame, the grid path doesn't change until a tower is placed, so there's no reason to recompute pixel positions from it repeatedly.

The Enemy struct

#include "raymath.h"

struct Enemy {
    Vector2 position;
    int currentWaypoint = 0;
    float speed = 100.f; // pixels per second
    float health = 100.f;
    bool reachedGoal = false;
};

raymath.h is raylib's companion header for vector math, Vector2 addition, subtraction, distance, and normalization, all the operations movement along a path needs, without hand-rolling them.

Moving along waypoints

void updateEnemy(Enemy& enemy, const std::vector<Vector2>& waypoints, float deltaTime) {
    if (enemy.currentWaypoint >= (int) waypoints.size()) {
        enemy.reachedGoal = true;
        return;
    }

    Vector2 target = waypoints[enemy.currentWaypoint];
    float distance = Vector2Distance(enemy.position, target);

    if (distance < 4.f) {
        enemy.currentWaypoint++; // close enough, advance to the next waypoint
        return;
    }

    Vector2 direction = Vector2Normalize(Vector2Subtract(target, enemy.position));
    enemy.position = Vector2Add(enemy.position, Vector2Scale(direction, enemy.speed * deltaTime));
}
  • Vector2Subtract(target, enemy.position) gives the raw direction vector, pointing from the enemy toward the current waypoint, but its length is however far away the waypoint happens to be.
  • Vector2Normalize(...) rescales that to a length of exactly 1, "which way to go," with the distance stripped out.
  • Vector2Scale(direction, enemy.speed * deltaTime) turns that pure direction back into "how far to move this frame," at a fixed speed, regardless of how far the current waypoint happens to be.
  • distance < 4.f is a small tolerance, not an exact == 0, floating-point movement will essentially never land on a waypoint's exact pixel, so "close enough" is what actually advances the enemy through the path.

Drawing enemies

void drawEnemy(const Enemy& enemy) {
    DrawCircleV(enemy.position, 10.f, RED);
}

TIP

currentWaypoint is exactly what makes a mid-path reroute possible later: when a tower blocks the grid and findPath runs again, converting the new path to waypoints and resetting currentWaypoint to 0 (rather than reusing the old index) is enough to make an enemy smoothly pick up the new route from wherever it currently stands.

📝 Enemy Movement Quiz

Passing score: 70%
  1. 1.Why convert the grid path to pixel waypoints once, instead of every frame?

  2. 2.Vector2Normalize returns a vector pointing in the same direction but with length 1.

  3. 3.updateEnemy checks distance < 4.f instead of distance == 0 because floating-point movement will almost never land exactly on a waypoint's ____ pixel.