Cameras and View Space

Create camera systems that follow characters, respond to player input, and properly frame your game scenes.
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 most games, the player cannot view the entire game world at the same time. Rather, we can imagine the player moving a virtual camera around the world and what that camera sees determines what appears on the player’s screen.

In a top-down game, for example, they might move that camera by clicking and dragging their mouse. In an action game, the camera is attached to the player they’re controlling. As such, we need to show the world from the perspective of that camera. This view is another example of a space, commonly called the view space or camera space.

In this lesson, we'll explore how view space works, we’ll implement camera movement controls, and create character-following mechanics.

We’ll be using the Vec2 struct we created earlier. A complete version of it is available below:

#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;
}

We’ll also be building upon the scene rendering architecture we created in the previous lesson:

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

class Scene {
public:
  Scene() {
    Objects.emplace_back("dwarf.png", Vec2{100, 200}, *this);  
    Objects.emplace_back("dragon.png", Vec2{800, 500}, *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() {
    for (GameObject& Object : Objects) {
      Object.Tick();
    }
  }

  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 <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() {}
  void Render(SDL_Surface* Surface);
  Vec2 Position;

 private:
  Image Image;
  const Scene& Scene;
};
#include <SDL.h>
#include "GameObject.h"
#include "Scene.h"

// #define DRAW_DEBUG_HELPERS

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

View Space

Let’s add a virtual camera to our Scene class. In the minimalist, two-dimensional case, the only property our camera really needs is its position in world space, so our camera can be represented by a simple Vec2:

class Scene {
  // ...
private:
  Vec2 CameraPosition{0, 0}; // World Space
  // ...
}

Somewhat counterintuitively, to create the illusion of a camera moving through our world, it’s more helpful to imagine that the world is instead moving around a stationary camera.

For example, if we wanted to create the illusion of our virtual camera moving to the left, we instead need to move everything in the world to the right:

class Scene {
  // ...
private:
  Vec2 CameraPosition{0, 0}; // World Space
  
  Vec2 ToViewSpace(const Vec2& Pos) const {
    return {
      Pos.x - CameraPosition.x,
      Pos.y - CameraPosition.y
    };
  }
  // ...
}

Vec2 Operators

Our code examples are showing the operations we perform on each component of our position vectors for clarity, but our Vec2 type includes a range of operators that can make our code more succinct.

For example, our ToViewSpace() function can be rewritten (or replaced entirely) with the - operator:

class Scene {
  // ...
private:
  Vec2 CameraPosition{0, 0}; // World Space
  
  Vec2 ToViewSpace(const Vec2& Pos) const {
    // Before
    return {
      Pos.x - CameraPosition.x,
      Pos.y - CameraPosition.y
    };
    
    // After
    return Pos - CameraPosition;
  }
  // ...
}

Let’s update our transformation pipeline to use our ToViewSpace() function. We need to transform our points from world space to view space based on the camera, and then from view space to screen space based on the viewport properties.

Because we’re defining our camera’s position in world space, it’s important that we perform the view space transformations before we convert our vectors to their screen space representations:

// Scene.h
// ...

class Scene {
public:
  Vec2 ToScreenSpace(const Vec2& Pos) const {
    // Transform to view space...
    auto [x, y]{ToViewSpace(Pos)};
    
    // ...then to screen space
    auto [vx, vy, vw, vh]{Viewport};
    float HorizontalScaling{vw / WorldSpaceWidth};
    float VerticalScaling{vh / WorldSpaceHeight};
    
    return {
      vx + Pos.x * HorizontalScaling, 
      vx + x * HorizontalScaling,
      vy + (WorldSpaceHeight - Pos.y) * VerticalScaling 
      vy + (WorldSpaceHeight - y) * VerticalScaling
    };
  }
  
  // ...
}

Screen Space Camera

If we wanted to define our camera’s position in screen space, we’d perform our transformations in the opposite order. We’d first transform our object’s positions based on the viewport settings, and then update them to account for the camera’s position:

// Scene.h
// ...

class Scene {
public:
  Vec2 CameraPosition{0, 0}; // Screen Space 
  Vec2 ToScreenSpace(const Vec2& Pos) const {
    auto [vx, vy, vw, vh]{Viewport};
    float HorizontalScaling{vw / WorldSpaceWidth};
    float VerticalScaling{vh / WorldSpaceHeight};

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

Camera Movement

Currently, our camera’s position is $(0, 0)$, so our scene should look the same as before:

Screenshot showing our rendered scene

Let’s move our camera to the right and up to confirm our view updates as expected. Given our camera’s position is in world space, we move it right by increasing it’s x position, and up by increasing it’s y position:

// Scene.h
// ...

class Scene {
// ...
private:
  // ...
  Vec2 CameraPosition{300, 50}; // World space 
}
Screenshot of the scene with the dragon in the center

Currently, our view space is defined very similarly to our world space. The only difference is that, in world space, the origin represents the bottom-left of the world whilst, in view space, the origin represents the location of the camera.

We can visualize the difference as the view space being offset from the world space. The direction and magnitude of that offset is controlled by the CameraPosition vector:

Diagram showing the view space relative to the world space

Remember, view space represents the scene as viewed from the perspective of the camera. In view space, the camera doesn’t move - it is always at the origin - $(0, 0)$.

To simulate the effect of a movable camera, we convert world space positions (highlighted in green) to equivalent view space positions (highlighted in blue) by moving them in the opposite direction of the CameraPosition vector.

For example, if our camera moves up and to the right, then from the perspective of the camera, the objects will move down and to the left:

Diagram showing the object translation required for the world space to view space transformation

The code in our current ToViewSpace() function implements this:

class Scene {
  // ...
private:
  Vec2 CameraPosition{0, 0}; // World Space
  
  Vec2 ToViewSpace(const Vec2& Pos) const {
    return {
      Pos.x - CameraPosition.x,
      Pos.y - CameraPosition.y
    };
  }
  // ...
}

Field of View

If our CameraPosition is $(0, 0)$, our world space and view space are identical, so effectively no transformation happens. As such, our scene would look like this:

Screenshot showing our rendered scene

However, if we imagined a real camera positioned at $(0, 0)$ in our world, this is not exactly what we’d expect the camera to see. We currently have the view space defined as below, where the origin is indeed the location of the camera, but the camera can only see things to its top right:

Diagram showing the view space with a bottom left origin

Instead, we want the camera to sees things "in front" of it. We can represent this as a rectangle positioned such that the camera is in the center of it. Or, equivalently, a view space where the origin represents the center:

Diagram showing the view space with a central origin

The size of this rectangle is determined by the camera’s field of view. For now, we’ll keep things simple and make our field of view the same size as our world space - a 1400 x 600 rectangle.

To transform our objects from world space to the definition of view space that includes the origin change, our ToViewSpace() function needs to be slightly expanded. Visually, we can imagine one way of achieving our goal would be moving our camera down and to the left by half of our rectangle’s height and width:

Diagram showing the camera offset required to change the view space origin

But again, our camera doesn’t really move in view space - it’s position is always $(0, 0)$. So instead, we replicate the effect by moving our objects by the same amount but in the opposite direction - up and to the right.

In view space, we move objects right by increasing their x position, and up by increasing their y position:

// Scene.h
// ...

class Scene {
// ...
private:
  Vec2 ToViewSpace(const Vec2& Pos) const {
    // Before
    return {
      Pos.x - CameraPosition.x,
      Pos.y - CameraPosition.y
    };
    
    // After
    return {
      (Pos.x - CameraPosition.x) + WorldSpaceWidth / 2,
      (Pos.y - CameraPosition.y) + WorldSpaceHeight / 2
    };
  }
  // ...
};

We should now see the view we’d expect from a camera positioned at $(0, 0)$ in the world space:

Screenshot of our scene with the dwarf character in the top right

We can reposition our camera to the center of the world space to get our original perspective back, but now calculated using a fully implemented view space transformation:

// Scene.h
// ...

class Scene {
// ...
private:
  // Unchanged
  float WorldSpaceWidth{1400};
  float WorldSpaceHeight{600};
  // ...
  
  Vec2 CameraPosition{
    WorldSpaceWidth / 2, WorldSpaceHeight / 2 
  }
};
Screenshot showing our rendered scene

Orthographic and Perspective Projection

This example of representing the camera’s field of view as a simple rectangle is called an orthographic projection. It is commonly used in 2D scenes as, by their nature, our scenes have no depth. Depth would be a third dimension and, in 3D scenes, it is typically labelled $z$.

In 3D scenes, creating a realistic camera is more complex due to this depth. Depth creates the illusion that objects, and the spaces between objects, appear smaller the further away they are from the camera.

Creating this effect involves the field of view being modeled as a frustum - a volume that grows in size as the distance from the camera increases:

Image showing a 3D camera frustum
Image Credit: Jakob Killian

The process of mapping the objects in this three-dimensional view space to a two-dimensional space, such as screen space, is called perspective projection. We cover 3D cameras and perspective projection in more detail later in the course.

Player-Controlled Camera

Let’s update our scene to give players control of the camera. Our scene is already receiving events through it’s HandleEvent() functions. We can examine the events flowing through our scene to detect if the player has pressed one of their arrow keys.

If they have, we can update our camera’s position:

// Scene.h
// ...

class Scene {
public:
  // ... 
  void HandleEvent(SDL_Event& E) {
    if (E.type == SDL_KEYDOWN) {
      switch (E.key.keysym.sym) {
        case SDLK_LEFT:
          CameraPosition.x -= 20;
          break;
        case SDLK_RIGHT:
          CameraPosition.x += 20;
          break;
        case SDLK_UP:
          CameraPosition.y += 20;
          break;
        case SDLK_DOWN:
          CameraPosition.y -= 20;
          break;
      }
    }

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

Attached Cameras

In many games, the camera is attached to some other object in the scene, often the character that the player is controlling. We can replicate this in a few ways. The most direct is to simply examine the player character’s position, and set the camera’s position accordingly.

CameraPosition = Player.Position;

In cases like this, we should be mindful of when our objects are being updated in each iteration of our game loop. To prevent our camera lagging one frame behind the player, we should set the camera’s position after the player’s position has been updated for the next frame, but before we start rendering that frame.

In our case, our player can’t move yet, but a good time in general to perform the update would be after our player-controlled character ticks, but before the scene renders. We’ll assume the GameObject that the player is controlling is the first in the scene’s Objects array:

// Scene.h
// ...

class Scene {
public:
  // ...
  void Render(SDL_Surface* Surface) {
    CameraPosition = Objects[0].Position; 
    
    // Remaining code unmodified
    SDL_GetClipRect(Surface, &Viewport);
    for (GameObject& Object : Objects) {
      Object.Render(Surface);
    }
  }
  // ...
};

Our camera should now center its view on the player character. Remember that the player’s Position denotes the top-left corner of their image, so our object’s rendered appearance will appear slightly lower and to the right of their actual Position:

Screenshot showing our rendered scene with the camera following the dwarf

Let’s update our HandleEvent() so that arrow keys move the player instead of the camera:

// Scene.h
// ...

class Scene {
public:
  // ...
  void HandleEvent(SDL_Event& E) {
    if (E.type == SDL_KEYDOWN) {
      switch (E.key.keysym.sym) {
        case SDLK_LEFT:
          Objects[0].Position.x -= 20;
          break;
        case SDLK_RIGHT:
          Objects[0].Position.x += 20;
          break;
        case SDLK_UP:
          Objects[0].Position.y += 20;
          break;
        case SDLK_DOWN:
          Objects[0].Position.y -= 20;
          break;
      }
    }

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

The player can now move their character around the scene. And, because the camera is attached to the player’s character, the camera will indirectly move too:

Screenshot showing our rendered scene with the dwarf moving closer to the dragon

Typically, we don’t want the camera to match the character’s position exactly. Most games want to apply some offset to the positioning, such that the scene is framed in a way that looks better.

Below, our camera still follows the player character, but is positioned 600 units to the right, and 100 units higher:

// Scene.h
// ...

class Scene {
public:
  // ...
  void Render(SDL_Surface* Surface) {
    CameraPosition = Objects[0].Position
      + Vec2{600, 100};
      
    // Unchanged
    SDL_GetClipRect(Surface, &Viewport);
    for (GameObject& Object : Objects) {
      Object.Render(Surface);
    }
  }
  // ...
};

This returns us to our original composition, where our dwarf is currently positioned at $(100, 200)$, and our camera is attached to the dwarf but with a $(600, 100)$ offset. Adding these, we get a final camera position of $(700, 300)$, which is at centre of our simple world:

Screenshot showing our rendered scene

Complete Code

Our updated Scene class is available below:

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

class Scene {
public:
  Scene() {
    Objects.emplace_back(
      "dwarf.png", Vec2{100, 200}, *this);  
    Objects.emplace_back(
      "dragon.png", Vec2{800, 500}, *this);
  }

  Vec2 ToScreenSpace(const Vec2& Pos) const {
    auto [vx, vy, vw, vh]{Viewport};
    float HorizontalScaling{vw / WorldSpaceWidth};
    float VerticalScaling{vh / WorldSpaceHeight};
    
    auto [x, y] {ToViewSpace(Pos)};
    return {
      vx + x * HorizontalScaling,
      vy + (WorldSpaceHeight - y) * VerticalScaling
    };
  }

  void HandleEvent(SDL_Event& E) {
    if (E.type == SDL_KEYDOWN) {
      switch (E.key.keysym.sym) {
        case SDLK_LEFT:
          Objects[0].Position.x -= 20;
          break;
        case SDLK_RIGHT:
          Objects[0].Position.x += 20;
          break;
        case SDLK_UP:
          Objects[0].Position.y += 20;
          break;
        case SDLK_DOWN:
          Objects[0].Position.y -= 20;
          break;
      }
    }

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

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

  void Render(SDL_Surface* Surface) {
    CameraPosition = Objects[0].Position
      + Vec2{600, 100};
    SDL_GetClipRect(Surface, &Viewport);
    for (GameObject& Object : Objects) {
      Object.Render(Surface);
    }
  }

private:
  Vec2 ToViewSpace(const Vec2& Pos) const {
    return {
      (Pos.x - CameraPosition.x) + WorldSpaceWidth / 2,
      (Pos.y - CameraPosition.y) + WorldSpaceHeight / 2
    };
  }
 
  SDL_Rect Viewport;
  std::vector<GameObject> Objects;
  float WorldSpaceWidth{1400};
  float WorldSpaceHeight{600};
  Vec2 CameraPosition{
    WorldSpaceWidth / 2, WorldSpaceHeight / 2
  };
};

Summary

In this lesson, we've explored the techniques involved in creating a 2D camera system that can be controlled directly or attached to a game character. Key takeaways:

  • Camera systems use coordinate space transformations to create the illusion of movement through the game world
  • Converting between world space and view space requires understanding the camera's position and field of view
  • In 2D games, cameras typically use orthographic projections which represent a simple rectangular view
  • Camera positioning can be manually controlled or automatically attached to follow characters
  • Applying offsets to attached cameras allows for better visual composition of game scenes
  • Coordinate transformations should happen in the correct order: world space → view space → screen space
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
Lesson Contents

Cameras and View Space

Create camera systems that follow characters, respond to player input, and properly frame your game scenes.

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
Spaces and Transformations
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