โš™๏ธ My First C++ Game ยท Episode 7 of 7 ยท See All Episodes
โš”๏ธ Episode 7 ยท Series Finale ยท Tower Defence Part 2 of 2

Wave After Wave!
Tower Defence Part 2

The grand finale: towers open fire with homing bullets, enemies arrive in escalating waves, kills pay bounties, towers upgrade, and a boss leads the final assault. Ship your biggest C++ project.

๐Ÿ‘ถ Ages 12+ โฑ๏ธ ~2 Hours โš™๏ธ C++ & SFML โœ“ Free
๐Ÿ”ซ Firing & cooldowns ๐ŸŽฏ Homing bullets ๐ŸŒŠ Wave system ๐Ÿ’€ Bounties โฌ†๏ธ Upgrades ๐Ÿ‰ Boss wave
โญ
0 XP
Level 1
๐Ÿ”ฅ0
Your Progress 0 / 6 steps
โš ๏ธ
Heads up: SFML 3 renamed a few things. The install step gives you SFML 3 today, but the short snippets in the steps below are written in the classic SFML 2 style. The differences are small and mechanical: sf::VideoMode({800, 500})sf::VideoMode({800, 500}) (curly braces), the new event loop while (const std::optional e = window.pollEvent())while (const std::optional e = window.pollEvent()) with e->is<sf::Event::Closed>()e->is<sf::Event::Closed>(), sf::Keyboard::Key::Leftsf::Keyboard::Key::Left (add ::Key::Key), and setPosition({x, y})setPosition({x, y}) / move({dx, dy})move({dx, dy}) (curly braces). The โœ… complete finished code at the bottom of this page is full SFML 3 and compiles on a fresh install, use it as your reference.
๐Ÿงฉ
Need the game so far? This episode continues Tower Defence Part 1, so it assumes you already have that project. Open your Part 1 file to keep building, or revisit it here: Tower Defence Part 1 โ†’ ยท C++ Cheatsheet โ†’
1
๐Ÿ”ซ
Towers Open Fire
Cooldown timers plus your nearestInRange from Part 1
Active
๐ŸŽฏ
Goal for this step

Make towers spawn bullets at the nearest target on a cooldown.

  • 1Continue in your Part 1 project, every system plugs in.
  • 2Bullet struct: position, target enemy index/pointer, speed 340, damage copied from the tower.
  • 3Each tower: cooldown โˆ’= dt; at โ‰ค 0 with a target from nearestInRange โ†’ push a bullet, reset cooldown to 0.7 s.
  • 4Draw bullets as small yellow circles.
tower.cpp
struct Bullet {
    sf::Vector2f pos;
    Enemy* target;
    float speed = 340;
    int damage;
};

std::vector<Bullet> bullets;

for (auto& t : towers) {
    t.cooldown -= dt;
    if (t.cooldown <= 0) {
        Enemy* target = nearestInRange(t, enemies);
        if (target) {
            bullets.push_back({ t.pos, target, 340, t.damage });
            t.cooldown = 0.7f;
        }
    }
}struct Bullet {
    sf::Vector2f pos;
    Enemy* target;
    float speed = 340;
    int damage;
};

std::vector<Bullet> bullets;

for (auto& t : towers) {
    t.cooldown -= dt;
    if (t.cooldown <= 0) {
        Enemy* target = nearestInRange(t, enemies);
        if (target) {
            bullets.push_back({ t.pos, target, 340, t.damage });
            t.cooldown = 0.7f;
        }
    }
}
โœ๏ธ
Fill in the Blanks
+15 XP
A tower may fire when its reaches zero AND nearestInRange returns a target instead of . Firing resets the cooldown to 0.7 s.
๐Ÿง 
Knowledge Check
+15 XP
Why does the cooldown only reset when a shot is actually fired?
ATo save memory
BSo a tower with no targets fires the INSTANT one appears, rather than being mid-cooldown for nothing
CCooldowns cannot reset twice
2
๐ŸŽฏ
Homing Bullets
Chase the target using Part 1 vector maths
Locked
๐ŸŽฏ
Goal for this step

