Overview
This lab builds RigidBodyDynamics.cpp, an SFML program where a RigidBody struct bundles a circle shape together with the position, velocity, radius and mass of a single falling ball, and exposes applyGravity, updatePosition and handleCollisions as its own member functions instead of loose global state. gravity accelerates the ball downward every frame, integration carries that velocity into position, and a restitution-scaled bounce off all four edges of the window, floor, ceiling and both side walls, keeps the ball contained inside it indefinitely.
Learning Objectives
By the end of this lab you will be able to:
- Explain why grouping a physics body's position, velocity, radius, mass and drawable shape into a single RigidBody struct with its own member functions organizes state that would otherwise be scattered across loose global variables and functions
- Implement applyGravity so a constant downward acceleration accumulates into a body's vertical velocity scaled by elapsed time
- Implement updatePosition so accumulated velocity is integrated into position each frame and the drawn shape stays in sync with it
- Implement handleCollisions so a body's position is clamped and its velocity reversed and scaled by a restitution coefficient against all four edges of a window
- Describe, from an observed run, why scaling velocity by a restitution coefficient below 1.0 on every bounce produces bounces that shrink over time instead of staying constant
Concept
A ball that only bounces off one edge, as built in the Gravity and Restitution lab, does not behave like a body genuinely confined to a box. once a scene needs more than one falling object at a time, scattering a single object's position, velocity, mass and drawable shape across loose global variables and free functions stops being maintainable, since every function that touches the ball needs its own separate parameter list.
A struct named RigidBody groups exactly that state together. an sf::CircleShape, a position and velocity as sf::Vector2f, and radius and mass as floats, with its own member functions, applyGravity, updatePosition and handleCollisions, that operate on that state directly instead of taking it as parameters. calling ball.applyGravity(deltaTime) adds a constant GRAVITY acceleration into velocity.y scaled by deltaTime, the same Euler integration used to fall in earlier labs, and updatePosition folds velocity back into position each frame, then copies position onto shape so the next window.draw(ball.shape) reflects where the body actually is. handleCollisions then checks all four edges of the window in turn, clamping position back inside whichever boundary it crossed and reversing the relevant component of velocity scaled by a RESTITUTION coefficient below 1.0f, so each bounce loses a little energy, the same floor bounce built earlier but now applied to every side of the window at once.
The API involved is small. sf::CircleShape::setOrigin() centers a shape so it moves and scales from its middle rather than its top-left corner. std::clamp from <algorithm> restrains a value between a low and a high bound in one call, used here to keep a horizontal position between the window's two side walls.
Tasks
Task 1 - Defining the RigidBody struct
Create a new Console App (C++) project the same way as in the Environment Setup lab, naming it RigidBodyDynamics. Use Add > New Item... to add a new C++ source file named RigidBodyDynamics.cpp, and write the skeleton below. Complete the constructor of RigidBody, which sets up shape from the radius, mass and startPos passed into it.
- Set
shape's radius toradius. - Set
shape's fill color tosf::Color::Green. - Set
shape's origin to(radius, radius), so it moves and scales from its center. - Set
shape's position toposition.
Success condition: build and run the project. a single static green ball appears near the top of the window and does not move, since nothing updates it yet.
#include <SFML/Graphics.hpp>
#include <algorithm>
#include <cmath>
const float WINDOW_WIDTH = 800;
const float WINDOW_HEIGHT = 600;
const float GRAVITY = 9.8f; // Pixels per second squared
const float RESTITUTION = 0.8f; // Bounce factor, between 0 (no bounce) and 1 (full bounce)
struct RigidBody {
sf::CircleShape shape;
sf::Vector2f position;
sf::Vector2f velocity;
float radius;
float mass;
RigidBody(float r, float m, sf::Vector2f startPos)
: radius(r), mass(m), position(startPos), velocity(0, 0) {
// TODO: set shape's radius to radius
// TODO: set shape's fill color to sf::Color::Green
// TODO: set shape's origin to (radius, radius), centering it
// TODO: set shape's position to position
}
};
int main() {
sf::RenderWindow window(sf::VideoMode({static_cast<unsigned int>(WINDOW_WIDTH), static_cast<unsigned int>(WINDOW_HEIGHT)}), "Rigid Body Dynamics with SFML");
window.setFramerateLimit(165);
RigidBody ball(20.0f, 1.0f, sf::Vector2f(WINDOW_WIDTH / 2, WINDOW_HEIGHT / 4));
sf::Clock clock;
while (window.isOpen()) {
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>())
window.close();
}
float deltaTime = clock.restart().asSeconds();
window.clear(sf::Color::Black);
window.draw(ball.shape);
window.display();
}
return 0;
}
Task 2 - Falling under gravity
Open RigidBodyDynamics.cpp. Add two member functions to RigidBody, applyGravity and updatePosition, then call both on ball inside the main loop, before window.clear().
- In
applyGravity(float deltaTime), addGRAVITY * deltaTimetovelocity.y. - In
updatePosition(float deltaTime), addvelocity * deltaTimetoposition, then setshape's position topositionso the drawn ball follows it. - In
main, callball.applyGravity(deltaTime)followed byball.updatePosition(deltaTime)each frame, before the window is cleared.
Success condition: build and run the project. the ball now falls, picking up speed as it goes. Task 3 asks you to actually run it and see what happens next.
#include <SFML/Graphics.hpp>
#include <algorithm>
#include <cmath>
const float WINDOW_WIDTH = 800;
const float WINDOW_HEIGHT = 600;
const float GRAVITY = 9.8f; // Pixels per second squared
const float RESTITUTION = 0.8f; // Bounce factor, between 0 (no bounce) and 1 (full bounce)
struct RigidBody {
sf::CircleShape shape;
sf::Vector2f position;
sf::Vector2f velocity;
float radius;
float mass;
RigidBody(float r, float m, sf::Vector2f startPos)
: radius(r), mass(m), position(startPos), velocity(0, 0) {
shape.setRadius(radius);
shape.setFillColor(sf::Color::Green);
shape.setOrigin({radius, radius});
shape.setPosition(position);
}
void applyGravity(float deltaTime) {
// TODO: add GRAVITY * deltaTime to velocity.y
}
void updatePosition(float deltaTime) {
// TODO: add velocity * deltaTime to position
// TODO: set shape's position to position
}
};
int main() {
sf::RenderWindow window(sf::VideoMode({static_cast<unsigned int>(WINDOW_WIDTH), static_cast<unsigned int>(WINDOW_HEIGHT)}), "Rigid Body Dynamics with SFML");
window.setFramerateLimit(165);
RigidBody ball(20.0f, 1.0f, sf::Vector2f(WINDOW_WIDTH / 2, WINDOW_HEIGHT / 4));
sf::Clock clock;
while (window.isOpen()) {
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>())
window.close();
}
float deltaTime = clock.restart().asSeconds();
// TODO: call ball.applyGravity(deltaTime)
// TODO: call ball.updatePosition(deltaTime)
window.clear(sf::Color::Black);
window.draw(ball.shape);
window.display();
}
return 0;
}
Task 3 - Observing an unconstrained fall
Build and run RigidBodyDynamics.cpp as it stands after Task 2, with no wall or floor checks yet.
- Watch the ball from the moment the window opens until it disappears.
- Note how its downward speed visibly increases the longer it falls, since
applyGravityadds a little more tovelocity.yevery single frame.
Success condition: the ball accelerates downward and falls straight off the bottom of the window, never to return. that is expected, nothing in RigidBody yet stops it there. Task 4 adds that.
Task 4 - Containing the body inside the window
Open RigidBodyDynamics.cpp. Add a third member function, handleCollisions, to RigidBody, then call it on ball in main, directly after ball.updatePosition(deltaTime).
- If
position.y + radiushas reached or passedWINDOW_HEIGHT, the bottom edge, setposition.ytoWINDOW_HEIGHT - radiusand multiplyvelocity.yby-RESTITUTION. - If
position.y - radiushas reached or passed0, the top edge, setposition.ytoradiusand multiplyvelocity.yby-RESTITUTION. - If
position.x - radiushas reached or passed0, orposition.x + radiushas reached or passedWINDOW_WIDTH, multiplyvelocity.xby-RESTITUTION, then setposition.xtostd::clamp(position.x, radius, WINDOW_WIDTH - radius).
Success condition: build and run the project. the ball now bounces off the floor instead of falling through it, each bounce a little lower than the last, and stays on screen indefinitely instead of disappearing off the bottom.
#include <SFML/Graphics.hpp>
#include <algorithm>
#include <cmath>
const float WINDOW_WIDTH = 800;
const float WINDOW_HEIGHT = 600;
const float GRAVITY = 9.8f; // Pixels per second squared
const float RESTITUTION = 0.8f; // Bounce factor, between 0 (no bounce) and 1 (full bounce)
struct RigidBody {
sf::CircleShape shape;
sf::Vector2f position;
sf::Vector2f velocity;
float radius;
float mass;
RigidBody(float r, float m, sf::Vector2f startPos)
: radius(r), mass(m), position(startPos), velocity(0, 0) {
shape.setRadius(radius);
shape.setFillColor(sf::Color::Green);
shape.setOrigin({radius, radius});
shape.setPosition(position);
}
void applyGravity(float deltaTime) {
velocity.y += GRAVITY * deltaTime;
}
void updatePosition(float deltaTime) {
position += velocity * deltaTime;
shape.setPosition(position);
}
void handleCollisions() {
// TODO: if position.y + radius has reached WINDOW_HEIGHT, clamp
// position.y to WINDOW_HEIGHT - radius and multiply velocity.y
// by -RESTITUTION
// TODO: if position.y - radius has reached 0, clamp position.y
// to radius and multiply velocity.y by -RESTITUTION
// TODO: if position.x - radius has reached 0, or position.x + radius
// has reached WINDOW_WIDTH, multiply velocity.x by -RESTITUTION and
// clamp position.x between radius and WINDOW_WIDTH - radius using
// std::clamp
}
};
int main() {
sf::RenderWindow window(sf::VideoMode({static_cast<unsigned int>(WINDOW_WIDTH), static_cast<unsigned int>(WINDOW_HEIGHT)}), "Rigid Body Dynamics with SFML");
window.setFramerateLimit(165);
RigidBody ball(20.0f, 1.0f, sf::Vector2f(WINDOW_WIDTH / 2, WINDOW_HEIGHT / 4));
sf::Clock clock;
while (window.isOpen()) {
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>())
window.close();
}
float deltaTime = clock.restart().asSeconds();
ball.applyGravity(deltaTime);
ball.updatePosition(deltaTime);
// TODO: call ball.handleCollisions()
window.clear(sf::Color::Black);
window.draw(ball.shape);
window.display();
}
return 0;
}
Task 5 - Testing every wall
With handleCollisions complete, the floor and ceiling checks are already visible since gravity alone drives the ball into them. the two side walls are not, because ball starts with velocity(0, 0) and nothing in the loop ever gives it horizontal speed. Open RigidBodyDynamics.cpp and give it some, just after ball is constructed in main.
- Add the line
ball.velocity.x = 150.0f;directly after theRigidBody ball(...)line. - Build and run the project. watch the ball fall, bounce vertically off the floor and ceiling, and drift sideways until it reaches a side wall.
- Change
RESTITUTIONto0.95f, rebuild, and run again. note how much longer the ball keeps bouncing to a visible height before its bounces become too small to notice. - Change
RESTITUTIONto0.4f, rebuild, and run again. note how much sooner it happens this time. - Set
RESTITUTIONback to0.8f.
Success condition: the ball bounces off all four edges of the window, floor, ceiling and both side walls, and you can explain from what you observed why a RESTITUTION closer to 1.0f keeps visible bounces going for longer than a value closer to 0.0f.
#include <SFML/Graphics.hpp>
#include <algorithm>
#include <cmath>
const float WINDOW_WIDTH = 800;
const float WINDOW_HEIGHT = 600;
const float GRAVITY = 9.8f; // Pixels per second squared
const float RESTITUTION = 0.8f; // Bounce factor, between 0 (no bounce) and 1 (full bounce). try 0.95f, then 0.4f
struct RigidBody {
sf::CircleShape shape;
sf::Vector2f position;
sf::Vector2f velocity;
float radius;
float mass;
RigidBody(float r, float m, sf::Vector2f startPos)
: radius(r), mass(m), position(startPos), velocity(0, 0) {
shape.setRadius(radius);
shape.setFillColor(sf::Color::Green);
shape.setOrigin({radius, radius});
shape.setPosition(position);
}
void applyGravity(float deltaTime) {
velocity.y += GRAVITY * deltaTime;
}
void updatePosition(float deltaTime) {
position += velocity * deltaTime;
shape.setPosition(position);
}
void handleCollisions() {
if (position.y + radius >= WINDOW_HEIGHT) {
position.y = WINDOW_HEIGHT - radius;
velocity.y *= -RESTITUTION;
}
if (position.y - radius <= 0) {
position.y = radius;
velocity.y *= -RESTITUTION;
}
if (position.x - radius <= 0 || position.x + radius >= WINDOW_WIDTH) {
velocity.x *= -RESTITUTION;
position.x = std::clamp(position.x, radius, WINDOW_WIDTH - radius);
}
}
};
int main() {
sf::RenderWindow window(sf::VideoMode({static_cast<unsigned int>(WINDOW_WIDTH), static_cast<unsigned int>(WINDOW_HEIGHT)}), "Rigid Body Dynamics with SFML");
window.setFramerateLimit(165);
RigidBody ball(20.0f, 1.0f, sf::Vector2f(WINDOW_WIDTH / 2, WINDOW_HEIGHT / 4));
ball.velocity.x = 150.0f;
sf::Clock clock;
while (window.isOpen()) {
while (const std::optional event = window.pollEvent()) {
if (event->is<sf::Event::Closed>())
window.close();
}
float deltaTime = clock.restart().asSeconds();
ball.applyGravity(deltaTime);
ball.updatePosition(deltaTime);
ball.handleCollisions();
window.clear(sf::Color::Black);
window.draw(ball.shape);
window.display();
}
return 0;
}
Solution
Try the tasks above before revealing the solution.