Gravity Works!
C++ Platformer
Gravity, jumping, platforms and coins, with a proper Player class, delta-time physics, and clean separation of update and draw. Your platformer skills, upgraded to C++.
Gravity, jumping, platforms and coins, with a proper Player class, delta-time physics, and clean separation of update and draw. Your platformer skills, upgraded to C++.
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.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 โStructure the player as a class with its own update method.
class Player {
public:
sf::RectangleShape shape;
float vx = 0, vy = 0;
bool onGround = false;
Player() {
shape.setSize(sf::Vector2f(34, 44));
shape.setPosition({100, 380});
shape.setFillColor(sf::Color(255, 119, 0));
}
void update(float dt) {
vy += 1400.f * dt; // gravity
shape.move({vx * dt, vy * dt});
}
};class Player {
public:
sf::RectangleShape shape;
float vx = 0, vy = 0;
bool onGround = false;
Player() {
shape.setSize(sf::Vector2f(34, 44));
shape.setPosition({100, 380});
shape.setFillColor(sf::Color(255, 119, 0));
}
void update(float dt) {
vy += 1400.f * dt; // gravity
shape.move({vx * dt, vy * dt});
}
};
Move with the arrows using target velocities, including mid-air.
// inside Player::update, before gravity: float target = 0; if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left)) target = -260; if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) target = 260; vx += (target - vx) * 12.f * dt; // smooth toward target// inside Player::update, before gravity: float target = 0; if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left)) target = -260; if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) target = 260; vx += (target - vx) * 12.f * dt; // smooth toward target
vx += (target - vx += (target - ) * 12.f * ) * 12.f * ;;
Add jumping plus the "coyote time" forgiveness window.
float sinceGround = 999;
// in update:
sinceGround += dt;
if (onGround) sinceGround = 0;
// jump (in the event handler):
if (const auto* kp = event->getIf<sf::Event::KeyPressed>())
if (kp->code == sf::Keyboard::Key::Space
&& player.sinceGround < 0.1f) {
player.vy = -560;
player.sinceGround = 999; // spend the jump
}float sinceGround = 999;
// in update:
sinceGround += dt;
if (onGround) sinceGround = 0;
// jump (in the event handler):
if (const auto* kp = event->getIf<sf::Event::KeyPressed>())
if (kp->code == sf::Keyboard::Key::Space
&& player.sinceGround < 0.1f) {
player.vy = -560;
player.sinceGround = 999; // spend the jump
}
Land on platforms with the falling-only rule, in clean C++.
std::vector<sf::FloatRect> platforms = {
{0, 440, 800, 40}, {150, 340, 140, 16},
{380, 260, 140, 16}, {610, 180, 140, 16},
};
onGround = false;
sf::FloatRect pb = shape.getGlobalBounds();
for (auto& plat : platforms) {
if (pb.findIntersection(plat) && vy > 0
&& pb.position.y + pb.size.y - vy * dt <= plat.position.y + 1) {
shape.setPosition({pb.position.x, plat.position.y - pb.size.y});
vy = 0;
onGround = true;
}
}std::vector<sf::FloatRect> platforms = {
{0, 440, 800, 40}, {150, 340, 140, 16},
{380, 260, 140, 16}, {610, 180, 140, 16},
};
onGround = false;
sf::FloatRect pb = shape.getGlobalBounds();
for (auto& plat : platforms) {
if (pb.findIntersection(plat) && vy > 0
&& pb.position.y + pb.size.y - vy * dt <= plat.position.y + 1) {
shape.setPosition({pb.position.x, plat.position.y - pb.size.y});
vy = 0;
onGround = true;
}
}
shape.setPosition({pb.position.x, plat.position.y - pb.size.shape.setPosition({pb.position.x, plat.position.y - pb.size.});
vy = });
vy = ;
onGround = ;
onGround = ;;
Collect coins using the idiom you met in Episode 3, this time for real.
std::vector<sf::FloatRect> coins = {
{205, 300, 18, 18}, {435, 220, 18, 18}, {665, 140, 18, 18} };
sf::FloatRect pb = player.shape.getGlobalBounds();
int before = coins.size();
coins.erase(
std::remove_if(coins.begin(), coins.end(),
[&](const sf::FloatRect& c){ return pb.findIntersection(c); }),
coins.end());
score += (before - coins.size()) * 10;std::vector<sf::FloatRect> coins = {
{205, 300, 18, 18}, {435, 220, 18, 18}, {665, 140, 18, 18} };
sf::FloatRect pb = player.shape.getGlobalBounds();
int before = coins.size();
coins.erase(
std::remove_if(coins.begin(), coins.end(),
[&](const sf::FloatRect& c){ return pb.findIntersection(c); }),
coins.end());
score += (before - coins.size()) * 10;
Complete the level and review the clean structure you built.
// main loop shape, this is the goal:
while (window.isOpen()) {
handleEvents(window, player);
float dt = clock.restart().asSeconds();
if (state == PLAY) {
player.update(dt);
resolveCollisions(player, platforms);
collectCoins(player, coins, score);
checkGoal(player, flag, coins, state);
}
draw(window, player, platforms, coins, scoreText);
}// main loop shape, this is the goal:
while (window.isOpen()) {
handleEvents(window, player);
float dt = clock.restart().asSeconds();
if (state == PLAY) {
player.update(dt);
resolveCollisions(player, platforms);
collectCoins(player, coins, score);
checkGoal(player, flag, coins, state);
}
draw(window, player, platforms, coins, scoreText);
}
if (pb.findIntersection(flag) && coins.if (pb.findIntersection(flag) && coins.())
state = ())
state = ;;
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++ platformer.cpp -o platformer -lsfml-graphics -lsfml-window -lsfml-systemg++ platformer.cpp -o platformer -lsfml-graphics -lsfml-window -lsfml-system then run ./platformer./platformer (on Windows, from the MSYS2 MinGW terminal so it finds the SFML DLLs).
// Complete C++ Platformer for SFML 3. Build:
// g++ platformer.cpp -o platformer -lsfml-graphics -lsfml-window -lsfml-system
#include <SFML/Graphics.hpp>
#include <optional>
#include <vector>
class Player {
public:
sf::RectangleShape shape;
float vx = 0, vy = 0;
float sinceGround = 999;
bool onGround = false;
Player() {
shape.setSize(sf::Vector2f(34, 44));
shape.setPosition({100, 380});
shape.setFillColor(sf::Color(255, 119, 0));
}
void update(float dt) {
float target = 0;
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left)) target = -260;
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) target = 260;
vx += (target - vx) * 12.f * dt; // smooth toward target
vy += 1400.f * dt; // gravity
sinceGround += dt;
if (onGround) sinceGround = 0;
shape.move({vx * dt, vy * dt});
auto pos = shape.getPosition();
if (pos.x < 0) shape.setPosition({0.f, pos.y});
if (pos.x + shape.getSize().x > 800) shape.setPosition({800.f - shape.getSize().x, pos.y});
}
void jump() {
if (sinceGround < 0.1f) { // coyote time
vy = -560;
sinceGround = 999; // spend the jump
}
}
};
int main() {
sf::RenderWindow window(sf::VideoMode({800, 480}), "C++ Platformer");
window.setFramerateLimit(60);
Player player;
bool won = false;
int score = 0;
std::vector<sf::FloatRect> platforms = {
{{0.f, 440.f}, {800.f, 40.f}},
{{150.f, 340.f},{140.f, 16.f}},
{{380.f, 260.f},{140.f, 16.f}},
{{610.f, 180.f},{140.f, 16.f}}
};
std::vector<sf::FloatRect> coins = {
{{205.f, 300.f},{18.f, 18.f}},
{{435.f, 220.f},{18.f, 18.f}},
{{665.f, 140.f},{18.f, 18.f}}
};
sf::Clock clock;
while (window.isOpen()) {
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) window.close();
if (const auto* kp = event->getIf<sf::Event::KeyPressed>())
if (kp->code == sf::Keyboard::Key::Space && !won) player.jump();
}
float dt = clock.restart().asSeconds();
if (!won) {
player.update(dt);
player.onGround = false;
sf::FloatRect pb = player.shape.getGlobalBounds();
for (auto& plat : platforms) {
if (pb.findIntersection(plat) && player.vy > 0
&& pb.position.y + pb.size.y - player.vy * dt <= plat.position.y + 1) {
player.shape.setPosition({pb.position.x, plat.position.y - pb.size.y});
player.vy = 0;
player.onGround = true;
pb = player.shape.getGlobalBounds();
}
}
for (size_t i = 0; i < coins.size(); ) {
if (pb.findIntersection(coins[i])) { coins.erase(coins.begin()+i); score += 10; }
else ++i;
}
if (coins.empty()) won = true;
}
window.clear(sf::Color(25, 30, 60));
for (auto& plat : platforms) {
sf::RectangleShape r(plat.size); r.setPosition(plat.position);
r.setFillColor(sf::Color(6, 214, 160)); 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);
}
window.draw(player.shape);
window.display();
}
}// Complete C++ Platformer for SFML 3. Build:
// g++ platformer.cpp -o platformer -lsfml-graphics -lsfml-window -lsfml-system
#include <SFML/Graphics.hpp>
#include <optional>
#include <vector>
class Player {
public:
sf::RectangleShape shape;
float vx = 0, vy = 0;
float sinceGround = 999;
bool onGround = false;
Player() {
shape.setSize(sf::Vector2f(34, 44));
shape.setPosition({100, 380});
shape.setFillColor(sf::Color(255, 119, 0));
}
void update(float dt) {
float target = 0;
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Left)) target = -260;
if (sf::Keyboard::isKeyPressed(sf::Keyboard::Key::Right)) target = 260;
vx += (target - vx) * 12.f * dt; // smooth toward target
vy += 1400.f * dt; // gravity
sinceGround += dt;
if (onGround) sinceGround = 0;
shape.move({vx * dt, vy * dt});
auto pos = shape.getPosition();
if (pos.x < 0) shape.setPosition({0.f, pos.y});
if (pos.x + shape.getSize().x > 800) shape.setPosition({800.f - shape.getSize().x, pos.y});
}
void jump() {
if (sinceGround < 0.1f) { // coyote time
vy = -560;
sinceGround = 999; // spend the jump
}
}
};
int main() {
sf::RenderWindow window(sf::VideoMode({800, 480}), "C++ Platformer");
window.setFramerateLimit(60);
Player player;
bool won = false;
int score = 0;
std::vector<sf::FloatRect> platforms = {
{{0.f, 440.f}, {800.f, 40.f}},
{{150.f, 340.f},{140.f, 16.f}},
{{380.f, 260.f},{140.f, 16.f}},
{{610.f, 180.f},{140.f, 16.f}}
};
std::vector<sf::FloatRect> coins = {
{{205.f, 300.f},{18.f, 18.f}},
{{435.f, 220.f},{18.f, 18.f}},
{{665.f, 140.f},{18.f, 18.f}}
};
sf::Clock clock;
while (window.isOpen()) {
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>()) window.close();
if (const auto* kp = event->getIf<sf::Event::KeyPressed>())
if (kp->code == sf::Keyboard::Key::Space && !won) player.jump();
}
float dt = clock.restart().asSeconds();
if (!won) {
player.update(dt);
player.onGround = false;
sf::FloatRect pb = player.shape.getGlobalBounds();
for (auto& plat : platforms) {
if (pb.findIntersection(plat) && player.vy > 0
&& pb.position.y + pb.size.y - player.vy * dt <= plat.position.y + 1) {
player.shape.setPosition({pb.position.x, plat.position.y - pb.size.y});
player.vy = 0;
player.onGround = true;
pb = player.shape.getGlobalBounds();
}
}
for (size_t i = 0; i < coins.size(); ) {
if (pb.findIntersection(coins[i])) { coins.erase(coins.begin()+i); score += 10; }
else ++i;
}
if (coins.empty()) won = true;
}
window.clear(sf::Color(25, 30, 60));
for (auto& plat : platforms) {
sf::RectangleShape r(plat.size); r.setPosition(plat.position);
r.setFillColor(sf::Color(6, 214, 160)); 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);
}
window.draw(player.shape);
window.display();
}
}