Bullets home in, hit, damage, and clean themselves up.

  • 1Bullet update = enemy path-following aimed at a moving point: normalise (target โˆ’ pos), move by speedยทdt.
  • 2Within 8 px: apply damage, mark the bullet dead. Dead or escaped target? Kill the bullet too (its pointer is stale!).
  • 3Enemy hp โ‰ค 0 โ†’ alive = false and pay the bounty: gold += e.bounty;
  • 4Clean dead bullets with erase-remove (Episode 3-4 skill). Fire away!
tower.cpp
for (auto& b : bullets) {
    if (!b.target->alive) { b.dead = true; continue; }
    sf::Vector2f d = b.target->pos - b.pos;
    float len = std::sqrt(d.x*d.x + d.y*d.y);
    if (len < 8.f) {
        b.target->hp -= b.damage;
        if (b.target->hp <= 0) {
            b.target->alive = false;
            gold += b.target->bounty;
        }
        b.dead = true;
    } else {
        b.pos += (d / len) * b.speed * dt;
    }
}
bullets.erase(std::remove_if(bullets.begin(), bullets.end(),
    [](const Bullet& b){ return b.dead; }), bullets.end());for (auto& b : bullets) {
    if (!b.target->alive) { b.dead = true; continue; }
    sf::Vector2f d = b.target->pos - b.pos;
    float len = std::sqrt(d.x*d.x + d.y*d.y);
    if (len < 8.f) {
        b.target->hp -= b.damage;
        if (b.target->hp <= 0) {
            b.target->alive = false;
            gold += b.target->bounty;
        }
        b.dead = true;
    } else {
        b.pos += (d / len) * b.speed * dt;
    }
}
bullets.erase(std::remove_if(bullets.begin(), bullets.end(),
    [](const Bullet& b){ return b.dead; }), bullets.end());
โŒจ๏ธ
Code Challenge
+20 XP
Pay the bounty on a kill:
tower.cpp
b.target->hp -= b.b.target->hp -= b.;
if (b.target->hp <= ;
if (b.target->hp <= ) {
    b.target->alive = false;
    gold += b.target->) {
    b.target->alive = false;
    gold += b.target->;
};
}
๐Ÿ’ก Hint: The bullet carries its damage; death is zero-or-less hp; the kill reward was staged in Part 1.
๐Ÿง 
Knowledge Check
+15 XP
Why must a bullet die when its target dies?
ABullets are expensive
BIts target pointer now refers to a dead enemy, chasing it is meaningless and using it after removal is dangerous
CSFML limits bullets
3
๐ŸŒŠ
The Wave System
Timed spawning of escalating enemy groups
Locked
๐ŸŽฏ
Goal for this step

Spawn enemies in numbered waves with growing difficulty.

  • 1Wave state: number, enemies left to spawn, a spawn-gap timer (0.8 s), and a between-waves rest (5 s).
  • 2Wave n: 4 + n*2 enemies, hp 20 + n*8, speed 70 + n*4.
  • 3All spawned AND all dead/escaped โ†’ rest timer โ†’ next wave.
  • 4HUD: "Wave 3, 6 enemies remain" and "Next wave in 3โ€ฆ" during rests. Information is a tower defence playerโ€™s oxygen.
tower.cpp
int wave = 0, toSpawn = 0;
float spawnGap = 0, rest = 3;

if (toSpawn == 0 && enemiesCleared(enemies)) {
    rest -= dt;
    if (rest <= 0) {
        wave++;
        toSpawn = 4 + wave * 2;
        rest = 5;
    }
} else if (toSpawn > 0) {
    spawnGap -= dt;
    if (spawnGap <= 0) {
        Enemy e;
        e.pos = path[0];
        e.hp = 20 + wave * 8;
        e.speed = 70 + wave * 4;
        enemies.push_back(e);
        toSpawn--;
        spawnGap = 0.8f;
    }
}int wave = 0, toSpawn = 0;
float spawnGap = 0, rest = 3;

