Momentum and Impulse Forces

Add explosions and jumping to your game by mastering momentum-based impulse forces
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

In this lesson, we'll explore how to implement physics interactions by adding momentum and impulses to our simulations.

We'll learn we can use physics to create features requiring sudden changes to motion, such as having our characters jump or get knocked back by explosions. We’ll also learn how to modify the strength of those forces based on how far away the source of the force is.

This lesson continues to use the components and application loop we introduced earlier in the section. We’ll mostly be working in the GameObject class.

The most relevant parts of this class to note for this lesson are its Tick() and ApplyForce() functions, as well as the Position, Velocity, Acceleration, and Mass variables. Those functions and variables currently look like this:

// GameObject.h
// ...

class GameObject {
 public:
  void Tick(float DeltaTime) {
    ApplyForce(FrictionForce(DeltaTime));
    ApplyForce(DragForce());
    Velocity += Acceleration * DeltaTime;
    Position += Velocity * DeltaTime;

    Acceleration = {0, -9.8};
    Clamp(Velocity);

    // Don't fall through the floor
    if (Position.y < 2) Position.y = 2;
  }

  void ApplyForce(const Vec2& Force) {
    Acceleration += Force / Mass;
  }
  
  // ...

 private:
  Vec2 Position{0, 0};
  Vec2 Velocity{5, 0};
  Vec2 Acceleration{0, -9.8};
  float Mass{70};
  // ....
};

Momentum

Before we start adding to our class, there are two more concepts in physics we need to understand - momentum, and impulse. Momentum is a combination of an object’s velocity with its mass:

Momentum=Mass×Velocity \text{Momentum} = \text{Mass} \times \text{Velocity}

For example, if two objects have the same velocity, the heavier object will have more momentum. If a lighter object has the same momentum as a heavier object, that means the lighter object is moving faster.

Given that momentum is a mass multiplied by a velocity, the unit used to represent momentum will be a unit of mass (often kilograms, kgkg) multiplied by a unit of velocity (often meters per second, m/sm/s). When units are multiplied together, it’s common to represent that multiplication using a center dot \cdot

For example:

  • A 1kg1kg object moving at 1m/s1m/s has a momentum of 1kgm/s1kg\mathclose{\cdot} m/s
  • A 10kg10kg object moving at 1m/s1m/s has a momentum of 10kgm/s10kg\mathclose{\cdot} m/s
  • A 1kg1kg object moving at 10m/s10m/s also has a momentum of 10kgm/s10kg\mathclose{\cdot} m/s

Impulse

A change in an object’s momentum is called an impulse. In our simulations, changes in momentum are almost always caused by changes in velocity, rather than a change in the object’s mass.

As we’ve seen, a change in velocity requires acceleration, and acceleration is caused by a force being applied to an object for some period of time. Increasing the force, or applying the force for a longer duration, results in larger changes of momentum.  Therefore:

Impulse=Force×Time \text{Impulse} = \text{Force} \times \text{Time}

Newton-Seconds

The kgm/skg\mathclose{\cdot}m/s unit used to measure momentum (and changes in momentum) is often referred to by an alternative, equivalent unit: the Newton-second, Ns\text{N}\mathclose{\cdot}s. For example:

  • Applying 1N1N of force for 1s1s creates an impulse of 1Ns1\text{N}\mathclose{\cdot}s
  • Applying 10N10N of force for 1s1s creates an impulse of 10Ns10\text{N}\mathclose{\cdot}s
  • Applying 1N1N of force for 10s10s also creates an impulse of 10Ns10\text{N}\mathclose{\cdot}s

Previously, we saw how a Newton is the amount of force required to accelerate a 1kg1kg object by 1m/s21m/s^2.

If we apply this 1m/s21m/s^2 acceleration for one second, the object’s velocity will change by 1m/s1m/s. And, given the object has 1kg1kg of mass, its momentum will change by 1kgm/s1kg\mathclose{\cdot} m/s. Therefore, 1Ns=1kgm/s1 \text{N}\mathclose{\cdot}s = 1 kg \cdot m/s. Here are some more examples:

  • 10Ns10 \text{N}\mathclose{\cdot}s of impulse will change a 1kg1kg object’s velocity by 10m/s10 m/s
  • 100Ns100 \text{N}\mathclose{\cdot}s of impulse will change a 10kg10kg object’s velocity by 10m/s10 m/s
  • 100Ns100 \text{N}\mathclose{\cdot}s of impulse will change a 100kg100kg object’s velocity by 1m/s1 m/s

