โš™๏ธ My First C++ Game ยท Episode 5 of 7 ยท See All Episodes
๐ŸŒฒ Episode 5 ยท Improver+ ยท Upgrades Episode 4

Full Game!
Platformer Part 2

Open your Episode 4 project, we are shipping a full game: an Enemy class with stomps, an sf::View scrolling camera, spikes, checkpoints and multi-level structure. C++ platforming, complete.

๐Ÿ‘ถ Ages 12+ โฑ๏ธ ~2 Hours โš™๏ธ C++ & SFML โœ“ Free
๐Ÿ‘พ Enemy class ๐Ÿฅพ Stomp kills ๐ŸŽฅ sf::View camera โš ๏ธ Spikes ๐Ÿšฉ Checkpoints ๐Ÿ—บ๏ธ Level structs
โญ
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}) / setCenter({x, y})setCenter({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.
๐Ÿงฉ
Need the game so far? This episode continues Platformer Part 1, so it assumes you already have that project. Open your Part 1 file to keep building, or revisit it here: Platformer Part 1 โ†’ ยท C++ Cheatsheet โ†’
1
๐Ÿ‘พ
The Enemy Class
Patrolling walkers built like the Player
Active
๐ŸŽฏ
Goal for this step

Add an Enemy class with patrol movement, stored in a vector.

  • 1Enemy mirrors Player: shape, vx, plus minX/maxX patrol bounds and an alive flag.
  • 2Its update: move by vxยทdt; flip vx at the bounds.
  • 3A constructor taking (x, y, minX, maxX) makes placing them one-liners.
  • 4Two enemies to start: one on the ground, one patrolling a ledge. Draw red.
platformer.cpp
class Enemy {
public:
    sf::RectangleShape shape;
    float vx = 120;
    float minX, maxX;
    bool alive = true;

    Enemy(float x, float y, float lo, float hi)
            : minX(lo), maxX(hi) {
        shape.setSize(sf::Vector2f(30, 24));
        shape.setPosition({x, y});
        shape.setFillColor(sf::Color(248, 113, 113));
    }

    void update(float dt) {
        shape.move({vx * dt, 0});
        float x = shape.getPosition().x;
        if (x < minX || x > maxX) vx = -vx;
    }
};

std::vector<Enemy> enemies;
enemies.emplace_back(390, 236, 380, 500);
enemies.emplace_back(60, 416, 40, 700);class Enemy {
public:
    sf::RectangleShape shape;
    float vx = 120;
    float minX, maxX;
    bool alive = true;

    Enemy(float x, float y, float lo, float hi)
            : minX(lo), maxX(hi) {
        shape.setSize(sf::Vector2f(30, 24));
        shape.setPosition({x, y});
        shape.setFillColor(sf::Color(248, 113, 113));
    }

    void update(float dt) {
        shape.move({vx * dt, 0});
        float x = shape.getPosition().x;
        if (x < minX || x > maxX) vx = -vx;
    }
};

std::vector<Enemy> enemies;
enemies.emplace_back(390, 236, 380, 500);
enemies.emplace_back(60, 416, 40, 700);
โœ๏ธ
Fill in the Blanks
+15 XP
The constructor line ": minX(lo), maxX(hi)" is an initialiser . Enemies are stored with emplace_back just like were in Episode 3.
๐Ÿง 
Knowledge Check
+15 XP
Enemy and Player share the shape+velocity+update pattern. In bigger games this becomesโ€ฆ
ACopy-paste forever
BA shared base class (inheritance), Entity with Player and Enemy deriving from it
CA single giant class
2
๐Ÿฅพ
Stomp or Suffer
Fall on heads to kill; touch sides to die
Locked
๐ŸŽฏ
Goal for this step

Implement the stomp with the falling-from-above test.

  • 1For each living enemy intersecting the player: falling AND bottom near their top โ†’ stomp (alive = false, bounce vy = -350, +score).
  • 2Otherwise โ†’ death: respawn (checkpoints next step).
  • 3Skip dead enemies everywhere (alive flag, Episode 3 pattern).
  • 4The bounce chains stomps, line your two enemies up and try a double!
platformer.cpp
for (auto& en : enemies) {
    if (!en.alive) continue;
    if (pb.findIntersection(en.shape.getGlobalBounds())) {
        float enTop = en.shape.getPosition().y;
        if (player.vy > 0
                && pb.position.y + pb.size.y - player.vy * dt <= enTop + 10) {
            en.alive = false;
            player.vy = -350;      // bounce!
            score += 50;
        } else {
            respawn(player);
        }
    }
}for (auto& en : enemies) {
    if (!en.alive) continue;
    if (pb.findIntersection(en.shape.getGlobalBounds())) {
        float enTop = en.shape.getPosition().y;
        if (player.vy > 0
                && pb.position.y + pb.size.y - player.vy * dt <= enTop + 10) {
            en.alive = false;
            player.vy = -350;      // bounce!
            score += 50;
        } else {
            respawn(player);
        }
    }
}
โŒจ๏ธ
Code Challenge
+20 XP
Squash the enemy and rebound:
platformer.cpp
en.alive = en.alive = ;
player.vy = ;
player.vy = ;
score += 50;;
score += 50;
๐Ÿ’ก Hint: The enemy stops existing; the player bounces upward with a negative vertical velocity.
๐Ÿง 
Knowledge Check
+15 XP
The stomp test reuses which platformer check?
AThe coin pickup
BLanding-from-above, the same "falling onto a top surface" logic aimed at an enemy
CThe wall clamp
3
๐ŸŽฅ
The sf::View Camera
SFMLโ€™s built-in camera, three lines of scrolling
Locked
๐ŸŽฏ
Goal for this step

Follow the player through a 2400 px level using sf::View.

  • 1SFML has a real camera class: sf::View. Centre it on the player, clamped to the level, then window.setView(view).
  • 2THAT IS IT, no offsetting of draw calls (remember doing that by hand in Python? Engines do this for you).
  • 3Clamp centre x between 400 and levelWidth โˆ’ 400 so the view never shows past the edges.
  • 4For screen-fixed HUD: switch back with window.setView(window.getDefaultView()) before drawing text.
platformer.cpp
const float LEVEL_W = 2400.f;
sf::View view(sf::FloatRect({0, 0}, {800, 480}));

// each frame:
float cx = player.shape.getPosition().x + 17;
cx = std::max(400.f, std::min(LEVEL_W - 400.f, cx));
view.setCenter(cx, 240);
window.setView(view);

// ... draw world ...
window.setView(window.getDefaultView());
window.draw(scoreText);          // HUD stays putconst float LEVEL_W = 2400.f;
sf::View view(sf::FloatRect({0, 0}, {800, 480}));

// each frame:
float cx = player.shape.getPosition().x + 17;
cx = std::max(400.f, std::min(LEVEL_W - 400.f, cx));
view.setCenter(cx, 240);
window.setView(view);

// ... draw world ...
window.setView(window.getDefaultView());
window.draw(scoreText);          // HUD stays put
โœ๏ธ
Fill in the Blanks
+15 XP
The camera is an sf:: centred on the player. Before drawing the HUD we restore the view so text stays fixed on screen.
๐Ÿง 
Knowledge Check
+15 XP
In Python you subtracted cam_x from every draw call. sf::View replaces that byโ€ฆ
ADrawing twice
BTransforming ALL drawing through the view automatically, the engine does the offsetting
CMoving the window
4
โš ๏ธ
Spikes & Checkpoints
Hazards, flags and one shared respawn function
Locked
๐ŸŽฏ
Goal for this step

Add deadly spikes and fair checkpoint respawns.

  • 1Spikes: vector of FloatRects on the floor; intersecting one calls respawn().
  • 2Checkpoints: flag rects that update a respawnPoint Vector2f when touched.
  • 3One respawn(player) function used by spikes, enemies and pits, single source of truth.
  • 4Place checkpoints before hard sections. Draw spikes as red triangles (sf::ConvexShape), flags teal.
platformer.cpp
sf::Vector2f respawnPoint(100, 380);

void respawn(Player& p) {
    p.shape.setPosition(respawnPoint);
    p.vx = 0; p.vy = 0;
}

for (auto& cp : checkpoints)
    if (pb.findIntersection(cp))
        respawnPoint = { cp.position.x, cp.position.y - 44 };

for (auto& sp : spikes)
    if (pb.findIntersection(sp))
        respawn(player);

if (player.shape.getPosition().y > 480)   // pit
    respawn(player);sf::Vector2f respawnPoint(100, 380);

void respawn(Player& p) {
    p.shape.setPosition(respawnPoint);
    p.vx = 0; p.vy = 0;
}

for (auto& cp : checkpoints)
    if (pb.findIntersection(cp))
        respawnPoint = { cp.position.x, cp.position.y - 44 };

for (auto& sp : spikes)
    if (pb.findIntersection(sp))
        respawn(player);

if (player.shape.getPosition().y > 480)   // pit
    respawn(player);
โŒจ๏ธ
Code Challenge
+20 XP
Update the respawn point at a checkpoint:
platformer.cpp
if (pb.findIntersection(cp))
    respawnPoint = { cp.position.x, cp.position.y - if (pb.findIntersection(cp))
    respawnPoint = { cp.position.x, cp.position.y -  }; };
๐Ÿ’ก Hint: Spawn standing ON the flag: its top minus the player height.
๐Ÿง 
Knowledge Check
+15 XP
Every death path calls one respawn() function. When you later add fall damage or a death sound, you editโ€ฆ
AEvery collision block
BExactly one function, that is why shared paths matter
CThe Enemy class
5
๐Ÿ—บ๏ธ
Levels as Structs
Bundle a whole level into one loadable object
Locked
๐ŸŽฏ
Goal for this step

Define levels as data structs and swap between them.

  • 1A struct Level holds: platforms, enemies (as spawn data), spikes, checkpoints, door, width.
  • 2A makeLevel(int n) function returns the struct for level n, pure data, no logic.
  • 3Loading = copying the structโ€™s contents into the live vectors and resetting the player.
  • 4Reaching the door loads n+1; after the last, the champion screen. Your Ep 4 game is now a multi-level product.
platformer.cpp
struct Level {
    std::vector<sf::FloatRect> platforms, spikes, checkpoints;
    std::vector<Enemy> enemies;
    sf::FloatRect door;
    float width;
};

Level makeLevel(int n) {
    if (n == 0) {
        Level L;
        L.width = 2400;
        L.platforms = { {{0,440},{2400,40}}, {{150,340},{140,16}} /* ... */ };
        L.enemies.emplace_back(390, 236, 380, 500);
        L.spikes = { {760, 424, 40, 16} };
        L.checkpoints = { {1000, 380, 12, 60} };
        L.door = {2320, 380, 30, 60};
        return L;
    }
    // n == 1 ...
}struct Level {
    std::vector<sf::FloatRect> platforms, spikes, checkpoints;
    std::vector<Enemy> enemies;
    sf::FloatRect door;
    float width;
};

Level makeLevel(int n) {
    if (n == 0) {
        Level L;
        L.width = 2400;
        L.platforms = { {{0,440},{2400,40}}, {{150,340},{140,16}} /* ... */ };
        L.enemies.emplace_back(390, 236, 380, 500);
        L.spikes = { {760, 424, 40, 16} };
        L.checkpoints = { {1000, 380, 12, 60} };
        L.door = {2320, 380, 30, 60};
        return L;
    }
    // n == 1 ...
}
โœ๏ธ
Fill in the Blanks
+15 XP
A Level bundles every vector one level needs. The function (n) returns pure data, building levels becomes filling in lists.
๐Ÿง 
Knowledge Check
+15 XP
Separating level DATA from game LOGIC meansโ€ฆ
ALevels run faster
BNew levels need zero new logic, and eventually data can come from files or a level editor
CThe compiler optimises structs
6
๐Ÿ
Polish & Ship
Death counter, timer, and the champion screen
Locked
๐ŸŽฏ
Goal for this step

Add the finishing touches and complete your C++ platformer.

  • 1Track deaths and a speedrun timer (one sf::Clock started at level 1), show both on the champion screen.
  • 2Squash animation: dead enemies flatten (setScale(1, 0.3)) for 0.3 s before vanishing.
  • 3Balance: walk each level; every death should feel like YOUR fault. Adjust spike placement until it does.
  • 4Compile, play, hand the keyboard to someone. Watching a playtester is a developer rite of passage. ๐Ÿ†
platformer.cpp
int deaths = 0;                 // ++ inside respawn()
sf::Clock runTimer;             // started at first level load

// champion screen:
int secs = (int)runTimer.getElapsedTime().asSeconds();
championText.setString(
    "CHAMPION!\nTime: " + std::to_string(secs / 60) + "m "
    + std::to_string(secs % 60) + "s\nDeaths: "
    + std::to_string(deaths));int deaths = 0;                 // ++ inside respawn()
sf::Clock runTimer;             // started at first level load

// champion screen:
int secs = (int)runTimer.getElapsedTime().asSeconds();
championText.setString(
    "CHAMPION!\nTime: " + std::to_string(secs / 60) + "m "
    + std::to_string(secs % 60) + "s\nDeaths: "
    + std::to_string(deaths));
โŒจ๏ธ
Code Challenge
+20 XP
Format the speedrun time from total seconds:
platformer.cpp
std::to_string(secs std::to_string(secs  60) + "m "
+ std::to_string(secs  60) + "m "
+ std::to_string(secs  60) + "s" 60) + "s"
๐Ÿ’ก Hint: Whole-number division gives minutes; the remainder operator gives leftover seconds.
๐Ÿง 
Knowledge Check
+15 XP
A death counter and timer on the win screen addโ€ฆ
ALag
BReplay value, beat your time, deathless runs. Numbers players can chase for free
CCompile warnings
โœ… 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++ platformer2.cpp -o platformer2 -lsfml-graphics -lsfml-window -lsfml-systemg++ platformer2.cpp -o platformer2 -lsfml-graphics -lsfml-window -lsfml-system then run ./platformer2./platformer2 (on Windows, from the MSYS2 MinGW terminal so it finds the SFML DLLs).

platformer2.cpp
// Complete C++ Platformer Part 2 for SFML 3 (double jump, wall jump, camera,
// ASCII levels, patrol+stomp enemies, spikes, coins, flag). Build:
//   g++ platformer2.cpp -o platformer2 -lsfml-graphics -lsfml-window -lsfml-system
#include <SFML/Graphics.hpp>
#include <optional>
#include <vector>
#include <string>
#include <cmath>

struct Enemy { float x, y, vx, x0, range; bool alive; };

int main() {
    const float TILE = 40.f;
    sf::RenderWindow window(sf::VideoMode({800, 480}), "C++ Platformer 2");
    window.setFramerateLimit(60);

    // # ground, = platform, ^ spike, o coin, E enemy, F flag
    std::vector<std::string> LEVEL = {
        "                                        ",
        "                                     F  ",
        "                                  ===   ",
        "              o          o              ",
        "         ==        ==        ===        ",
        "    o                            o      ",
        "   ===        ^^       E     ==         ",
        "                   ===                  ",
        "         E                     ^^       ",
        "  ==           o         ==             ",
        "          ===                   ===     ",
        "########################################"
    };
    const float LEVEL_W = LEVEL[0].size() * TILE;

    std::vector<sf::FloatRect> platforms, spikes, coins;
    std::vector<Enemy> enemies;
    sf::FloatRect flag;
    for (size_t r = 0; r < LEVEL.size(); r++)
        for (size_t c = 0; c < LEVEL[r].size(); c++) {
            float x = c * TILE, y = r * TILE;
            char ch = LEVEL[r][c];
            if (ch == '#' || ch == '=') platforms.push_back({{x, y}, {TILE, TILE}});
            if (ch == 'o') coins.push_back({{x + 11, y + 11}, {18, 18}});
            if (ch == '^') spikes.push_back({{x, y + 20}, {TILE, 20}});
            if (ch == 'E') enemies.push_back({x, y + 10, 1.2f, x, 70.f, true});
            if (ch == 'F') flag = sf::FloatRect({x, y}, {TILE, TILE});
        }

    float px = 60, py = 360, pw = 30, ph = 40, vx = 0, vy = 0;
    int jumpsLeft = 2, onWall = 0, wallKick = 0, score = 0;
    bool onGround = false, won = false;

    auto respawn = [&]() { px = 60; py = 360; vx = 0; vy = 0; };

    while (window.isOpen()) {
        while (const std::optional event = window.pollEvent()) {
            if (event->is<sf::Event::Closed>()) window.close();
            if (const auto* k = event->getIf<sf::Event::KeyPressed>()) {
                if ((k->code == sf::Keyboard::Key::Up || k->code == sf::Keyboard::Key::Space) && !won) {
                    if (onWall != 0 && !onGround) { vy = -10; vx = -onWall * 6.f; wallKick = 12; jumpsLeft = 1; }
                    else if (jumpsLeft > 0) { vy = (jumpsLeft == 2) ? -11.f : -9.f; jumpsLeft--; onGround = false; }
                }
                if (k->code == sf::Keyboard::Key::R) { respawn(); }
            }
        }

        if (!won) {
            if (wallKick > 0) wallKick--;
            else {
                vx = 0;
                if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left))  vx = -4;
                if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) vx = 4;
            }
            px += vx;
            if (px < 0) px = 0;
            onWall = 0;
            sf::FloatRect pr({px, py}, {pw, ph});
            for (auto& p : platforms) if (pr.findIntersection(p)) {
                if (vx > 0) { px = p.position.x - pw; onWall = 1; }
                else if (vx < 0) { px = p.position.x + p.size.x; onWall = -1; }
                pr = sf::FloatRect({px, py}, {pw, ph});
            }

            vy += 0.5f; py += vy;
            onGround = false;
            pr = sf::FloatRect({px, py}, {pw, ph});
            for (auto& p : platforms) if (pr.findIntersection(p)) {
                if (vy > 0) { py = p.position.y - ph; vy = 0; onGround = true; jumpsLeft = 2; }
                else if (vy < 0) { py = p.position.y + p.size.y; vy = 0; }
                pr = sf::FloatRect({px, py}, {pw, ph});
            }
            if (onWall != 0 && !onGround && vy > 1.5f) vy = 1.5f;
            if (py > 480 + 80) respawn();

            pr = sf::FloatRect({px, py}, {pw, ph});
            for (auto& en : enemies) {
                if (!en.alive) continue;
                en.x += en.vx;
                if (en.x > en.x0 + en.range || en.x < en.x0 - en.range) en.vx = -en.vx;
                sf::FloatRect er({en.x, en.y}, {28, 28});
                if (pr.findIntersection(er)) {
                    if (vy > 0 && (py + ph - en.y) < 20) { en.alive = false; vy = -8; score += 5; }
                    else respawn();
                }
            }
            for (auto& s : spikes) if (pr.findIntersection(s)) respawn();
            for (size_t i = 0; i < coins.size(); ) {
                if (pr.findIntersection(coins[i])) { coins.erase(coins.begin()+i); score += 1; }
                else ++i;
            }
            if (pr.findIntersection(flag)) won = true;
        }

        // camera follows the player
        float camX = px + pw/2 - 400;
        if (camX < 0) camX = 0;
        if (camX > LEVEL_W - 800) camX = LEVEL_W - 800;
        sf::View view(sf::FloatRect({camX, 0}, {800, 480}));
        window.setView(view);

        window.clear(sf::Color(16, 22, 42));
        for (auto& p : platforms) { sf::RectangleShape r(p.size); r.setPosition(p.position); r.setFillColor(sf::Color(59,74,99)); window.draw(r); }
        for (auto& s : spikes)    { sf::RectangleShape r(s.size); r.setPosition(s.position); r.setFillColor(sf::Color(255,84,112)); window.draw(r); }
        for (auto& c : coins)     { sf::RectangleShape r(c.size); r.setPosition(c.position); r.setFillColor(sf::Color(255,209,102)); window.draw(r); }
        for (auto& en : enemies) if (en.alive) { sf::RectangleShape r({28,28}); r.setPosition({en.x,en.y}); r.setFillColor(sf::Color(168,85,247)); window.draw(r); }
        { sf::RectangleShape r({12,40}); r.setPosition({flag.position.x+6, flag.position.y}); r.setFillColor(sf::Color(74,222,128)); window.draw(r); }
        { sf::RectangleShape r({pw,ph}); r.setPosition({px,py}); r.setFillColor(sf::Color(96,165,250)); window.draw(r); }
        window.display();
    }
}// Complete C++ Platformer Part 2 for SFML 3 (double jump, wall jump, camera,
// ASCII levels, patrol+stomp enemies, spikes, coins, flag). Build:
//   g++ platformer2.cpp -o platformer2 -lsfml-graphics -lsfml-window -lsfml-system
#include <SFML/Graphics.hpp>
#include <optional>
#include <vector>
#include <string>
#include <cmath>

