Physical Motion

Create realistic object movement by applying fundamental physics concepts
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 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 (0,0)(0, 0) to (1400,600)(1400, 600):

Diagram showing an example world space that we'll use in this lesson

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};
  // ...
};

Position and Displacement

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 GameObjects:

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

Units

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 (0,0)(0, 0) to (1400,600)(1400, 600).

If that represents meters, our world space is much larger than we need. Let’s shrink it down to a width of 14m14m and height of 6m6m:

// 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 (3,4)(3, 4):

// 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 32+42=5\sqrt{3^2 + 4^2} = 5

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:

Screenshot of our scene in its initial state

Advanced: User-Defined Literals

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

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, mm, as our unit of position, and we’ll commonly use seconds, ss, as our unit of time. As such, velocity will be measured in meters per second or m/sm/s

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 11 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;
  }
  // ...
};
Screenshot of our scene showing the effect of velocity

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 }

Delta

By convention, the concept of "change" in maths and physics is typically represented by the Greek letter delta, which looks like Δ\Delta in uppercase or δ\delta 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};

Speed

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, Speed=Velocity\text{Speed} = \lvert \text{Velocity} \rvert.

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:

Acceleration

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:

  • Displacement is measured in meters: mm
  • Velocity, as the change in displacement over time, is measured in meters per second: m/sm/s
  • Acceleration, as the change in velocity over time, is measured in meters per second per second: (m/s)/s(m/s)/s. This is equivalent to m/(s×s)m/(s \times s), usually written as m/s2m/s^2

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;

  }
  // ...
};

Deceleration

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 (1,0)(1, 0), and constant acceleration of (1,0)(-1, 0):

// 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 (3,0)(3, 0), initial velocity of (1,0)(1, 0), and constant acceleration of (1,0)(-1, 0) has the following effects:

  • Because of the initial velocity, our object is initially moving right (in the positive xx direction). However, its acceleration is pointing to the left, thereby reducing that rightward velocity over time.
  • After one second (t=1t = 1) the acceleration has reduced the velocity to (0,0)(0, 0) and the object momentarily stops moving.
  • From then on, the acceleration and velocity are both pointing left, so the object’s velocity starts to increase again, but now in the opposite direction to its initial velocity. By t=2t=2, the object’s velocity has become (1,0)(-1, 0) and its position is back where it started (x=3x = 3)
  • If the acceleration doesn’t change, the object will continue to move left at ever-increasing speed.
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

Gravity

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 9.8m/s29.8m/s^2 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.

Trajectories

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};
  // ...
};
Screenshot of our scene showing a falling trajectory

The Relationship Between Displacement, Velocity, and Acceleration

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 yy direction) with a magnitude that is not changing over time. On a chart, our acceleration value would be represented as a straight line:

A chart showing constant acceleration

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:

A chart showing constant acceleration and linear 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:

A chart showing constant acceleration, linear velocity, and exponential displacement

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.

Combining Player Input with Physics

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;
      }
    }
  }
  // ...
};
A screenshot of our scene combining physics with player input

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.

Equations of Motion and Motion Prediction

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:

  • ss is displacement
  • uu is the initial velocity
  • vv is the final velocity
  • aa is acceleration
  • tt is time

The equations are as follows:

v=u+ats=ut+12at2s=12(u+v)tv2=u2+2ass=vt12at2 \begin{align} v &= u + at \\ s &= ut + \dfrac{1}{2}at^2 \\ s &= \dfrac{1}{2}(u + v)t \\ v^2 &= u^2 + 2as \\ s &= vt - \dfrac{1}{2}at^2 \end{align}

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.

Using Vectors

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.

v=u+ats=ut+12at2s=12(u+v)tv2=u2+2ass=vt12at2 \begin{align} \bold{v} &= \bold{u} + \bold{a}t \\ \bold{s} &= \bold{u}t + \dfrac{1}{2}\bold{a}t^2 \\ \bold{s} &= \dfrac{1}{2}(\bold{u} + \bold{v})t \\ \lvert \bold{v} \rvert ^2 &= \lvert \bold{u} \rvert ^2 + 2\bold{a} \cdot \bold{s} \\ \bold{s} &= \bold{v}t - \dfrac{1}{2}\bold{a}t^2 \end{align}

There are two things to note from the vector form of these equations.

Firstly, the time variable (tt) is a scalar, whilst all other variables are vectors. It’s common in mathematical notation to make vector variables bold

Secondly, v2=u2+2as\lvert \bold{v} \rvert ^2 = \lvert \bold{u} \rvert ^2 + 2\bold{a} \cdot \bold{s} has been slightly modified from its scalar counterpart v2=u2+2asv^2 = u^2 + 2as. This is to remove some ambiguity associated with vector multiplication.

The equations are equivalent, but it may not be entirely clear what expressions like v2\bold{v}^2, u2\bold{u}^2, and 2as2\bold{as} mean when v\bold{v}, u\bold{u}, a\bold{a}, and s\bold{s} are vectors.

Vector Multiplication and Dot Product

A vector product is the result of multiplying two vectors together. Scalar multiplication, as in 2×32 \times 3, only has a single definition, but there are two different forms of vector multiplication:

  • The dot product, which uses the \cdot symbol, as in AB\bold{A} \cdot \bold{B}
  • The cross product, which uses the ×\times symbol, as in A×B\bold{A} \times \bold{B}.

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 (1,2)(1, 2) and (3,4)(3, 4) is 1111:

(1,2)(3,4)=(1×3)+(2×4)=3+8=11 \begin{aligned} (1, 2) \cdot (3, 4) &= (1 \times 3) + (2 \times 4) \\ &= 3 + 8 \\ &= 11 \end{aligned}

Generalizing to any pair of 2D vectors, the equation looks like this:

AB=(AxBx)+(AyBy) \bold{A} \cdot \bold{B} = (\bold{A}_x \bold{B}_x) + (\bold{A}_y \bold{B}_y)

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:

  • If a direction vector is facing toward a position vector, their dot product will be positive
  • If a direction vector is facing away from a position vector, their dot product will be negative
  • If the vectors are exactly perpendicular, their dot product will be 0
A chart showing examples of the dot product

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:

  • If we normalize our two vectors (that is, set their magnitude 11), then their dot product will be in the range 1-1 to 11. This gives us an easy way to determine the exact extent to which two vectors point in the same direction - from 1-1 (directly opposite) to 11 (exact same direction)
  • The dot product of a vector with itself, as in vv\bold{v} \cdot \bold{v}, is equal to the square of the magnitude of v\bold{v}. Therefore, vv\bold{v} \cdot \bold{v} and v2\lvert \bold{v} \rvert ^2 are equivalent.
  • vv\bold{v} \cdot \bold{v} and v2\lvert \bold{v} \rvert ^2 is sometimes written as v2\bold{v}^2, but this is slightly ambiguous as, in some contexts, v2\bold{v}^2 is also used to represent the cross product v×v\bold{v} \times \bold{v}.

A more detailed introduction to the dot product of two vectors is available at mathisfun.com.

Summary

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:

  • Position and displacement are represented as 2D vectors that define an object's location in world space
  • Velocity is the rate of change in position over time, measured in meters per second
  • Acceleration changes velocity over time and is measured in meters per second squared
  • The relationship between displacement, velocity, and acceleration creates realistic motion
  • The SUVAT equations allow us to predict object movement given initial conditions
  • Vector mathematics enables us to represent and calculate motion in multiple dimensions
  • Physics can be combined with player input to create responsive game controls
  • Consistent units (like meters and seconds) are crucial for accurate physics simulations
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