if (toSpawn == 0 && enemiesCleared(enemies)) {
    rest -= dt;
    if (rest <= 0) {
        wave++;
        toSpawn = 4 + wave * 2;
        rest = 5;
    }
} else if (toSpawn > 0) {
    spawnGap -= dt;
    if (spawnGap <= 0) {
        Enemy e;
        e.pos = path[0];
        e.hp = 20 + wave * 8;
        e.speed = 70 + wave * 4;
        enemies.push_back(e);
        toSpawn--;
        spawnGap = 0.8f;
    }
}
โœ๏ธ
Fill in the Blanks
+15 XP
Wave n spawns 4 + nร— enemies with a second gap between them, followed by a second rest before the next wave.
๐Ÿง 
Knowledge Check
+15 XP
The rest period between waves exists so players canโ€ฆ
AGet snacks
BSpend their bounty gold, build and upgrade between assaults. Tension, release, decision. That is the TD loop
CLet the CPU cool down
4
โฌ†๏ธ
Tower Upgrades
Right-click to upgrade: damage, range, fire rate
Locked
๐ŸŽฏ
Goal for this step

Add a 3-level upgrade path that changes the maths meaningfully.

  • 1Tower gains int level = 1. Right-clicking one (mouse within 20 px of its centre) upgrades if gold allows.
  • 2Cost 40ร—level. Effects per level: damage +4, range +25, cooldown ร—0.8.
  • 3Show level as small pips on the tower; deepen its colour per level.
  • 4Strategy emerges: three level-1 towers or one level-3? (Spoiler: focused fire usually wins, let players discover it.)
tower.cpp
// on right-click near tower t:
int cost = 40 * t.level;
if (gold >= cost && t.level < 3) {
    gold -= cost;
    t.level++;
    t.damage += 4;
    t.range  += 25;
    t.cooldown *= 0.8f;    // faster firing
}// on right-click near tower t:
int cost = 40 * t.level;
if (gold >= cost && t.level < 3) {
    gold -= cost;
    t.level++;
    t.damage += 4;
    t.range  += 25;
    t.cooldown *= 0.8f;    // faster firing
}
โŒจ๏ธ
Code Challenge
+20 XP
Apply the level-up bonuses:
tower.cpp
t.level++;
t.damage += t.level++;
t.damage += ;
t.range  += ;
t.range  += ;
t.cooldown *= ;
t.cooldown *= f;f;
๐Ÿ’ก Hint: Damage grows by four, range by twenty-five, and the time between shots shrinks to 80%.
๐Ÿง 
Knowledge Check
+15 XP
Multiplying cooldown by 0.8 per level (instead of subtracting) meansโ€ฆ
AUpgrades get weaker
BCompounding: 0.7s โ†’ 0.56s โ†’ 0.45s, each level stays a meaningful % boost without ever hitting zero
CThe tower jams
5
๐Ÿ‰
The Boss Wave
Wave 10: a tank with a health bar leads the charge
Locked
๐ŸŽฏ
Goal for this step

Cap the game with a boss that tests the playerโ€™s whole build.

  • 1Give Enemy one new field: bool isBoss = false;, so a boss can be told apart from regular enemies.
  • 2Wave 10 spawns one BOSS: hp 600, speed 40, bounty 200, drawn twice the size with a floating health bar.
  • 3A boss health bar: red back-bar + green front scaled by hp/maxHp, hovering above it (you have built this bar in two languages!).
  • 4Boss escapes โ†’ instant defeat (lives = 0). Boss dies โ†’ VICTORY screen.
  • 5A slow tank rewards sustained damage, upgraded tower clusters shine, exactly what the player has been learning to build.
tower.cpp
// add to the Enemy struct: bool isBoss = false;

if (wave == 10 && toSpawn == 1) {     // last spawn = the boss
    Enemy boss;
    boss.pos = path[0];
    boss.hp = 600;  boss.maxHp = 600;
    boss.speed = 40;
    boss.bounty = 200;
    boss.isBoss = true;
    enemies.push_back(boss);
    toSpawn = 0;
}

// boss escape = instant loss:
if (e.isBoss && e.wp >= (int)path.size())
    lives = 0;// add to the Enemy struct: bool isBoss = false;

if (wave == 10 && toSpawn == 1) {     // last spawn = the boss
    Enemy boss;
    boss.pos = path[0];
    boss.hp = 600;  boss.maxHp = 600;
    boss.speed = 40;
    boss.bounty = 200;
    boss.isBoss = true;
    enemies.push_back(boss);
    toSpawn = 0;
}

// boss escape = instant loss:
if (e.isBoss && e.wp >= (int)path.size())
    lives = 0;