Momentum Deltas and Δp\Delta p

In academic papers and learning resources, the variable pp momentum is often represented by the variable pp, and the greek letter Delta, Δ\Delta or it’s lowercase form δ\delta, is used to represent change.

Therefore, impulse (change in momentum) is often written as Δp\Delta p and its equation is Δp=FΔt\Delta p = F \Delta t.

In this equation, FF is the force, and Δt\Delta t is the duration of time over which the force is applied - that is, the change between the starting time and ending time.

Implementing Impulses

The time component of an impulse can be any duration but, in our simulations, the main use case for impulses is to apply instantaneous changes in momentum. That is, a force that is applied a single time, within a single tick / frame.

Examples of this might include letting our character jump off the ground or having objects react to an explosition.

The key technical difference between an instant impulse and a continuous force is that impulses should not be modified by our frame’s delta time. A continuous force gets applied across many steps of our simulation, so its effect on each frame should depend on how much time has passed since the previous frame.

But an instant impulse is applied all within a single step, and its magnitude should not depend on how long that frame took to generate. Therefore, an implementation of impulses might look like this:

// GameObject.h
// ...

class GameObject {
// ...
 private:
  void ApplyImpulse(const Vec2& Impulse) {
    Velocity += Impulse / Mass;
  }
  // ...
};

Example: Jumping

Below, we use this mechanism to let our character jump when the player presses their spacebar:

// GameObject.h
// ...

class GameObject {
 public:
  // ...
  void HandleEvent(const SDL_Event& E) {
    if (E.type == SDL_KEYDOWN) {
      if (E.key.keysym.sym == SDLK_SPACE) {
        ApplyImpulse({0.0f, 15.0f});
      }
    }
  }
  // ...
};
A screenshot of our scene showing a jumping trajectory

Non-Instant Impulses

If we want to apply a force that continues for a fixed period of time, we can use our previous ApplyForce() technique, in conjunction with some mechanism that keeps track of time.

For example, we could use the SDL_Timer mechanisms we introduced previously, or accumulate time deltas that are flowing through our Tick() function until our accumulation has reached some target.

The following program uses the latter technique to apply a force for approximately 5 seconds:

// GameObject.h
// ...

class GameObject {
public:
  // ...
  void Tick(float DeltaTime) {
    ApplyForce(FrictionForce(DeltaTime));
    ApplyForce(DragForce());
    
    // Apply the timed force if active
    if (ForceTimeRemaining > 0) {
      ApplyForce({1, 2});
      ForceTimeRemaining -= DeltaTime;
    }
    
    Velocity += Acceleration * DeltaTime;
    Position += Velocity * DeltaTime;

    Acceleration = {0.0f, -9.8};
    Clamp(Velocity);

    // Don't fall through the floor
    if (Position.y < 2) {
      Position.y = 2;
      Velocity.y = 0;
    }
  }
  
  // ...
private:
  float ForceTimeRemaining{5.0f};
  // ...
};

Positional Impulses

So far, our examples have implemented forces such that our objects feel their full effect. This is reasonable for effects like gravity and jumping but, in many cases, we want to simulate forces that have a specific point of origin.

For example, we might have an explosion happening somewhere in the world, with the effect that objects are knocked away from that location.

As such, the direction of the effect of that force on each object depends on that specific object’s position relative to where the explosion happened. Let’s set that up:

// GameObject.h
// ...

class GameObject {
// ...
 private:
  void ApplyPositionalImpulse(
    const Vec2& Origin, float Magnitude
  ) {
    Vec2 Direction{(Position - Origin)
      .Normalize()};
  
    ApplyImpulse(Direction * Magnitude);
  }
  // ...
};

Positional Forces

We can create similar positional effects for sustained forces rather than impulses. We’d apply the exact same logic, but rather than applying an immediate change in momentum using ApplyImpulse(), we’d use the ApplyForce() approach we covered in the previous lesson.

We’d also need to ensure that the force is continuously sustained by, for example, applying it on every Tick() invocation until it is no longer needed.

Example: Explosions

Let’s create an example where the player can click a position on the screen to create an explosion, knocking nearby objects away. The first challenge we have is that SDL will report the position of the click in screen-space, and we need the position to be in world-space for our physics simulation.

