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.
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.
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.Add an Enemy class with patrol movement, stored in a vector.
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);
Implement the stomp with the falling-from-above test.
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);
}
}
}
en.alive = en.alive = ; player.vy = ; player.vy = ; score += 50;; score += 50;
Follow the player through a 2400 px level using sf::View.
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
Add deadly spikes and fair checkpoint respawns.
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);
if (pb.findIntersection(cp))
respawnPoint = { cp.position.x, cp.position.y - if (pb.findIntersection(cp))
respawnPoint = { cp.position.x, cp.position.y - }; };
Define levels as data structs and swap between them.
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 ...
}
Add the finishing touches and complete your C++ platformer.
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));
std::to_string(secs std::to_string(secs 60) + "m " + std::to_string(secs 60) + "m " + std::to_string(secs 60) + "s" 60) + "s"
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).
// 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();
}
}