โœ๏ธ
Fill in the Blanks
+15 XP
The boss has hp but only speed 40, a slow . If it escapes, lives drops instantly to .
๐Ÿง 
Knowledge Check
+15 XP
A good final boss testsโ€ฆ
AReaction speed only
BEverything the game taught: economy, placement, upgrades, a slow tank is an exam on total damage output
CThe pause button
6
๐Ÿ†
Victory, Defeat & Series Wrap
End screens, restart, and your C++ journey reviewed
Locked
๐ŸŽฏ
Goal for this step

Finish the game and take stock of a serious achievement.

  • 1DEFEAT at lives โ‰ค 0; VICTORY when the boss dies. Both freeze play, show wave reached + towers built, R restarts fresh.
  • 2What you used across 7 episodes: compilation & linking, dt physics, structs/classes/enums, deque/vector, pointers & nullptr, normalised vectors, erase-remove, cooldown systems, state machines.
  • 3That list IS an engine-programming foundation. Unreal C++ will read like your own code.
  • 4Series complete. Take the quiz, claim the badge, and go build something nobody has seen before. โš™๏ธ๐ŸŽ‰
๐Ÿ’ก
Want to keep going in C++? Add tower types (slow/splash), a second map, or save the best wave to a file. Every one is within your reach now.
โœ๏ธ
Fill in the Blanks
+15 XP
Across the series you used std::deque for , normalised vectors for following, and nullptr-returning searches for tower .
๐Ÿง 
Knowledge Check
+15 XP
The single most C++-specific superpower you practised in this series isโ€ฆ
ADrawing rectangles
BChoosing precise tools, structs vs classes, deque vs vector, pointers, enums, control over exactly how data lives
CUsing the mouse
โœ… See the complete finished game , full SFML 3, compiles today

The whole game in one file, written in current SFML 3 (what the install step gives you today) and compiled with g++ + SFML 3.0 to confirm it builds. The step snippets above use classic SFML 2 names; this is your working reference. Build it with g++ tower.cpp -o tower -lsfml-graphics -lsfml-window -lsfml-systemg++ tower.cpp -o tower -lsfml-graphics -lsfml-window -lsfml-system then run ./tower./tower (on Windows, from the MSYS2 MinGW terminal so it finds the SFML DLLs).

tower.cpp
// Complete C++ Tower Defence (Parts 1 + 2) for SFML 3. Build:
//   g++ tower.cpp -o tower -lsfml-graphics -lsfml-window -lsfml-system
#include <SFML/Graphics.hpp>
#include <optional>
#include <vector>
#include <cmath>
#include <string>
#include <algorithm>

struct Enemy { sf::Vector2f pos; float speed; int hp, maxHp, bounty; int wp; bool alive; bool isBoss = false; };
struct Tower { sf::Vector2f pos; float range; int damage; float cooldown; };
struct Bullet { sf::Vector2f pos; Enemy* target; float speed; int damage; bool dead; };