Our Scene class already has a function to convert world space positions to screen space positions. Let’s add an inverse function for converting screen space to world space:

// Scene.h
// ...

class Scene {
public:
  // ...
Vec2 ToScreenSpace(const Vec2&) {/*...*/} Vec2 ToWorldSpace(const Vec2& Pos) const { auto [vx, vy, vw, vh]{Viewport}; float HorizontalScaling{WorldSpaceWidth / vw}; float VerticalScaling{WorldSpaceHeight / vh}; return { (Pos.x - vx) * HorizontalScaling, WorldSpaceHeight - (Pos.y - vy) * VerticalScaling }; } // ... };

Our GameObject instances are currently receiving events within their HandleEvent() method, so we can access click events and the corresponding mouse position from there.

However, within our GameObject.h file, Scene is an incomplete type, meaning we can’t use our new ToWorldSpace() function from this header file. So, similar to our Render() function, our HandleEvent() function definition needs to move to the source file:

// GameObject.h
// ...

class Scene;  // Incomplete type

class GameObject {
 public:
  // Definition moved to GameObject.cpp
  void HandleEvent(const SDL_Event& E); 
};

In our source file, we’ll create a Vec2 based on the mouse position, and we’ll pass that vector to our new ToWorldSpace() function to determine where that click happened in our world.

The force from an explosion is a quick, sudden, blast. As such, we’ll implement it as an immediate change in momentum using our ApplyPositionalImpulse() function. We’ll pass our Vec2 to that function, alongside a magnitude that feels right We’ll go with 100N100N for now:

// GameObject.cpp
// ...

void GameObject::HandleEvent(const SDL_Event& E) {
  if (E.type == SDL_MOUSEBUTTONDOWN) {
    if (E.button.button == SDL_BUTTON_LEFT) {
      ApplyPositionalImpulse(
        Scene.ToWorldSpace({
          static_cast<float>(E.button.x),
          static_cast<float>(E.button.y)
      }), 100);
    }
  }
  // ... Other event handlers
}

Running our game, we should now see that any time we click in our window, our objects are knocked away from our mouse:

A screenshot of our scene showing the reaction to explosions

Falloff and the Inverse-Square Law

An additional property of a force originating from a specific position, such as an explosion, is that objects closer to the explosion are affected more than objects further away.

To make our calculations simpler, when specifying the magnitude of a positional force, we specify it in terms of how that force will feel to an object that is one meter away from it.

For example, we might want to create an explosion where objects one meter away experience 100N100N of force. Let’s represent that as F(1)=100\text{F(1)} = 100.

The magnitude of the force experienced by objects at different distances follows the inverse-square law, where the effect falls off in proportion to the square of the distance.

As such, a function for calculating this falloff would look something like the following, where dd represents the distance between the explosion and the object that’s reacting to it, and F(1)\text{F(1)} is the magnitude that would be experienced by an object one meter away:

F(d)=F(1)d2 \text{F(d)} = \dfrac{\text{F(1)}}{d^2}

Let’s revisit our hypothetical explosion, where we defined F(1)\text{F(1)} to be 100N100N. An object 1010 meters away from the same explosion will experience 11 Newton of force as:

F(10)=F(1)102=100N100=1N \text{F(10)} = \dfrac{\text{F(1)}}{10^2} = \dfrac{100N}{100} = 1N

An object that is only 5050 centimeters (0.50.5 meters) away will experience 400400 Newtons of force as:

F(0.5)=F(1)0.52=100N0.25=400N \text{F(0.5)} = \dfrac{\text{F(1)}}{0.5^2} = \dfrac{100N}{0.25} = 400N

Inverse Linear Law

The inverse-square law comes from the fact that, in the real world, the force dissipates in three dimensions. We can think of it as expanding into a sphere centered at the origin of the force.

However, in a 2D space, the force dissipates in only two dimensions - a circle with the origin of the force at the centre. As such, in 2D spaces, falloff follows the inverse linear law.

It is similar to the inverse square law - the only difference is that we do not square the distance:

F(d)=F(1)d \text{F(d)} = \dfrac{\text{F(1)}}{d}

However, this doesn’t necessarily mean we should use it even if we’re making a 2D game. It may be the case that the 3D variation feels more natural or fun for our game, even if it’s not physically accurate for a 2D simulation.

Implementing Falloff

Let’s update our ApplyPositionalForce() function to modify its effects based on the distance between our object and the origin of the force:

// GameObject.h
// ...

class GameObject {
// ...
 private:
  void ApplyPositionalImpulse(
    const Vec2& Origin, float Magnitude
  ) {
    Vec2  Displacement{Position - Origin};
    Vec2  Direction{Displacement.Normalize()};
    float Distance{Displacement.GetLength()};
  
    // Apply inverse-square law with a small
    // offset to prevent extreme forces
    float AdjustedMagnitude{Magnitude /
      (Distance * Distance)};
  
    ApplyImpulse(Direction * AdjustedMagnitude);
  }
  // ...
};

The inverse-linear law is quite often a source of janky physics as, when the distance between an object and a force gets very small, the resulting magnitude of that force becomes extremely large. Additionally, if the distance is ever exactly 00, our division will become problematic.

To solve this, we should intervene in our falloff calculation. In games, we don’t need to be especially accurate, but we do need to be resource-efficient, so a common solution is to add some small number to our distances:

F(d)=F(1)(d+0.1)2 \text{F(d)} = \dfrac{\text{F(1)}}{(d + 0.1)^2}

Let’s update our code to use this:

// GameObject.h
// ...

class GameObject {
// ...
 private:
  void ApplyPositionalImpulse(
    const Vec2& Origin, float Magnitude
  ) {
    Vec2  Displacement{Position - Origin};
    Vec2  Direction{Displacement.Normalize()};
    float Distance{Displacement.GetLength()};
  
    // Apply inverse-square law with a small
    // offset to prevent extreme forces
    float AdjustedMagnitude{Magnitude /
      ((Distance + 0.1f) * (Distance + 0.1f))};
  
    ApplyImpulse(Direction * AdjustedMagnitude);
  }
  // ...
};

Complete Code

Our updated Scene and GameObject classes that include all of the topics we covered are provided below:

#pragma once
#include <SDL.h>
#include <vector>
#include "GameObject.h"

class Scene {
public:
  Scene() {
    Objects.emplace_back(
      "dwarf.png", Vec2{6, 2}, *this);
  }

