Towers: Targeting, Shooting & Blocking the Grid
Towers do two jobs: shoot enemies that wander into range, and, the mechanic that ties this whole course together, block the grid so enemies have to path around them.
The Tower struct
struct Tower {
GridPos gridPos;
Vector2 pixelPos;
float range = 100.f;
float damage = 10.f;
float fireRate = 1.f; // shots per second
float cooldown = 0.f;
};
Targeting and shooting
void updateTower(Tower& tower, std::vector<Enemy>& enemies, float deltaTime) {
tower.cooldown -= deltaTime;
if (tower.cooldown > 0.f) return; // still reloading
for (Enemy& enemy : enemies) {
if (enemy.health <= 0.f || enemy.reachedGoal) continue;
if (Vector2Distance(tower.pixelPos, enemy.position) <= tower.range) {
enemy.health -= tower.damage;
tower.cooldown = 1.f / tower.fireRate;
break; // one shot per cooldown, stop after the first valid target
}
}
}
tower.cooldown counts down every frame by deltaTime, and resets to 1.f / tower.fireRate the instant a shot fires, a tower with fireRate = 2.f reloads in half a second, one with fireRate = 0.5f takes two full seconds. This is the same "accumulate/reset a timer" pattern as the horse race course's per-tick movement, just applied to shooting instead of distance.
Placing a tower: the maze-blocking rule
This is the payoff for everything built so far. A player should be able to block some of the path to slow enemies down, funneling them past more towers, but never seal off the goal entirely. Checking that is exactly one extra call to findPath:
bool tryPlaceTower(GridPos pos, GridPos startPos, GridPos goalPos, std::vector<Tower>& towers) {
if (grid[pos.row][pos.col] != CellType::Empty) {
return false; // already a wall, or the start/goal cell
}
grid[pos.row][pos.col] = CellType::Wall; // tentatively block it
std::vector<GridPos> testPath = findPath(startPos, goalPos);
if (testPath.empty()) {
grid[pos.row][pos.col] = CellType::Empty; // undo, this would have sealed off the goal
return false;
}
Tower tower;
tower.gridPos = pos;
tower.pixelPos = gridToPixel(pos);
towers.push_back(tower);
return true; // caller should recompute waypoints from testPath for every enemy in play
}
The block-then-test-then-maybe-undo sequence is the whole trick: mark the cell as a wall before checking, run the exact same findPath from the earlier lesson, and only keep the change if a path still exists. If it doesn't, revert the one cell that was just changed, no other state needs touching, since nothing else was modified yet.
WARNING
tryPlaceTower finds a valid path, but doesn't rebuild every enemy's waypoints itself. After a successful placement, the caller still needs to convert testPath to waypoints and reset each enemy's currentWaypoint to 0, exactly the reroute mechanism from the end of the last lesson, otherwise enemies keep walking a path that may now be blocked.