struct Enemy { float x, y, vx, x0, range; bool alive; };

int main() {
    const float TILE = 40.f;
    sf::RenderWindow window(sf::VideoMode({800, 480}), "C++ Platformer 2");
    window.setFramerateLimit(60);

    // # ground, = platform, ^ spike, o coin, E enemy, F flag
    std::vector<std::string> LEVEL = {
        "                                        ",
        "                                     F  ",
        "                                  ===   ",
        "              o          o              ",
        "         ==        ==        ===        ",
        "    o                            o      ",
        "   ===        ^^       E     ==         ",
        "                   ===                  ",
        "         E                     ^^       ",
        "  ==           o         ==             ",
        "          ===                   ===     ",
        "########################################"
    };
    const float LEVEL_W = LEVEL[0].size() * TILE;

    std::vector<sf::FloatRect> platforms, spikes, coins;
    std::vector<Enemy> enemies;
    sf::FloatRect flag;
    for (size_t r = 0; r < LEVEL.size(); r++)
        for (size_t c = 0; c < LEVEL[r].size(); c++) {
            float x = c * TILE, y = r * TILE;
            char ch = LEVEL[r][c];
            if (ch == '#' || ch == '=') platforms.push_back({{x, y}, {TILE, TILE}});
            if (ch == 'o') coins.push_back({{x + 11, y + 11}, {18, 18}});
            if (ch == '^') spikes.push_back({{x, y + 20}, {TILE, 20}});
            if (ch == 'E') enemies.push_back({x, y + 10, 1.2f, x, 70.f, true});
            if (ch == 'F') flag = sf::FloatRect({x, y}, {TILE, TILE});
        }

    float px = 60, py = 360, pw = 30, ph = 40, vx = 0, vy = 0;
    int jumpsLeft = 2, onWall = 0, wallKick = 0, score = 0;
    bool onGround = false, won = false;

    auto respawn = [&]() { px = 60; py = 360; vx = 0; vy = 0; };

    while (window.isOpen()) {
        while (const std::optional event = window.pollEvent()) {
            if (event->is<sf::Event::Closed>()) window.close();
            if (const auto* k = event->getIf<sf::Event::KeyPressed>()) {
                if ((k->code == sf::Keyboard::Key::Up || k->code == sf::Keyboard::Key::Space) && !won) {
                    if (onWall != 0 && !onGround) { vy = -10; vx = -onWall * 6.f; wallKick = 12; jumpsLeft = 1; }
                    else if (jumpsLeft > 0) { vy = (jumpsLeft == 2) ? -11.f : -9.f; jumpsLeft--; onGround = false; }
                }
                if (k->code == sf::Keyboard::Key::R) { respawn(); }
            }
        }

        if (!won) {
            if (wallKick > 0) wallKick--;
            else {
                vx = 0;
                if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left))  vx = -4;
                if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) vx = 4;
            }
            px += vx;
            if (px < 0) px = 0;
            onWall = 0;
            sf::FloatRect pr({px, py}, {pw, ph});
            for (auto& p : platforms) if (pr.findIntersection(p)) {
                if (vx > 0) { px = p.position.x - pw; onWall = 1; }
                else if (vx < 0) { px = p.position.x + p.size.x; onWall = -1; }
                pr = sf::FloatRect({px, py}, {pw, ph});
            }

            vy += 0.5f; py += vy;
            onGround = false;
            pr = sf::FloatRect({px, py}, {pw, ph});
            for (auto& p : platforms) if (pr.findIntersection(p)) {
                if (vy > 0) { py = p.position.y - ph; vy = 0; onGround = true; jumpsLeft = 2; }
                else if (vy < 0) { py = p.position.y + p.size.y; vy = 0; }
                pr = sf::FloatRect({px, py}, {pw, ph});
            }
            if (onWall != 0 && !onGround && vy > 1.5f) vy = 1.5f;
            if (py > 480 + 80) respawn();

            pr = sf::FloatRect({px, py}, {pw, ph});
            for (auto& en : enemies) {
                if (!en.alive) continue;
                en.x += en.vx;
                if (en.x > en.x0 + en.range || en.x < en.x0 - en.range) en.vx = -en.vx;
                sf::FloatRect er({en.x, en.y}, {28, 28});
                if (pr.findIntersection(er)) {
                    if (vy > 0 && (py + ph - en.y) < 20) { en.alive = false; vy = -8; score += 5; }
                    else respawn();
                }
            }
            for (auto& s : spikes) if (pr.findIntersection(s)) respawn();
            for (size_t i = 0; i < coins.size(); ) {
                if (pr.findIntersection(coins[i])) { coins.erase(coins.begin()+i); score += 1; }
                else ++i;
            }
            if (pr.findIntersection(flag)) won = true;
        }

        // camera follows the player
        float camX = px + pw/2 - 400;
        if (camX < 0) camX = 0;
        if (camX > LEVEL_W - 800) camX = LEVEL_W - 800;
        sf::View view(sf::FloatRect({camX, 0}, {800, 480}));
        window.setView(view);

        window.clear(sf::Color(16, 22, 42));
        for (auto& p : platforms) { sf::RectangleShape r(p.size); r.setPosition(p.position); r.setFillColor(sf::Color(59,74,99)); window.draw(r); }
        for (auto& s : spikes)    { sf::RectangleShape r(s.size); r.setPosition(s.position); r.setFillColor(sf::Color(255,84,112)); window.draw(r); }
        for (auto& c : coins)     { sf::RectangleShape r(c.size); r.setPosition(c.position); r.setFillColor(sf::Color(255,209,102)); window.draw(r); }
        for (auto& en : enemies) if (en.alive) { sf::RectangleShape r({28,28}); r.setPosition({en.x,en.y}); r.setFillColor(sf::Color(168,85,247)); window.draw(r); }
        { sf::RectangleShape r({12,40}); r.setPosition({flag.position.x+6, flag.position.y}); r.setFillColor(sf::Color(74,222,128)); window.draw(r); }
        { sf::RectangleShape r({pw,ph}); r.setPosition({px,py}); r.setFillColor(sf::Color(96,165,250)); window.draw(r); }
        window.display();
    }
}
๐ŸŽ‰๐Ÿ†๐ŸŽฎโœจ๐ŸŽ‰
Workshop Complete!
Enemy objects, a real camera, checkpoints and levels, a complete C++ platformer. Two more episodes: the Tower Defence grand finale!
0
Total XP
1
Level
0
Best Streak
0%
Accuracy
โ–ถ Episode 6: Tower Defence Part 1 โ†’
โญ 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