In this lesson, we'll explore fundamental physics principles and how to implement them in our games. We'll explore how to represent position, velocity, and acceleration using our vector system, and how these properties interact over time.
By the end, you'll understand how to create natural movement, predict object trajectories, and integrate player input with physics systems.
We’ll be working with the basic Scene
, GameObject
, and Vec2
types we created previously:
// GameObject.h
#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(SDL_Event& E) {}
void Tick(float DeltaTime) {}
void Render(SDL_Surface* Surface);
Vec2 Position;
private:
Image Image;
const Scene& Scene;
};
#include <SDL.h>
#include "GameObject.h"
#include "Scene.h"
void GameObject::Render(SDL_Surface* Surface) {
Image.Render(Surface, Scene.ToScreenSpace(Position));
}
#pragma once
#include <SDL.h>
#include <vector>
#include "GameObject.h"
class Scene {
public:
Scene() {
Objects.emplace_back("dwarf.png", Vec2{100, 200}, *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
};
}
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{1400};
float WorldSpaceHeight{600};
};
#pragma once
#include <iostream>
struct Vec2 {
float x;
float y;
float GetLength() const {
return std::sqrt(x * x + y * y);
}
float GetDistance(const Vec2& Other) const {
return (*this - Other).GetLength();
}
Vec2 Normalize() const {
return *this / GetLength();
}
Vec2 operator*(float Multiplier) const {
return Vec2{x * Multiplier, y * Multiplier};
}
Vec2 operator/(float Divisor) const {
if (Divisor == 0.0f) { return Vec2{0, 0}; }
return Vec2{x / Divisor, y / Divisor};
}
Vec2& operator*=(float Multiplier) {
x *= Multiplier;
y *= Multiplier;
return *this;
}
Vec2& operator/=(float Divisor) {
if (Divisor == 0.0f) { return *this; }
x /= Divisor;
y /= Divisor;
return *this;
}
Vec2 operator+(const Vec2& Other) const {
return Vec2{x + Other.x, y + Other.y};
}
Vec2 operator-(const Vec2& Other) const {
return *this + (-Other);
}
Vec2& operator+=(const Vec2& Other) {
x += Other.x;
y += Other.y;
return *this;
}
Vec2& operator-=(const Vec2& Other) {
return *this += (-Other);
}
Vec2 operator-() const {
return Vec2{-x, -y};
}
};
inline Vec2 operator*(float M, const Vec2& V) {
return V * M;
}
inline std::ostream& operator<<(
std::ostream& Stream, const Vec2& V) {
Stream << "{ x = " << V.x
<< ", y = " << V.y << " }";
return Stream;
}
Our game loop is provided below within main.cpp
, as well as our supporting Image
and Window
components. We won’t be changing any of these in this chapter:
#include <SDL.h>
#include "Window.h"
#include "Scene.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
Window GameWindow;
Scene GameScene;
Uint64 LastTick{SDL_GetPerformanceCounter()};
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
GameScene.HandleEvent(Event);
if (Event.type == SDL_QUIT) {
SDL_Quit();
return 0;
}
}
Uint64 CurrentTick{SDL_GetPerformanceCounter()};
float DeltaTime{static_cast<float>(
CurrentTick - LastTick) /
SDL_GetPerformanceFrequency()
};
LastTick = CurrentTick;
// Tick
GameScene.Tick(DeltaTime);
// Render
GameWindow.Render();
GameScene.Render(GameWindow.GetSurface());
// Swap
GameWindow.Update();
}
return 0;
}
#pragma once
#include <SDL.h>
#include <SDL_image.h>
#include <string>
class Image {
public:
Image() = default;
Image(const std::string& Path)
: ImageSurface{IMG_Load(Path.c_str())} {
if (!ImageSurface) {
std::cout << "Error creating image: "
<< SDL_GetError();
}
}
void Render(
SDL_Surface* Surface, const Vec2& Pos
) {
SDL_Rect Rect(Pos.x, Pos.y, 0, 0);
SDL_BlitSurface(
ImageSurface, nullptr, Surface, &Rect);
}
// Move constructor
Image(Image&& Other) noexcept
: ImageSurface(Other.ImageSurface) {
Other.ImageSurface = nullptr;
}
~Image() {
if (ImageSurface) {
SDL_FreeSurface(ImageSurface);
}
}
// Prevent copying
Image(const Image&) = delete;
Image& operator=(const Image&) = delete;
private:
SDL_Surface* ImageSurface{nullptr};
};
#pragma once
#include <iostream>
#include <SDL.h>
class Window {
public:
Window() {
SDLWindow = SDL_CreateWindow(
"Scene",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
700, 300, 0
);
}
~Window() {
if (SDLWindow) {
SDL_DestroyWindow(SDLWindow);
}
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
void Render() {
SDL_FillRect(
GetSurface(), nullptr,
SDL_MapRGB(GetSurface()->format,
220, 220, 220));
}
void Update() {
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() {
return SDL_GetWindowSurface(SDLWindow);
}
private:
SDL_Window* SDLWindow;
};
We want our physics calculations to be done in world space. We’ll use the same world space definition we created in the previous section, where the origin is at the bottom left, increasing x
values corresponds to rightward movement, whilst increasing y
values corresponds to upward movement. Initially, all of our objects are in the range to :
To render our objects, our world space positions are converted to screen space within the Scene
class. This is the same class we used in the previous section but, to keep things simple, we’ve removed the camera and are just doing a direct world space to screen space conversion:
// Scene.h
// ...
class Scene {
public:
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
};
}
// ...
private:
SDL_Rect Viewport;
float WorldSpaceWidth{1400};
float WorldSpaceHeight{600};
// ...
};
Previously, we’ve seen how we can represent the position of an object in a 2D space by using a 2D vector:
// GameObject.h
// ...
class GameObject {
public:
// ...
Vec2 Position;
// ...
};
The initial positions of our GameObject
instances are set through the constructor:
// GameObject.h
// ...
class GameObject {
public:
GameObject(
const std::string& ImagePath,
const Vec2& InitialPosition,
const Scene& Scene
) : Image{ImagePath},
Position{InitialPosition},
Scene{Scene} {}
// ...
};
Our Scene
object is setting these initial positions when they construct our GameObject
s:
// Scene.h
// ...
class Scene {
public:
Scene() {
Objects.emplace_back("dwarf.png", Vec2{300, 400}, *this);
}
// ...
};
Specifically, this is the object’s position relative to the origin of our space. This is sometimes referred to as its displacement.
When working with physics systems, it can be helpful to assign some real-world unit of measurement to this displacement.
For example, the Unity game engine uses meters, whilst Unreal Engine uses centimeters. We’re free to choose whichever we want, but it’s important to be consistent across all our objects. For these examples, we’ll use meters as our unit of distance.
This choice directly impacts how we would define our world space. In the previous section, our world space ranged from to .
If that represents meters, our world space is much larger than we need. Let’s shrink it down to a width of and height of :
// Scene.h
// ...
class Scene {
// ...
private:
float WorldSpaceWidth{14}; // meters
float WorldSpaceHeight{6}; // meters
};
Naturally, any object positioned in this space should adopt the same units of measurement. We’ll update our dwarf’s initial position to be :
// Scene.h
// ...
class Scene {
public:
Scene() {
Objects.emplace_back("dwarf.png", Vec2{3, 4}, *this);
}
// ...
};
A position vector of {3, 4}
represents the object being displaced 3 meters horizontally from the origin, and 4 meters vertically. In total, the object is 5 meters from the origin, as
Remember, in our simple 2D rendering setup, an object’s position represents the top left corner of its image. We could change that if we want, but we’ll keep it simple for now. To visualize our object’s motion, our screenshots include a red rectangle showing the object’s Position
on each frame:
When our code contains literal values that represent units of measurement, it can be unclear what units are being used. Our previous example added comments to clarify that our values represent meters:
float WorldSpaceWidth{14}; // meters
float WorldSpaceHeight{6}; // meters
However, we may want to improve this, and it is a textbook example of where we’d consider applying user-defined literals. These let us state within our code exactly what our values represent:
float WorldSpaceWidth{14_meters};
float WorldSpaceHeight{6_meters};
Beyond documentation, user-defined literals can also unlock some additional capabilities. For example, if we wanted to provide a value in some other unit (such as kilometers or inches) and have it converted to our standard unit of measurement (meters) behind the scenes, they give us a succinct way to do that:
float WorldSpaceWidth{0.14_kilometers};
float WorldSpaceHeight{236_inches};
We cover this concept in much more detail in the advanced course, but the quick syntax to make our previous examples possible looks like the following. We’d add these functions to a header file, and then #include
it wherever the literals are needed:
// DistanceLiterals.h
#pragma once
float operator""_meters(long double D){
return D;
}
float operator""_inches(long double D){
return D / 39.37;
}
float operator""_kilometers(long double D){
return D * 1000;
}
Velocity is the core idea that allows our objects to move. That is, a velocity represents a change in an object’s displacement over some unit of time. We’ve chosen meters, , as our unit of position, and we’ll commonly use seconds, , as our unit of time. As such, velocity will be measured in meters per second or
As movement has a direction, our velocity will also have a direction, so we’ll use our Vec2
type:
// GameObject.h
// ...
class GameObject {
public:
// ...
Vec2 Velocity{1, 0};
// ...
};
For example, if an object’s velocity is {x = 1, y = 0}
, our object is moving meter to the right every second.
We can implement this behavior in our Tick()
function. Our velocity represents how much we want our displacement to change every second, but our Tick()
function isn’t called every second. Therefore, we need to scale our change in velocity by how much time has passed since the previous tick.
We can do this by multiplying our change by our tick function’s DeltaTime
argument:
// GameObject.h
// ...
class GameObject {
public:
// ...
void Tick(float DeltaTime) {
Position += Velocity * DeltaTime;
}
// ...
};
Our application should be ticking multiple times per second, so our DeltaTime
argument will be a floating point number between 0
and 1
. Multiplying a vector by this value will have the effect of reducing its magnitude but, after one second of tick function invocations, we should see our displacement change by approximately the correct amount.
We can temporarily add a Time
variable and some logging to confirm this:
// GameObject.h
// ...
class GameObject {
public:
// ...
float Time{0};
void Tick(float DeltaTime) {
Position += Velocity * DeltaTime;
Time += DeltaTime;
std::cout << "\nt = " << Time
<< ", Position = " << Position;
}
// ...
};
t = 0.0223181, Position = { x = 3.02232, y = 4 }
t = 0.0360435, Position = { x = 3.03604, y = 4 }
...
t = 0.9969611, Position = { x = 3.99696, y = 4 }
t = 0.9983365, Position = { x = 3.99833, y = 4 }
t = 1.0000551, Position = { x = 4.00005, y = 4 }
By convention, the concept of "change" in maths and physics is typically represented by the Greek letter delta, which looks like in uppercase or in lowercase. For example, the naming of the TimeDelta
parameter denotes that this variable represents a change in time.
Velocity is a change in position, so the delta naming convention can show up here, too. It’s somewhat common to see velocity-related variables use names prefixed with d
. For example, if x
and y
represent positions, variables representing velocity (ie, changes in these positions) will often be called dx
and dy
:
Vec2 Velocity{1, 2};
auto[dx, dy]{Velocity};
The concepts of speed and velocity are closely related. The key difference is that velocity includes the concept of direction, whilst speed does not. Accordingly, velocity requires a more complex vector type, whilst speed is a simple scalar, like a float
or an int
.
To calculate the speed associated with a velocity, we simply calculate the length (or magnitude) of the velocity vector. So, in mathematical notation, .
Below, we calculate our speed based on the object’s Velocity
variable. We also do some vector arithmetic to confirm our simulation really is moving our object at that speed:
// GameObject.h
// ...
class GameObject {
public:
// ...
void Tick(float DeltaTime) {
Vec2 PreviousPosition{Position};
Position += Velocity * DeltaTime;
std::cout << "\nIntended Speed = "
<< Velocity.GetLength()
<< ", Actual Speed = "
<< Position.GetDistance(PreviousPosition)
/ DeltaTime;
}
//...
};
Intended Speed = 1, Actual Speed = 1.00005
Intended Speed = 1, Actual Speed = 0.999974
Intended Speed = 1, Actual Speed = 1.00009
...
It’s often required to perform this calculation in the opposite direction, where we want to travel at a specific speed in a given direction. Rather than defining a Velocity
member for our objects, we could recreate it on-demand by multiplying a direction vector by a movement speed scalar:
// GameObject.h
// ...
class GameObject {
public:
// ...
// Now calculated within Tick()
Vec2 Velocity{1, 0};
Vec2 Direction{1, 0};
float MaximumSpeed{1}; // meters per second
void Tick(float DeltaTime) {
Vec2 Velocity{
Direction.Normalize() * MaximumSpeed};
Position += Velocity * DeltaTime;
Time += DeltaTime;
std::cout << "\nt = " << Time
<< ", Position = " << Position;
}
};
t = 0.0180497, Position = { x = 3.01805, y = 4 }
t = 0.0251397, Position = { x = 3.02514, y = 4 }
...
t = 0.997757, Position = { x = 3.99776, y = 4 }
t = 0.999264, Position = { x = 3.99926, y = 4 }
t = 1.00025, Position = { x = 4.00025, y = 4 }
We covered how to use vectors to define movement in more detail earlier in the course:
Just as velocity is the change in displacement over time, acceleration is the change in velocity over time. Therefore, let’s figure out what the units of acceleration should be:
Acceleration also has a direction, so is typically represented by a vector:
// GameObject.h
// ...
class GameObject {
public:
// ...
// Accelerate one meter per second per
// second to the left
Vec2 Acceleration{-1, 0};
// ...
};
Much like how we used the velocity to update an object’s displacement on tick, we can use acceleration to update an object’s velocity:
// GameObject.h
// ...
class GameObject {
public:
// ...
// meters per second
Vec2 Velocity{0, 0};
// meters per second per second
Vec2 Acceleration{0, 1};
void Tick(float DeltaTime) {
Velocity += Acceleration * DeltaTime;
// Unchanged
Position += Velocity * DeltaTime;
}
// ...
};
Acceleration doesn’t necessarily cause an object’s speed to increase. If the direction of acceleration is generally in the opposite direction to the object’s current velocity, that acceleration will cause the speed to reduce.
This is sometimes called "deceleration", but the mechanics are identical. In the following code example, our object has an initial velocity of , and constant acceleration of :
// GameObject.h
// ...
class GameObject {
public:
// ...
// meters per second
Vec2 Velocity{1, 0};
// meters per second per second
Vec2 Acceleration{-1, 0};
void Tick(float DeltaTime) {
Velocity += Acceleration * DeltaTime;
// Unchanged
Position += Velocity * DeltaTime;
Time += DeltaTime;
std::cout << "\nt = " << Time
<< ", x = " << Position.x
<< ", dx = " << Velocity.x;
}
// ...
};
The initial position of , initial velocity of , and constant acceleration of has the following effects:
t = 0.0179667, x = 3.01764, dx = 0.982033
t = 0.0358256, x = 3.03486, dx = 0.964174
...
t = 0.999145, x = 3.49913, dx = 0.000855972
t = 1.00022, x = 3.49913, dx = -0.000214628
...
t = 1.99998, x = 2.99812, dx = -0.999977
t = 2.0007, x = 2.99739, dx = -1.0007
...
t = 2.99909, x = 1.49116, dx = -1.99909
t = 3.00025, x = 1.48884, dx = -2.00025
The most common accelerative force we simulate is gravity - the force that causes objects to fall to the ground. On Earth, the acceleration caused by gravity is approximately towards the ground. As such, an object falling with this acceleration will feel realistic:
// GameObject.h
// ...
class GameObject {
public:
// ...
Vec2 Acceleration{0, -9.8};
// ...
};
Later in the chapter, we’ll see how we can stop an object from falling once it hits the ground, or any other surface.
A trajectory is the path that an object takes through space, given its initial velocity and the accelerative forces acting on it. The most common trajectory we’re likely to recognize is projectile motion. For example, when we throw an object, we apply some initial velocity to it and, whilst it flies through the air, gravity is constantly accelerating it towards the ground.
This causes the object to follow a familiar, arcing trajectory:
// GameObject.h
// ...
class GameObject {
public:
// ...
Vec2 Velocity{5, 5};
Vec2 Acceleration{0, -9.8};
// ...
};
In the previous sections, we saw that acceleration updates velocity, and velocity updates displacement. It’s worth spending a moment reflecting on the implications of these relationships, as they’re not immediately intuitive.
For example, imagine we have an acceleration that is pointing upwards (in the positive direction) with a magnitude that is not changing over time. On a chart, our acceleration value would be represented as a straight line:
However, as long as the acceleration is not zero, it is continuously changing our velocity value over time. So, a constant acceleration results in a linear change in velocity:
But velocity is also changing our displacement over that time. If our velocity is constantly increasing, that means our displacement is not only changing over time, but the rate at which it is changing is also increasing. On our chart, the exponential increase looks like this:
In the next lesson, we’ll introduce concepts like drag and friction, which counteract these effects and ensure that the effect of acceleration doesn’t result in velocities and displacements that quickly scale towards unmanageably huge numbers.
When making games, our physics simulations typically need to interact in a compelling way with user input. For example, if our player tries to move their character to the right, we need to intervene in our physics simulation to make that happen.
There is no universally correct way of doing that - the best approach just depends on the type of game we’re making, and what feels most realistic or fun when we’re playing.
A common approach is to have player input directly influence an object’s velocity or acceleration, and then have the physics system incorporate that change into its simulation.
This is usually easy to set up as, within each frame, player input is processed before the physics simulation updates. In our context, that means that HandleEvent()
happens before Tick()
. Below, we let the player set the horizontal component of their character’s velocity before the physics simulation ticks:
// GameObject.h
// ...
class GameObject {
public:
// ...
void HandleEvent(SDL_Event& E) {
if (E.type == SDL_KEYDOWN) {
switch (E.key.keysym.sym) {
case SDLK_LEFT:
Velocity.x = -5;
break;
case SDLK_RIGHT:
Velocity.x = 5;
break;
}
}
}
// ...
};
Later in this chapter, we’ll expand this to include collisions in our physics simulations, which will stop the player from running through walls or falling through the floor.
Naturally, a lot of academic effort has been made investigating and analyzing the physical world. We can apply those learnings to our own projects when we need to simulate natural processes, such as the motion of objects.
Some of the most useful examples are the SUVAT equations. In scenarios where our acceleration isn’t changing, the SUVAT equations establish relationships between 5 variables:
The equations are as follows:
It’s typically the case that the second equation (or rearrangements of the second equation) are most useful. For example, if we know an object’s current position, current velocity, and acceleration, we can predict where it will be in the future.
#include <iostream>
int main() {
float u{1}; // velocity
float a{1}; // acceleration
float t{10}; // time
// displacement
float s = u * t + 0.5 * a * t * t;
std::cout << "In " << t << " seconds I "
"predict I will have moved "
<< s << " meters";
float CurrentDisplacement{3};
std::cout << "\nMy new displacement would be "
<< CurrentDisplacement + s << " meters";
}
In 10 seconds I predict I will have moved 60 meters
My new displacement would be 63 meters
How accurate the prediction will be depends on how much the acceleration changes over the time interval. If the acceleration doesn’t change at all, our prediction will be exactly correct.
Our previous equations and examples were using scalars for displacements, velocities, and acceleration. This is fine if we only care about motion in a straight line, but what about motion in a 2D or 3D space?
Thanks to the magic of vectors, we can drop them straight into the same equations:
#include <iostream>
#include "Vec2.h"
int main() {
Vec2 u{1, 2}; // 2D velocity
Vec2 a{1, 2}; // 2D acceleration
float t{10}; // time
// 2D displacement
Vec2 s = u * t + 0.5 * a * t * t;
std::cout << "In " << t << " seconds I "
"predict I will have moved "
<< s << " meters";
std::cout << "\nThis is " << s.GetLength()
<< " meters in total";
Vec2 CurrentDisplacement{3, 5};
std::cout << "\nMy new displacement would be "
<< CurrentDisplacement + s << " meters";
}
In 10 seconds I predict I will have moved { x = 60, y = 120 } meters
This is 134.164 meters in total
My new displacement would be { x = 63, y = 125 } meters
The equivalent to all of the SUVAT equations using vectors is below.
There are two things to note from the vector form of these equations.
Firstly, the time variable () is a scalar, whilst all other variables are vectors. It’s common in mathematical notation to make vector variables bold
Secondly, has been slightly modified from its scalar counterpart . This is to remove some ambiguity associated with vector multiplication.
The equations are equivalent, but it may not be entirely clear what expressions like , , and mean when , , , and are vectors.
A vector product is the result of multiplying two vectors together. Scalar multiplication, as in , only has a single definition, but there are two different forms of vector multiplication:
We won’t need vector multiplication in this course, and the cross product isn’t defined for 2D vectors anyway. However, we’ll briefly introduce the dot product here.
The dot product of two vectors is calculated by multiplying their corresponding components together, and then adding those results together.
For example, the dot product of the vectors and is :
Generalizing to any pair of 2D vectors, the equation looks like this:
We could add a Dot()
method to our Vec2
type like this:
// Vec2.h
// ...
struct Vec2 {
// ...
float Dot(const Vec2& Other) const {
return (x * Other.x) + (y * Other.y);
}
};
// ...
We won’t need the dot product in this course, but it has useful properties that are widely used to solve problems in computer graphics and game simulations. For example, the dot product can tell us the extent to which a vector is "facing" another vector:
We could use this as part of a calculation to determine whether a character is in some other character’s line of sight:
// GameObject.h
// ...
class GameObject {
public:
// ...
// Where am I?
Vec2 Position;
// Which direction am I facing?
Vec2 Direction{1, 0};
bool CanSee(const GameObject& Target) const {
// The vector from me to the target
Vec2 ToTarget{Target.Position - Position};
return
// Target is close to me...
ToTarget.GetLength() < 10
// ...and target is in front of me
&& Direction.Dot(ToTarget) > 0;
}
// ...
};
Here are some more properties of the dot product:
A more detailed introduction to the dot product of two vectors is available at mathisfun.com.
Physics is the backbone of realistic movement in games. In this lesson, we've explored how to implement fundamental motion physics in C++ using SDL, transforming static objects into dynamic entities that behave naturally. Key takeaways:
Create realistic object movement by applying fundamental physics concepts
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games