University of Wales Trinity Saint David
ACCB5017 - Games Architecture and Mechanics

Spring-Driven Motion

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:

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.

  1. Set initialVelocity to the result of equating the spring's potential energy with the projectile's kinetic energy and solving for v, using std::sqrt.
  2. Print initialVelocity with std::cout so 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.

12kx2=12mv2\frac{1}{2} k x^2 = \frac{1}{2} m v^2
v=kx2mv = \sqrt{\frac{k x^2}{m}}
SpringLauncher.cpp
#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.

  1. Convert launchAngleDegrees to radians with launchAngleDegrees * PI / 180.0f.
  2. Set velocity.x to initialVelocity times std::cos of that angle in radians.
  3. Set velocity.y to negative initialVelocity times std::sin of 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.

vx=vcos⁡θvy=−vsin⁡θv_x = v \cos\theta \qquad v_y = -v \sin\theta
SpringLauncher.cpp
#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.

  1. Build and run with launchAngleDegrees at 45.0f and springConstant at 500000.0f, press Space, and note roughly how high and how far the projectile travels.
  2. Change launchAngleDegrees to 25.0f, rebuild, run again, and note how the arc becomes flatter and longer.
  3. Set launchAngleDegrees back to 45.0f, change springConstant to 100000.0f, rebuild, run again, and note how much slower and shorter the launch becomes.
  4. Set both constants back to values of your choice and leave a short comment above initialVelocity explaining, 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.

SpringLauncher.cpp
#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.