  Vec2 ToScreenSpace(const Vec2& Pos) const {
    auto [vx, vy, vw, vh]{Viewport};
    float HorizontalScaling{vw / WorldSpaceWidth};
    float VerticalScaling{vh / WorldSpaceHeight};

    return {
      vx + Pos.x * HorizontalScaling,
      vy + (WorldSpaceHeight - Pos.y)
        * VerticalScaling
    };
  }

  Vec2 ToWorldSpace(const Vec2& Pos) const {
    auto [vx, vy, vw, vh]{Viewport};
    float HorizontalScaling{WorldSpaceWidth / vw};
    float VerticalScaling{WorldSpaceHeight / vh};

    return {
      (Pos.x - vx) * HorizontalScaling,
      WorldSpaceHeight - (Pos.y - vy)
        * VerticalScaling
    };
  }

  void HandleEvent(SDL_Event& E) {
    for (GameObject& Object : Objects) {
      Object.HandleEvent(E);
    }
  }

  void Tick(float DeltaTime) {
    for (GameObject& Object : Objects) {
      Object.Tick(DeltaTime);
    }
  }

  void Render(SDL_Surface* Surface) {
    SDL_GetClipRect(Surface, &Viewport);
    for (GameObject& Object : Objects) {
      Object.Render(Surface);
    }
  }

private:
  SDL_Rect Viewport;
  std::vector<GameObject> Objects;
  float WorldSpaceWidth{14}; // meters
  float WorldSpaceHeight{6}; // meters
};
#pragma once
#include <SDL.h>
#include "Vec2.h"
#include "Image.h"

class Scene;

class GameObject {
 public:
  GameObject(
    const std::string& ImagePath,
    const Vec2& InitialPosition,
    const Scene& Scene
  ) : Image{ImagePath},
      Position{InitialPosition},
      Scene{Scene} {}

  void HandleEvent(const SDL_Event& E);

  void Tick(float DeltaTime) {
    ApplyForce(FrictionForce(DeltaTime));
    ApplyForce(DragForce());
    Velocity += Acceleration * DeltaTime;
    Position += Velocity * DeltaTime;

    Acceleration = {0, -9.8};

    // Don't fall through the floor
    if (Position.y < 2) {
      Position.y = 2;
      Velocity.y = 0;
    }
    Clamp(Velocity);
  }

