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};
// ....
};
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:
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, ) multiplied by a unit of velocity (often meters per second, ). When units are multiplied together, it’s common to represent that multiplication using a center dot
For example:
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:
The unit used to measure momentum (and changes in momentum) is often referred to by an alternative, equivalent unit: the Newton-second, . For example:
Previously, we saw how a Newton is the amount of force required to accelerate a object by .
If we apply this acceleration for one second, the object’s velocity will change by . And, given the object has of mass, its momentum will change by . Therefore, . Here are some more examples:
In academic papers and learning resources, the variable momentum is often represented by the variable , and the greek letter Delta, or it’s lowercase form , is used to represent change.
Therefore, impulse (change in momentum) is often written as and its equation is .
In this equation, is the force, and is the duration of time over which the force is applied - that is, the change between the starting time and ending time.
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;
}
// ...
};
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});
}
}
}
// ...
};
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};
// ...
};
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);
}
// ...
};
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.
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 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:
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 of force. Let’s represent that as .
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 represents the distance between the explosion and the object that’s reacting to it, and is the magnitude that would be experienced by an object one meter away:
Let’s revisit our hypothetical explosion, where we defined to be . An object meters away from the same explosion will experience Newton of force as:
An object that is only centimeters ( meters) away will experience Newtons of force as:
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:
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.
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 , 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:
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);
}
// ...
};
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});
}
}
}
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:
Add explosions and jumping to your game by mastering momentum-based impulse forces
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games