Momentum and Impulse Forces

Add explosions and jumping to your game by mastering momentum-based impulse forces

Ryan McCombe
Updated

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.

Starting Point

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 we introduced previously.

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.

src/GameObject.h

#pragma once
#include <SDL3/SDL.h>
#include <algorithm>
#include "Vec2.h"
#include "Image.h"
#include "Config.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(SDL_Event& E) {}

  void Tick(float DeltaTime) {
    ApplyForce(GetFrictionForce(DeltaTime));
    ApplyForce(GetDragForce());
    Velocity += Acceleration * DeltaTime;
    Position += Velocity * DeltaTime;
    Acceleration = {0, 9.8f * PIXELS_PER_METER};
    Clamp(Velocity);

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

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

  void Render(SDL_Surface* Surface);

 private:
  float DragCoefficient{0.2f};
  Vec2 GetDragForce() const {
    return -Velocity * Velocity.GetLength()
      * DragCoefficient;
  }

  float GetFrictionCoefficient() const {
    if (Position.y < 200) {
      // Object isn't on the ground
      return 0;
    }
    return 0.5f;
  }

  Vec2 GetFrictionForce(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.1f ? V.x : 0.0f;
    V.y = std::abs(V.y) > 0.1f ? V.y : 0.0f;
  }

  Vec2 Acceleration{0, 9.8f * PIXELS_PER_METER};
  float Mass{50.0f};
  Vec2 Position{0, 0};
  Vec2 Velocity{0, 0};
  Image Image;
  const Scene& Scene;
};

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 game 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

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 of our simulation.

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

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:

src/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.

In our physics simulation, we're using a scale where 5050 pixels equals 11 meter, and our character has a mass of 5050. To make the character jump upwards (negative Y) with a reasonable speed (e.g., 6 meters/second), we need to calculate the required impulse.

Using J=Δv×mJ = \Delta v \times m, if we want Δv\Delta v to be 300-300 pixels/sec (6 meters/sec upwards), and mm is 5050, then the impulse required is 15000-15000.

src/GameObject.h

// ...

class GameObject {
 public:
  // ...
  void HandleEvent(SDL_Event& E) {
    if (E.type == SDL_EVENT_KEY_DOWN) {
      if (E.key.key == SDLK_SPACE) {
        // Apply upward impulse
        ApplyImpulse({0.0f, -15000.0f});
      }
    }
  }
  // ...
};

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:

src/GameObject.h

// ...

class GameObject {
public:
  // ...
  void Tick(float DeltaTime) {
    ApplyForce(GetFrictionForce(DeltaTime));
    ApplyForce(GetDragForce());

    // Apply the timed force if active
    if (ForceTimeRemaining > 0) {
      ApplyForce({10000, 0});
      ForceTimeRemaining -= DeltaTime;
    }

    Velocity += Acceleration * DeltaTime;
    Position += Velocity * DeltaTime;

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

    // Don't fall through the floor
    if (Position.y > 200) {
      Position.y = 200;
      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 regardless of their position. 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.

We can set that up by calculating the direction from the origin of the force to the position of our object, normalizing that vector so it has a magnitude of 11, and then multiplying it by the magnitude of the force we're applying:

src/GameObject.h

// ...

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

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.

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

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 the mouse coordinates to that function, alongside a magnitude that feels right. We'll use 100000 units of impulse, which is roughly enough to knock our character back at a speed of 40 meters/second if they are 1 meter away.

The following example applies the full effect of the force regardless of how far away it is from our object, but we'll implement distance falloff in the next section:

src/GameObject.cpp

#include <SDL3/SDL.h>
#include "GameObject.h"
#include "Scene.h"

void GameObject::Render(SDL_Surface* Surface) {
  Image.Render(Surface, Position);
}

void GameObject::HandleEvent(SDL_Event& E) {
  if (E.type == SDL_EVENT_MOUSE_BUTTON_DOWN) {
    if (E.button.button == SDL_BUTTON_LEFT) {
      ApplyPositionalImpulse(
        Vec2{E.button.x, E.button.y},
        10000.0f
      );
    }
  } else if (E.type == SDL_EVENT_KEY_DOWN) {
    if (E.key.key == SDLK_SPACE) {
      if (Position.y > 200) return;
      ApplyImpulse({0.0f, -15000.0f});
    }
  }
}

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

Note that the Position of our object currently represents the top left corner of its rendered image. We can change where our image is rendered relative to this position by updating the Render() function of our Image class. For example, the following moves our image up and to the left, such that the Position of our object is at the center of the image:

src/Image.h

// ...

class Image {
 public:
  // ...

  void Render(
    SDL_Surface* Surface, const Vec2& Pos
  ) {
    if (ImageSurface) {
      SDL_Rect Rect{
        int(Pos.x),
        int(Pos.x) - ImageSurface->w / 2,
        int(Pos.y)
        int(Pos.y) - ImageSurface->h / 2,
        ImageSurface->w, ImageSurface->h
      };
      SDL_BlitSurface(
        ImageSurface, nullptr, Surface, &Rect);
    }
  }

  // ...
};

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 11 meter away experience 100N100N of force. We'll 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

Minimum Distance

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.

F(0.001)=F(1)0.0012=100N0.000001=100,000,000N \text{F(0.001)} = \dfrac{\text{F(1)}}{0.001^2} = \dfrac{100N}{0.000001} = 100{\small,}000{\small,}000N

If you've ever seen physics bugs where an object shoots off the screen at incredibly high speed, this division by a very small number was likely the cause.

To solve this, we intervene in our falloff calculation. A common solution is to add some small number to our distances - for example, 0.10.1.

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

Game simulations don't need to be entirely accurate - they just need to look right, and this tiny change isn't noticible when our objects are a normal distance apart. However, it makes a big difference when they're unrealistically close together:

F(0.001)=F(1)(0.001+0.1)2100N0.01=10,000N \text{F(0.001)} = \dfrac{\text{F(1)}}{(0.001 + 0.1)^2} \approx \dfrac{100N}{0.01} = 10{\small,}000N

Implementing Falloff

Let's update our ApplyPositionalImpulse() function to modify its effects based on the distance between our object and the origin of the force.

To make the physics calculations intuitive, we'll convert our distance from pixels to meters using our PIXELS_PER_METER constant before applying the inverse-square law. This ensures that "one meter away" actually means 50 pixels away in our game world.

src/GameObject.h

// ...

class GameObject {
// ...
 private:
  void ApplyPositionalImpulse(
    const Vec2& Origin, float Magnitude
  ) {
    Vec2  Displacement{Position - Origin};
    Vec2  Direction{Displacement.Normalize()};
    float DistancePixels{Displacement.GetLength()};
    float DistanceMeters{DistancePixels / PIXELS_PER_METER};

    // Apply inverse-square law with a small
    // offset to prevent extreme forces
    float AdjustedMagnitude{Magnitude /
      ((DistanceMeters + 0.1f) * (DistanceMeters + 0.1f))};

    ApplyImpulse(Direction * AdjustedMagnitude);
  }
  // ...
};

Complete Code

Our updated Scene, GameObject header, and GameObject source files are provided below.

The GameObject.cpp file also includes the code we used to draw trajectories in our screenshots for reference, but we'll walk through debug drawing in more detail later in the course.

Files

src
Select a file to view its content

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.
Have a question about this lesson?
Answers are generated by AI models and may not be accurate