  void Render(SDL_Surface* Surface);

  void ApplyForce(const Vec2& Force) {
    Acceleration += Force / Mass;
  }

 private:
  Image Image;
  const Scene& Scene;

  Vec2 Position{0, 0};
  Vec2 Velocity{0, 0};
  Vec2 Acceleration{0, -9.8};
  float Mass{70};

  float DragCoefficient{0.2};
  Vec2 DragForce() const {
    return -Velocity * DragCoefficient
      * Velocity.GetLength();
  }

  float GetFrictionCoefficient() const {
    if (Position.y > 2) {
      return 0;
    }

    return 0.5;
  }
  Vec2 FrictionForce(float DeltaTime) const {
    float MaxMagnitude{GetFrictionCoefficient()
      * Mass * -Acceleration.y};
    if (MaxMagnitude <= 0) return Vec2(0, 0);

    float StoppingMagnitude{Mass *
      Velocity.GetLength() / DeltaTime};

    return -Velocity.Normalize() * std::min(
      MaxMagnitude, StoppingMagnitude);
  }

  void Clamp(Vec2& V) const {
    V.x = std::abs(V.x) > 0.01 ? V.x : 0;
    V.y = std::abs(V.y) > 0.01 ? V.y : 0;
  }

  void ApplyImpulse(const Vec2& Impulse) {
    Velocity += Impulse / Mass;
  }

  void ApplyPositionalImpulse(
    const Vec2& Origin, float Magnitude
  ) {
    Vec2  Displacement{Position - Origin};
    Vec2  Direction{Displacement.Normalize()};
    float Distance{Displacement.GetLength()};
  
    // Apply inverse-square law with a small
    // offset to prevent extreme forces
    float AdjustedMagnitude{Magnitude /
      ((Distance + 0.1f) * (Distance + 0.1f))};
  
    ApplyImpulse(Direction * AdjustedMagnitude);
  }
};
#include <SDL.h>
#include "GameObject.h"
#include "Scene.h"

#define DRAW_DEBUG_HELPERS

#ifdef DRAW_DEBUG_HELPERS
namespace{
SDL_Surface* Trajectories{
  SDL_CreateRGBSurfaceWithFormat(
    0, 700, 300, 32,
    SDL_PIXELFORMAT_RGBA32
  )};
}
#endif

void GameObject::Render(SDL_Surface* Surface) {
#ifdef DRAW_DEBUG_HELPERS
  auto [x, y] = Scene.ToScreenSpace(Position);
  SDL_Rect PositionIndicator{
    int(x) - 10, int(y) - 10, 20, 20};
  SDL_FillRect(Trajectories, &PositionIndicator,
    SDL_MapRGB(Trajectories->format, 220, 0, 0));
                   
  SDL_BlitSurface(
    Trajectories, nullptr,
    Surface, nullptr);
#endif
  Image.Render(
    Surface, Scene.ToScreenSpace(Position)
  );
}

void GameObject::HandleEvent(const SDL_Event& E) {
  if (E.type == SDL_MOUSEBUTTONDOWN) {
    // Create explosion at click position
    if (E.button.button == SDL_BUTTON_LEFT) {
      ApplyPositionalImpulse(
        Scene.ToWorldSpace({
          static_cast<float>(E.button.x),
          static_cast<float>(E.button.y)
      }),1000);
    }
  } else if (E.type == SDL_KEYDOWN) {
    // Jump
    if (E.key.keysym.sym == SDLK_SPACE) {
      if (Position.y > 2) return;
      ApplyImpulse({0.0f, 300.0f});
    }
  }
}

Summary

This lesson explores implementing momentum and impulses in our physics system, enabling instantaneous forces like jumps and explosions.

We've learned how to calculate the effects of these forces on objects with different masses and at varying distances. Key takeaways:

  • Momentum is the product of mass and velocity, measured in kgm/s\text{kg} \mathclose{\cdot} \text{m/s} or Newton-seconds, Ns\text{N}\mathclose{\cdot}s
  • Impulse represents a change in momentum and equals force multiplied by time
  • Instantaneous impulses apply forces within a single frame without being affected by delta time
  • Positional impulses allow for effects like explosions where force originates from a specific point
  • Force falloff follows the inverse-square law, making objects closer to the origin experience stronger effects
  • Adding a small offset to distance calculations prevents extreme forces when objects are very close to the origin
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Motion and Collisions
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 96 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved