Overview
This lab builds a small SFML program, SpringLauncher.cpp, where a sf::CircleShape projectile sits pulled back on a compressed spring until Space is pressed. Hooke's Law converts the spring's stored energy into a single launch speed, which is then split into horizontal and vertical velocity components at a launch angle. once launched, the projectile falls under gravity and bounces off the floor and the right wall using the same per-frame gravity integration and restitution-style bounce built in the Gravity and Restitution lab, given here as complete working code rather than repeated as a new task. You will complete the launch speed calculation and the velocity decomposition, then optionally run the result and tune one constant to see its effect.
Learning Objectives
By the end of this lab you will be able to:
- Explain how equating a spring's stored potential energy with a projectile's kinetic energy produces a formula for its launch speed
- Implement that formula to compute a projectile's launch speed from a spring constant, compression distance and mass
- Decompose a single launch speed into horizontal and vertical velocity components using std::cos and std::sin of a launch angle
- Explain why a launch angle's vertical velocity component must be negated in SFML's window coordinate system
- Run the launch and describe, from what is observed, how changing the launch angle or spring constant changes the flight path
Concept
Hooke's Law is not just a way to launch one projectile in one demo. it describes any restoring force that grows in proportion to how far something is displaced, and that pattern shows up across game development far beyond a literal spring. A catapult, a pinball plunger, and a bow drawn back to fire an arrow all convert a pulled-back distance into a launch speed exactly the way this lab's spring does. The same relationship also drives spring-damper systems used to smooth a camera that follows the player instead of snapping to their position, ease a UI element into place, or give a character's secondary animation some "squash and stretch" instead of moving rigidly. all of these reuse the same idea, a single spring constant that turns a displacement into a force or a speed, because it is cheap to compute and easy to tune without running a full physics simulation. The launch speed calculated in this lab is the same calculation underneath all of those uses, applied to the simplest case there is, one spring, one launch, one direction.
The projectile in this lab does not start with a velocity handed to it, it earns one from a spring. Before the spring is released, a spring constant and a compression distance already determine, on their own, exactly how fast the projectile leaves the spring. Without a way to convert that stored energy into a speed, there is no single number to hand off to the launch, only a spring constant and a distance sitting in isolation.
Hooke's Law says a compressed spring stores potential energy equal to 0.5f * springConstant * compressionDistance * compressionDistance. when the spring releases, that energy converts entirely into the projectile's kinetic energy, 0.5f * mass * velocity * velocity. setting the two equal and solving for velocity gives a single formula for launch speed, computed once with std::sqrt from <cmath>. that speed alone is not a usable velocity yet, since a velocity in this project is an sf::Vector2f with separate x and y components, and a spring only tells us how fast, not in which of those two directions.
Splitting a single speed into a direction is vector decomposition. a launch angle, measured in degrees and converted to radians for std::cos and std::sin, gives velocity.x as speed times cosine of the angle and velocity.y as speed times sine of the angle. because SFML's window coordinates place y increasing downward, an upward launch needs that y component negated, or the projectile would launch into the floor instead of into the air.
Tasks
Task 1 - Converting spring energy into launch speed
Open SpringLauncher.cpp and find initialVelocity, just above the launch angle constant. springConstant is the spring constant k, compressionDistance is x, and mass is m. Complete the formula that turns those three into a single launch speed v.
- Set
initialVelocityto the result of equating the spring's potential energy with the projectile's kinetic energy and solving forv, usingstd::sqrt. - Print
initialVelocitywithstd::coutso its value can be checked before the projectile ever launches.
Success condition: build and run the project. the console prints an initialVelocity of approximately 300, matching springConstant = 500000.0f, compressionDistance = 0.3f and mass = 0.5f. the projectile itself does not move yet, since velocity.x and velocity.y are still 0.0f until Task 2.
#include <SFML/Graphics.hpp>
#include <cmath>
#include <iostream>
int main()
{
sf::RenderWindow window(sf::VideoMode({800, 600}), "Projectile launched by a spring");
sf::CircleShape projectile(10.0f);
projectile.setFillColor(sf::Color::Red);
projectile.setOrigin({projectile.getRadius(), projectile.getRadius()});
sf::Vector2f position(100.0f, 500.0f);
projectile.setPosition(position);
const float springConstant = 500000.0f; // spring constant k, in newtons per metre
const float compressionDistance = 0.3f; // how far the spring is compressed, in metres
const float mass = 0.5f; // projectile mass in kilograms
const float gravity = 9.81f;
const float launchAngleDegrees = 45.0f;
bool launched = false;
float initialVelocity = 0.0f;
// TODO: set initialVelocity to the launch speed a spring with
// springConstant and compressionDistance imparts on a projectile of
// the given mass, using std::sqrt and the formula from the concept
// section above
// TODO: print initialVelocity with std::cout so its value can be
// checked before the projectile ever launches
const float PI = 3.14159265359f;
sf::Vector2f velocity(0.0f, 0.0f);
// velocity.x and velocity.y are completed in Task 2
sf::Clock clock;
while (window.isOpen())
{
while (const std::optional event = window.pollEvent())
{
if (event->is<sf::Event::Closed>()) window.close();
if (const auto* key = event->getIf<sf::Event::KeyPressed>(); key && key->code == sf::Keyboard::Key::Space && !launched)
{
launched = true;
}
}
float deltaTime = clock.restart().asSeconds();
if (launched)
{
position += velocity * deltaTime;
velocity.y += gravity * deltaTime;
}
else
{
projectile.setPosition(position - sf::Vector2f(compressionDistance * 100.0f, 0.0f));
}
if (position.y > window.getSize().y - projectile.getRadius())
{
position.y = window.getSize().y - projectile.getRadius();
velocity.y *= -0.5f;
}
if (position.x > window.getSize().x - projectile.getRadius())
{
position.x = window.getSize().x - projectile.getRadius();
velocity.x *= -0.5f;
}
window.clear(sf::Color::White);
projectile.setPosition(position);
window.draw(projectile);
window.display();
}
}
Task 2 - Decomposing launch speed into a velocity vector
Open SpringLauncher.cpp again. initialVelocity from Task 1 is filled in below. Add the two lines that split it into velocity.x and velocity.y at launchAngleDegrees.
- Convert
launchAngleDegreesto radians withlaunchAngleDegrees * PI / 180.0f. - Set
velocity.xtoinitialVelocitytimesstd::cosof that angle in radians. - Set
velocity.yto negativeinitialVelocitytimesstd::sinof that angle in radians. the sign is negative because SFML's y axis increases downward, so an upward launch needs a negative y velocity.
Success condition: build and run, then press Space. the projectile launches up and to the right at roughly a 45 degree angle, arcs under gravity, and bounces off the floor and the right wall the way the Gravity and Restitution lab already established, instead of sitting still or dropping straight down.
#include <SFML/Graphics.hpp>
#include <cmath>
#include <iostream>
int main()
{
sf::RenderWindow window(sf::VideoMode({800, 600}), "Projectile launched by a spring");
sf::CircleShape projectile(10.0f);
projectile.setFillColor(sf::Color::Red);
projectile.setOrigin({projectile.getRadius(), projectile.getRadius()});
sf::Vector2f position(100.0f, 500.0f);
projectile.setPosition(position);
const float springConstant = 500000.0f; // spring constant k, in newtons per metre
const float compressionDistance = 0.3f; // how far the spring is compressed, in metres
const float mass = 0.5f; // projectile mass in kilograms
const float gravity = 9.81f;
const float launchAngleDegrees = 45.0f;
bool launched = false;
float initialVelocity = std::sqrt((springConstant * compressionDistance * compressionDistance) / mass);
std::cout << "initial launch speed: " << initialVelocity << std::endl;
const float PI = 3.14159265359f;
sf::Vector2f velocity(0.0f, 0.0f);
// TODO: convert launchAngleDegrees to radians (launchAngleDegrees * PI / 180.0f)
// TODO: set velocity.x to initialVelocity * std::cos(angle in radians)
// TODO: set velocity.y to -initialVelocity * std::sin(angle in radians)
sf::Clock clock;
while (window.isOpen())
{
while (const std::optional event = window.pollEvent())
{
if (event->is<sf::Event::Closed>()) window.close();
if (const auto* key = event->getIf<sf::Event::KeyPressed>(); key && key->code == sf::Keyboard::Key::Space && !launched)
{
launched = true;
}
}
float deltaTime = clock.restart().asSeconds();
if (launched)
{
position += velocity * deltaTime;
velocity.y += gravity * deltaTime;
}
else
{
projectile.setPosition(position - sf::Vector2f(compressionDistance * 100.0f, 0.0f));
}
if (position.y > window.getSize().y - projectile.getRadius())
{
position.y = window.getSize().y - projectile.getRadius();
velocity.y *= -0.5f;
}
if (position.x > window.getSize().x - projectile.getRadius())
{
position.x = window.getSize().x - projectile.getRadius();
velocity.x *= -0.5f;
}
window.clear(sf::Color::White);
projectile.setPosition(position);
window.draw(projectile);
window.display();
}
}
Task 3 - Running the launch and tuning the spring
With both formulas complete, SpringLauncher.cpp now runs the full launch described in the lab overview. there is no new code in this task, just constants to edit directly and observe.
- Build and run with
launchAngleDegreesat45.0fandspringConstantat500000.0f, pressSpace, and note roughly how high and how far the projectile travels. - Change
launchAngleDegreesto25.0f, rebuild, run again, and note how the arc becomes flatter and longer. - Set
launchAngleDegreesback to45.0f, changespringConstantto100000.0f, rebuild, run again, and note how much slower and shorter the launch becomes. - Set both constants back to values of your choice and leave a short comment above
initialVelocityexplaining, in your own words, what changing each constant did to the launch.
Success condition: you can describe, from what you observed, why a smaller launchAngleDegrees produces a flatter arc and why a smaller springConstant produces a slower launch, in terms of the formula from Task 1.
#include <SFML/Graphics.hpp>
#include <cmath>
#include <iostream>
int main()
{
sf::RenderWindow window(sf::VideoMode({800, 600}), "Projectile launched by a spring");
sf::CircleShape projectile(10.0f);
projectile.setFillColor(sf::Color::Red);
projectile.setOrigin({projectile.getRadius(), projectile.getRadius()});
sf::Vector2f position(100.0f, 500.0f);
projectile.setPosition(position);
const float springConstant = 500000.0f; // spring constant k, in newtons per metre. try 100000.0f
const float compressionDistance = 0.3f; // how far the spring is compressed, in metres
const float mass = 0.5f; // projectile mass in kilograms
const float gravity = 9.81f;
const float launchAngleDegrees = 45.0f; // try 25.0f for a flatter arc
bool launched = false;
float initialVelocity = std::sqrt((springConstant * compressionDistance * compressionDistance) / mass);
std::cout << "initial launch speed: " << initialVelocity << std::endl;
const float PI = 3.14159265359f;
float launchAngleRadians = launchAngleDegrees * PI / 180.0f;
sf::Vector2f velocity(initialVelocity * std::cos(launchAngleRadians), -initialVelocity * std::sin(launchAngleRadians));
sf::Clock clock;
while (window.isOpen())
{
while (const std::optional event = window.pollEvent())
{
if (event->is<sf::Event::Closed>()) window.close();
if (const auto* key = event->getIf<sf::Event::KeyPressed>(); key && key->code == sf::Keyboard::Key::Space && !launched)
{
launched = true;
}
}
float deltaTime = clock.restart().asSeconds();
if (launched)
{
position += velocity * deltaTime;
velocity.y += gravity * deltaTime;
}
else
{
projectile.setPosition(position - sf::Vector2f(compressionDistance * 100.0f, 0.0f));
}
if (position.y > window.getSize().y - projectile.getRadius())
{
position.y = window.getSize().y - projectile.getRadius();
velocity.y *= -0.5f;
}
if (position.x > window.getSize().x - projectile.getRadius())
{
position.x = window.getSize().x - projectile.getRadius();
velocity.x *= -0.5f;
}
window.clear(sf::Color::White);
projectile.setPosition(position);
window.draw(projectile);
window.display();
}
}
Solution
Try the tasks above before revealing the solution.