Hold the Line!
Tower Defence Part 1
The two-part finale: a path-following enemy system, grid-snapped tower placement, gold economy, and range visualisation. Tower defence is systems programming, exactly what C++ is for.
The two-part finale: a path-following enemy system, grid-snapped tower placement, gold economy, and range visualisation. Tower defence is systems programming, exactly what C++ is for.
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}) / setOrigin({x, y})setOrigin({x, y}) (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.sf::RenderWindowsf::RenderWindow, the game loop and event polling. If you're starting here or need that boilerplate, grab it in one click: Episode 1 setup โ ยท C++ Cheatsheet โDefine the enemy path as waypoints and draw the road.
std::vector<sf::Vector2f> path = {
{-20, 100}, {300, 100}, {300, 300},
{120, 300}, {120, 480}, {600, 480},
{600, 220}, {820, 220}
};
// road drawing (segment between path[i] and path[i+1]):
for (size_t i = 0; i + 1 < path.size(); i++) {
sf::Vector2f a = path[i], b = path[i+1];
sf::Vector2f d = b - a;
float len = std::sqrt(d.x*d.x + d.y*d.y);
sf::RectangleShape seg(sf::Vector2f(len, 36));
seg.setOrigin(0, 18);
seg.setPosition(a);
seg.setRotation(std::atan2(d.y, d.x) * 57.2958f);
seg.setFillColor(sf::Color(194, 178, 128));
window.draw(seg);
}std::vector<sf::Vector2f> path = {
{-20, 100}, {300, 100}, {300, 300},
{120, 300}, {120, 480}, {600, 480},
{600, 220}, {820, 220}
};
// road drawing (segment between path[i] and path[i+1]):
for (size_t i = 0; i + 1 < path.size(); i++) {
sf::Vector2f a = path[i], b = path[i+1];
sf::Vector2f d = b - a;
float len = std::sqrt(d.x*d.x + d.y*d.y);
sf::RectangleShape seg(sf::Vector2f(len, 36));
seg.setOrigin(0, 18);
seg.setPosition(a);
seg.setRotation(std::atan2(d.y, d.x) * 57.2958f);
seg.setFillColor(sf::Color(194, 178, 128));
window.draw(seg);
}
Make enemies walk the path corner to corner at constant speed.
struct Enemy {
sf::Vector2f pos;
float speed = 70;
int hp = 20;
int wp = 0; // next waypoint index
bool alive = true;
};
void updateEnemy(Enemy& e, float dt) {
if (e.wp >= (int)path.size()) return; // escaped
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++; return; }
e.pos += (d / len) * e.speed * dt; // normalised!
}struct Enemy {
sf::Vector2f pos;
float speed = 70;
int hp = 20;
int wp = 0; // next waypoint index
bool alive = true;
};
void updateEnemy(Enemy& e, float dt) {
if (e.wp >= (int)path.size()) return; // escaped
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++; return; }
e.pos += (d / len) * e.speed * dt; // normalised!
}
sf::Vector2f d = path[e.wp] - e.pos; float len = std::sf::Vector2f d = path[e.wp] - e.pos; float len = std::(d.x*d.x + d.y*d.y); e.pos += (d / (d.x*d.x + d.y*d.y); e.pos += (d / ) * e.speed * ) * e.speed * ;;
Show a snapped, colour-coded ghost tower under the cursor.
sf::Vector2i m = sf::Mouse::getPosition(window);
int gx = (m.x / 40) * 40;
int gy = (m.y / 40) * 40;
bool blocked = nearPath((float)gx + 20, (float)gy + 20, 30.f)
|| occupied(gx, gy);
sf::RectangleShape ghost(sf::Vector2f(36, 36));
ghost.setPosition({(float)gx + 2, (float)gy + 2});
ghost.setFillColor(blocked
? sf::Color(248, 113, 113, 120)
: sf::Color(6, 214, 160, 120));
window.draw(ghost);sf::Vector2i m = sf::Mouse::getPosition(window);
int gx = (m.x / 40) * 40;
int gy = (m.y / 40) * 40;
bool blocked = nearPath((float)gx + 20, (float)gy + 20, 30.f)
|| occupied(gx, gy);
sf::RectangleShape ghost(sf::Vector2f(36, 36));
ghost.setPosition({(float)gx + 2, (float)gy + 2});
ghost.setFillColor(blocked
? sf::Color(248, 113, 113, 120)
: sf::Color(6, 214, 160, 120));
window.draw(ghost);
Place towers on click, spend gold, block double-building.
struct Tower {
sf::Vector2f pos; // cell centre
float range = 110;
int damage = 5;
float cooldown = 0;
};
std::vector<Tower> towers;
int gold = 100;
const int TOWER_COST = 50;
// on MouseButtonPressed (left):
if (!blocked && gold >= TOWER_COST) {
towers.push_back({ {(float)gx + 20, (float)gy + 20} });
gold -= TOWER_COST;
}struct Tower {
sf::Vector2f pos; // cell centre
float range = 110;
int damage = 5;
float cooldown = 0;
};
std::vector<Tower> towers;
int gold = 100;
const int TOWER_COST = 50;
// on MouseButtonPressed (left):
if (!blocked && gold >= TOWER_COST) {
towers.push_back({ {(float)gx + 20, (float)gy + 20} });
gold -= TOWER_COST;
}
if (!if (! && gold >= && gold >= ) {
towers.push_back({ {(float)gx + 20, (float)gy + 20} });
gold -= ) {
towers.push_back({ {(float)gx + 20, (float)gy + 20} });
gold -= ;
};
}
Visualise tower range and detect enemies within it.
Enemy* nearestInRange(Tower& t, std::vector<Enemy>& enemies) {
Enemy* best = nullptr;
float bestDist = t.range * t.range;
for (auto& e : enemies) {
if (!e.alive) continue;
float dx = e.pos.x - t.pos.x;
float dy = e.pos.y - t.pos.y;
float d2 = dx*dx + dy*dy;
if (d2 <= bestDist) {
bestDist = d2;
best = &e;
}
}
return best;
}Enemy* nearestInRange(Tower& t, std::vector<Enemy>& enemies) {
Enemy* best = nullptr;
float bestDist = t.range * t.range;
for (auto& e : enemies) {
if (!e.alive) continue;
float dx = e.pos.x - t.pos.x;
float dy = e.pos.y - t.pos.y;
float d2 = dx*dx + dy*dy;
if (d2 <= bestDist) {
bestDist = d2;
best = &e;
}
}
return best;
}
Balance the opening economy and stage the code for Part 2.
struct Enemy {
// ... existing fields ...
int bounty = 15; // paid on kill (Part 2)
};
int lives = 20;
// escape handling (enemy past last waypoint):
if (e.wp >= (int)path.size() && e.alive) {
e.alive = false;
lives--;
}struct Enemy {
// ... existing fields ...
int bounty = 15; // paid on kill (Part 2)
};
int lives = 20;
// escape handling (enemy past last waypoint):
if (e.wp >= (int)path.size() && e.alive) {
e.alive = false;
lives--;
}
if (e.wp >= (int)path.if (e.wp >= (int)path.() && e.alive) {
e.alive = () && e.alive) {
e.alive = ;
lives;
lives;
};
}
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).
// 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; };
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; };
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();
}
}