int main() {
    sf::RenderWindow window(sf::VideoMode({800, 600}), "C++ Tower Defence");
    window.setFramerateLimit(60);

    std::vector<sf::Vector2f> path = {
        {-20, 120}, {300, 120}, {300, 320}, {560, 320}, {560, 120}, {820, 120}
    };

    std::vector<Enemy> enemies; enemies.reserve(4096);   // reserve so Enemy* stays valid
    std::vector<Tower> towers;
    std::vector<Bullet> bullets;
    int gold = 150, health = 20, wave = 0, toSpawn = 0;
    float spawnGap = 0, rest = 3.f;
    bool won = false, lost = false;
    const int MAX_WAVE = 5, TOWER_COST = 50;

    auto nearPath = [&](float x, float y, float r) {
        for (size_t i = 0; i + 1 < path.size(); i++) {
            sf::Vector2f a = path[i], b = path[i+1];
            sf::Vector2f ab = b - a, ap = sf::Vector2f(x,y) - a;
            float t = (ab.x*ap.x + ab.y*ap.y) / (ab.x*ab.x + ab.y*ab.y);
            t = std::max(0.f, std::min(1.f, t));
            sf::Vector2f c = a + ab * t;
            float dx = x - c.x, dy = y - c.y;
            if (std::sqrt(dx*dx + dy*dy) < r) return true;
        }
        return false;
    };
    auto nearest = [&](Tower& t) -> Enemy* {
        Enemy* best = nullptr; float bd = t.range;
        for (auto& e : enemies) {
            if (!e.alive) continue;
            float dx = e.pos.x - t.pos.x, dy = e.pos.y - t.pos.y;
            float d = std::sqrt(dx*dx + dy*dy);
            if (d < bd) { bd = d; best = &e; }
        }
        return best;
    };

    sf::Clock clock;
    while (window.isOpen()) {
        float dt = clock.restart().asSeconds();
        if (dt > 0.05f) dt = 0.05f;

        while (const std::optional event = window.pollEvent()) {
            if (event->is<sf::Event::Closed>()) window.close();
            if (const auto* m = event->getIf<sf::Event::MouseButtonPressed>()) {
                if (m->button == sf::Mouse::Button::Left && !won && !lost) {
                    int gx = (m->position.x / 40) * 40, gy = (m->position.y / 40) * 40;
                    float cx = gx + 20.f, cy = gy + 20.f;
                    if (!nearPath(cx, cy, 30.f) && gold >= TOWER_COST) {
                        towers.push_back({ {cx, cy}, 110.f, 5, 0.f });
                        gold -= TOWER_COST;
                    }
                }
            }
        }

        if (!won && !lost) {
            // wave director
            bool cleared = true;
            for (auto& e : enemies) if (e.alive) cleared = false;
            if (toSpawn == 0 && cleared) {
                rest -= dt;
                if (rest <= 0) {
                    wave++;
                    if (wave > MAX_WAVE) won = true;
                    else { toSpawn = 4 + wave * 2; rest = 3.f; }
                }
            } else if (toSpawn > 0) {
                spawnGap -= dt;
                if (spawnGap <= 0) {
                    bool boss = (wave == MAX_WAVE && toSpawn == 1);
                    int hp = boss ? 200 : (16 + wave * 4);
                    enemies.push_back({ path[0], boss ? 45.f : 70.f, hp, hp, boss ? 60 : 8, 1, true });
                    toSpawn--; spawnGap = 0.8f;
                }
            }

            // enemies
            for (auto& e : enemies) {
                if (!e.alive) continue;
                if (e.wp >= (int)path.size()) { e.alive = false; if (--health <= 0) lost = true; continue; }
                sf::Vector2f d = path[e.wp] - e.pos;
                float len = std::sqrt(d.x*d.x + d.y*d.y);
                if (len < 4.f) { e.wp++; }
                else e.pos += d / len * e.speed * dt;
            }

            // towers fire
            for (auto& t : towers) {
                t.cooldown -= dt;
                if (t.cooldown <= 0) {
                    Enemy* target = nearest(t);
                    if (target) { bullets.push_back({ t.pos, target, 340.f, t.damage, false }); t.cooldown = 0.7f; }
                }
            }
            // bullets home
            for (auto& b : bullets) {
                if (!b.target->alive) { b.dead = true; continue; }
                sf::Vector2f d = b.target->pos - b.pos;
                float len = std::sqrt(d.x*d.x + d.y*d.y);
                if (len < 8.f) {
                    b.target->hp -= b.damage;
                    if (b.target->hp <= 0) { b.target->alive = false; gold += b.target->bounty; }
                    b.dead = true;
                } else b.pos += d / len * b.speed * dt;
            }
            bullets.erase(std::remove_if(bullets.begin(), bullets.end(),
                [](const Bullet& b){ return b.dead; }), bullets.end());
        }

        // draw
        window.clear(sf::Color(24, 40, 28));
        for (size_t i = 0; i + 1 < path.size(); i++) {
            sf::Vector2f a = path[i], b = path[i+1], d = b - a;
            float len = std::sqrt(d.x*d.x + d.y*d.y);
            sf::RectangleShape seg({len, 36.f});
            seg.setPosition(a);
            seg.setOrigin({0.f, 18.f});
            seg.setRotation(sf::radians(std::atan2(d.y, d.x)));
            seg.setFillColor(sf::Color(70, 62, 48));
            window.draw(seg);
        }
        for (auto& t : towers) {
            sf::CircleShape rng(t.range); rng.setOrigin({t.range, t.range}); rng.setPosition(t.pos);
            rng.setFillColor(sf::Color(96, 165, 250, 24)); window.draw(rng);
            sf::RectangleShape r({30.f, 30.f}); r.setOrigin({15.f, 15.f}); r.setPosition(t.pos);
            r.setFillColor(sf::Color(96, 165, 250)); window.draw(r);
        }
        for (auto& e : enemies) if (e.alive) {
            sf::CircleShape c(12.f); c.setOrigin({12.f, 12.f}); c.setPosition(e.pos);
            c.setFillColor(e.maxHp > 100 ? sf::Color(200, 60, 200) : sf::Color(230, 80, 70));
            window.draw(c);
        }
        for (auto& b : bullets) {
            sf::CircleShape c(4.f); c.setOrigin({4.f, 4.f}); c.setPosition(b.pos);
            c.setFillColor(sf::Color(250, 204, 21)); window.draw(c);
        }
        window.display();
    }
}// Complete C++ Tower Defence (Parts 1 + 2) for SFML 3. Build:
//   g++ tower.cpp -o tower -lsfml-graphics -lsfml-window -lsfml-system
#include <SFML/Graphics.hpp>
#include <optional>
#include <vector>
#include <cmath>
#include <string>
#include <algorithm>

struct Enemy { sf::Vector2f pos; float speed; int hp, maxHp, bounty; int wp; bool alive; bool isBoss = false; };
struct Tower { sf::Vector2f pos; float range; int damage; float cooldown; };
struct Bullet { sf::Vector2f pos; Enemy* target; float speed; int damage; bool dead; };

int main() {
    sf::RenderWindow window(sf::VideoMode({800, 600}), "C++ Tower Defence");
    window.setFramerateLimit(60);

    std::vector<sf::Vector2f> path = {
        {-20, 120}, {300, 120}, {300, 320}, {560, 320}, {560, 120}, {820, 120}
    };

    std::vector<Enemy> enemies; enemies.reserve(4096);   // reserve so Enemy* stays valid
    std::vector<Tower> towers;
    std::vector<Bullet> bullets;
    int gold = 150, health = 20, wave = 0, toSpawn = 0;
    float spawnGap = 0, rest = 3.f;
    bool won = false, lost = false;
    const int MAX_WAVE = 5, TOWER_COST = 50;

    auto nearPath = [&](float x, float y, float r) {
        for (size_t i = 0; i + 1 < path.size(); i++) {
            sf::Vector2f a = path[i], b = path[i+1];
            sf::Vector2f ab = b - a, ap = sf::Vector2f(x,y) - a;
            float t = (ab.x*ap.x + ab.y*ap.y) / (ab.x*ab.x + ab.y*ab.y);
            t = std::max(0.f, std::min(1.f, t));
            sf::Vector2f c = a + ab * t;
            float dx = x - c.x, dy = y - c.y;
            if (std::sqrt(dx*dx + dy*dy) < r) return true;
        }
        return false;
    };
    auto nearest = [&](Tower& t) -> Enemy* {
        Enemy* best = nullptr; float bd = t.range;
        for (auto& e : enemies) {
            if (!e.alive) continue;
            float dx = e.pos.x - t.pos.x, dy = e.pos.y - t.pos.y;
            float d = std::sqrt(dx*dx + dy*dy);
            if (d < bd) { bd = d; best = &e; }
        }
        return best;
    };

    sf::Clock clock;
    while (window.isOpen()) {
        float dt = clock.restart().asSeconds();
        if (dt > 0.05f) dt = 0.05f;

        while (const std::optional event = window.pollEvent()) {
            if (event->is<sf::Event::Closed>()) window.close();
            if (const auto* m = event->getIf<sf::Event::MouseButtonPressed>()) {
                if (m->button == sf::Mouse::Button::Left && !won && !lost) {
                    int gx = (m->position.x / 40) * 40, gy = (m->position.y / 40) * 40;
                    float cx = gx + 20.f, cy = gy + 20.f;
                    if (!nearPath(cx, cy, 30.f) && gold >= TOWER_COST) {
                        towers.push_back({ {cx, cy}, 110.f, 5, 0.f });
                        gold -= TOWER_COST;
                    }
                }
            }
        }

        if (!won && !lost) {
            // wave director
            bool cleared = true;
            for (auto& e : enemies) if (e.alive) cleared = false;
            if (toSpawn == 0 && cleared) {
                rest -= dt;
                if (rest <= 0) {
                    wave++;
                    if (wave > MAX_WAVE) won = true;
                    else { toSpawn = 4 + wave * 2; rest = 3.f; }
                }
            } else if (toSpawn > 0) {
                spawnGap -= dt;
                if (spawnGap <= 0) {
                    bool boss = (wave == MAX_WAVE && toSpawn == 1);
                    int hp = boss ? 200 : (16 + wave * 4);
                    enemies.push_back({ path[0], boss ? 45.f : 70.f, hp, hp, boss ? 60 : 8, 1, true });
                    toSpawn--; spawnGap = 0.8f;
                }
            }

            // enemies
            for (auto& e : enemies) {
                if (!e.alive) continue;
                if (e.wp >= (int)path.size()) { e.alive = false; if (--health <= 0) lost = true; continue; }
                sf::Vector2f d = path[e.wp] - e.pos;
                float len = std::sqrt(d.x*d.x + d.y*d.y);
                if (len < 4.f) { e.wp++; }
                else e.pos += d / len * e.speed * dt;
            }

            // towers fire
            for (auto& t : towers) {
                t.cooldown -= dt;
                if (t.cooldown <= 0) {
                    Enemy* target = nearest(t);
                    if (target) { bullets.push_back({ t.pos, target, 340.f, t.damage, false }); t.cooldown = 0.7f; }
                }
            }
            // bullets home
            for (auto& b : bullets) {
                if (!b.target->alive) { b.dead = true; continue; }
                sf::Vector2f d = b.target->pos - b.pos;
                float len = std::sqrt(d.x*d.x + d.y*d.y);
                if (len < 8.f) {
                    b.target->hp -= b.damage;
                    if (b.target->hp <= 0) { b.target->alive = false; gold += b.target->bounty; }
                    b.dead = true;
                } else b.pos += d / len * b.speed * dt;
            }
            bullets.erase(std::remove_if(bullets.begin(), bullets.end(),
                [](const Bullet& b){ return b.dead; }), bullets.end());
        }

        // draw
        window.clear(sf::Color(24, 40, 28));
        for (size_t i = 0; i + 1 < path.size(); i++) {
            sf::Vector2f a = path[i], b = path[i+1], d = b - a;
            float len = std::sqrt(d.x*d.x + d.y*d.y);
            sf::RectangleShape seg({len, 36.f});
            seg.setPosition(a);
            seg.setOrigin({0.f, 18.f});
            seg.setRotation(sf::radians(std::atan2(d.y, d.x)));
            seg.setFillColor(sf::Color(70, 62, 48));
            window.draw(seg);
        }
        for (auto& t : towers) {
            sf::CircleShape rng(t.range); rng.setOrigin({t.range, t.range}); rng.setPosition(t.pos);
            rng.setFillColor(sf::Color(96, 165, 250, 24)); window.draw(rng);
            sf::RectangleShape r({30.f, 30.f}); r.setOrigin({15.f, 15.f}); r.setPosition(t.pos);
            r.setFillColor(sf::Color(96, 165, 250)); window.draw(r);
        }
        for (auto& e : enemies) if (e.alive) {
            sf::CircleShape c(12.f); c.setOrigin({12.f, 12.f}); c.setPosition(e.pos);
            c.setFillColor(e.maxHp > 100 ? sf::Color(200, 60, 200) : sf::Color(230, 80, 70));
            window.draw(c);
        }
        for (auto& b : bullets) {
            sf::CircleShape c(4.f); c.setOrigin({4.f, 4.f}); c.setPosition(b.pos);
            c.setFillColor(sf::Color(250, 204, 21)); window.draw(c);
        }
        window.display();
    }
}
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
Seven episodes of C++: Pong to a full tower defence with waves, upgrades and a boss. You are a C++ game programmer now. Go build YOUR game. โš™๏ธ๐Ÿ†
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โญ View My Progress & Certificates

This workshop was free and took many hours to build. If it helped you learn something new, consider supporting the project.

☕ Support on Ko-fi