Positioning and Rendering Entities

Updating our TransformComponent and ImageComponent to support positioning and rendering
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

Now that we have images loaded via our AssetManager, it's time to display them correctly in our game world. This lesson covers positioning entities, setting up a world coordinate system, transforming coordinates, and using SDL's blitting function to render images. Key steps include:

  • Defining a scene's world space dimensions.
  • Implementing ToScreenSpace() coordinate conversion.
  • Rendering ImageComponent surfaces using SDL_BlitSurface().
  • Adding relative offsets to ImageComponent for fine-tuning position.
  • Creating visual debug helpers for entity locations.

Starting Point

In the previous lesson, we refactored our ImageComponent to use an AssetManager and std::shared_ptr, simplifying its code and optimizing memory usage. Here's the state of our ImageComponent as we begin this lesson:

#pragma once
#include <memory>
#include <string>
#include <SDL.h>
#include "Component.h"

class ImageComponent : public Component {
 public:
  using Component::Component;
  ImageComponent(
    Entity* Owner,
    const std::string& FilePath
  );

  void Initialize() override;
  void Render(SDL_Surface* Surface) override;
  bool LoadNewImage(const std::string& NewPath);

private:
  std::shared_ptr<SDL_Surface> ImageSurface{nullptr};
  std::string ImageFilePath;
};
#include "ImageComponent.h"
#include "Entity.h"
#include "AssetManager.h"

ImageComponent::ImageComponent(
  Entity* Owner,
  const std::string& FilePath
) : Component(Owner),
    ImageFilePath(FilePath
) {
  ImageSurface = GetAssetManager()
    .LoadSurface(ImageFilePath);
}

void ImageComponent::Render(
  SDL_Surface* Surface
) {
  if (!ImageSurface) return;

  TransformComponent* Transform{
    GetOwner()->GetTransformComponent()};

  if (Transform) {
    std::cout << "ImageComponent ("
      << ImageFilePath
      << ") ready to render at: "
      << Transform->GetPosition() << '\n';
  } else {
    std::cout << "ImageComponent ("
      << ImageFilePath
      << ") ready, but no TransformComponent found\n";
  }
}

bool ImageComponent::LoadNewImage(
  const std::string& NewPath
) {
  ImageFilePath = NewPath;
  ImageSurface = GetAssetManager()
    .LoadSurface(NewPath);
  return ImageSurface != nullptr;
}

void ImageComponent::Initialize() {
  Entity* Owner{GetOwner()};
  if (!Owner->GetTransformComponent()) {
    std::cout <<
      "Error: ImageComponent requires"
      " TransformComponent on its Owner\n";

    Owner->RemoveComponent(this);
  }
}

Positioning Entities

Currently, all of our entities are positioned at $(0, 0)$, the default value of our TransformComponent's Position vector.

We need a way to change this. Let’s add a setter for this:

// TransformComponent.h
// ...

class TransformComponent : public Component {
public:
  // ...
  void SetPosition(const Vec2& NewPosition) { 
    Position = NewPosition; 
  } 
  // ...
};

Now, back in Scene.h, let's use this to position two entities in our scene. We’ll also give them each an ImageComponent, which we’ll render later in the lesson:

// Scene.h
// ...

class Scene {
 public:
  Scene() {
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>(*this))};
    Player->AddTransformComponent()
          ->SetPosition({1, 2});  
    Player->AddImageComponent("player.png");

    EntityPtr& Enemy{Entities.emplace_back(
      std::make_unique<Entity>(*this))};
    Enemy->AddTransformComponent()
         ->SetPosition({8, 5});  
    Enemy->AddImageComponent("dragon.png");
  }
  // ...
};

World Space

Within our scene, we want the freedom to position objects independendly of screen coordinates. Just like we did in our earlier chapter on spaces and transformations, let’s define a world space for our scene.

We’ll set it up in exactly the same way we did in the earlier. We will use the y-up, x-left convention and, in this case, we’ll set it to be 14 meters wide and 6 meters tall:

Diagram showing our world space

Note that there’s no real significance to his 14 x 6 decision. We could have choosen anything. Let’s add variables to our Scene to store whichever values we choose:

// Scene.h
// ...

class Scene {
  // ...
private:
  // ...
  float WorldSpaceWidth{14}; // meters
  float WorldSpaceHeight{6}; // meters
};

To render images to the screen, we need a way to convert their world space coordinates to screen space coordinates. Our screen space coordinates will use SDL’s standard Y-Down convention. Our window’s size is 700x300 in our screenshots, but the code we write will work with any window size.

Diagram showing our screen space

To help us get the window size, our Scene will keep track of the viewport it is being rendered to. We’ll use the same technique as before, updating an SDL_Rect using SDL_GetClipRect() before rendering anything in the scene:

// Scene.h
// ...

class Scene {
public:
  // ...

  void Render(SDL_Surface* Surface) {
    SDL_GetClipRect(Surface, &Viewport);
    for (EntityPtr& Entity : Entities) {
      Entity->Render(Surface);
    }
  }

private:
  // ...
  SDL_Rect Viewport;
};

To keep things simple, we won’t bother with a dynamic camera for now. We’ll just map everything in our $(14 \times 6)$ world space to the corresponding screen space. This is the same transformation function we walked through creating earlier in the course:

#pragma once
#include <SDL.h>
#include <vector>
#include "GameObject.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
    };
  }

  // ...
};

Our ToSceenSpace() function in our Scene is public, so components can now access it through the GetScene() function defined on the Component base class:

// Conceptual Example
class SomeComponent : public Component {
  void SomeFunction() {
    GetScene().ToScreenSpace({1, 2});
  }
};

API Improvements

We expect to be accessing this ToScreenSpace() function a lot within our components, so let’s add a helper function to the Component base class that makes accessing this function a little friendlier:

// Component.h
// ...
class Vec2; 

class Component {
 public:
  // ...
  Vec2 ToScreenSpace(const Vec2& Pos) const;
  // ...
};
// Component.cpp
// ...

Vec2 Component::ToScreenSpace(const Vec2& Pos) const {
  return GetScene().ToScreenSpace(Pos);
}

Any of our components can now use a simpler API to transform a Vec2 to screen space:

// Conceptual Example
class SomeComponent : public Component {
  void SomeFunction() {
    // Before
    GetScene().ToScreenSpace({1, 2}); 

    // After
    ToScreenSpace({1, 2});
  }
};

Our components are likely to also frequently get and set the position of their owning Entity, we can add further helpers to make that easier:

// Component.h
// ...

class Component {
 public:
  // ...
  Vec2 GetOwnerPosition() const;
  void SetOwnerPosition(const Vec2& Pos) const;
  Vec2 GetOwnerScreenSpacePosition() const;
  // ...
};
// Component.cpp
// ...

Vec2 Component::GetOwnerPosition() const {
  TransformComponent* Transform{
    GetOwner()->GetTransformComponent()};
  if (!Transform) {
    std::cerr << "Error: attempted to get position"
      " of an entity with no transform component\n";
    return {0, 0};
  }
  return Transform->GetPosition();
}

void Component::SetOwnerPosition(const Vec2& Pos) const {
  TransformComponent* Transform{
    GetOwner()->GetTransformComponent()};
  if (!Transform) {
    std::cerr << "Error: attempted to set position"
      " of an entity with no transform component\n";
  } else {
    Transform->SetPosition(Pos);
  }
}

Vec2 Component::GetOwnerScreenSpacePosition() const {
  return ToScreenSpace(GetOwnerPosition());
}

This further simplifies our API:

// Conceptual Example
class SomeComponent : public Component {
  void SomeFunction() {
    // Before
    GetOwner()->GetTransformComponent()
      ->GetPosition();
    // After
    GetOwnerPosition();

    // Before
    GetOwner()->GetTransformComponent()
      ->SetPosition({1, 2});
    // After
    SetOwnerPosition({1, 2}); 

    // Before
    ToScreenSpace(GetOwner()
      ->GetTransformComponent()->GetPosition());
    // After
    GetOwnerScreenSpacePosition(); 
  }
};

Rendering Debug Helpers

It's often useful to "see" the invisible data in our game world, like the exact point our TransformComponent represents. We can achieve this using debug helpers: temporary graphics rendered only for developers. Let's create a way for components to draw their own debug information.

To set this up, we’ll add a virtual function to our Component base class, including an SDL_Surface* argument specifying where the helpers should be drawn:

// Component.h
// ...

class Component {
 public:
  // ...
  virtual void DrawDebugHelpers(
    SDL_Surface* Surface) {}
  // ...
};

If our build has enabled debug helpers through a preprocessor flag, our Entity will call this new DrawDebugHelpers() on all of it’s components. We typically draw debug helpers after everything else has rendered, as this ensures the helpers are drawn on top of the objects in our scene.

// Entity.h
// ...

#define DRAW_DEBUG_HELPERS 

class Entity {
public:
  // ...
  virtual void Render(SDL_Surface* Surface) {
    for (ComponentPtr& C : Components) {
      C->Render(Surface);
    }
    #ifdef DRAW_DEBUG_HELPERS
    for (ComponentPtr& C : Components) {
      C->DrawDebugHelpers(Surface);
    }
    #endif
  }
  // ...
};

Rendering Entity Positions

To render world space positions to the screen, we need to perform two conversions:

  • Convert them from world space to screen space
  • Convert them from floating point numbers to integers

We already have the ToScreenSpace() function in our Scene, so we just need to tackle rounding them to integers.

Let’s add a function to convert SDL_FRect objects to SDL_Rect by rounding its four values. We’ll define this function in a new Utilities header file and namespace so we can easily include it wherever it is needed:

// Utilities.h
#pragma once
#include <SDL.h>

namespace Utilities{
inline SDL_Rect Round(const SDL_FRect& R) {
  return {
    static_cast<int>(SDL_round(R.x)),
    static_cast<int>(SDL_round(R.y)),
    static_cast<int>(SDL_round(R.w)),
    static_cast<int>(SDL_round(R.h)),
  };
}
}

Note that the function in this header file includes the inline keyword. This will prevent future issues when we #include it in multiple files.

Let’s now combine the Component::ToScreenSpace() and Utilities::Round() functions to overload the DrawDebugHelpers() function in our TransformComponent. We’ll have it draw a 20 x 20 pixel square, centered at its screen space position:

// TransformComponent.h
// ...
#include <SDL.h> // for SDL_Surface
#include "Utilities.h" // for Round

class TransformComponent : public Component {
 public:
  // ...
  void DrawDebugHelpers(SDL_Surface* S) override {
    auto [x, y]{ToScreenSpace(Position)};
    SDL_Rect Square{Utilities::Round({
      x - 10, y - 10, 20, 20
    })};
    SDL_FillRect(S, &Square, SDL_MapRGB(
      S->format, 255, 0, 0));
  }
  // ...
};

We should now see the the position of the player and enemy entities we added to our Scene:

Screenshot showing our Transform Component's debug helpers rendering

Rendering Images

In the previous section, we successfully loaded our images into SDL_Surfaces within our AssetManager, and our ImageComponent instances have a shared pointer to those surfaces. Now, let's get use them to render our images to the screen!

The SDL function we need is SDL_BlitSurface().

Reminder: Blitting and SDL_BlitSurface()

Think of "blitting" as efficiently copying a rectangular block of pixels from one place (our loaded image surface) to another (our window's surface).

To tell SDL_BlitSurface() what and where to draw, we need to provide 4 arguments:

  1. The source surface: A pointer to the SDL_Surface containing the image we want to copy
  2. The source rectangle: A pointer to an SDL_Rect representing the rectangular area of the source surface that we want to copy. This can be a nullptr if we want to copy the entire surface
  3. The destination surface: A pointer to the SDL_Surface where we want the content blitted to
  4. The destination rectangle: A pointer to an SDL_Rect controlling where on the destination surface we will blit to. The x and y specify the top-left corner. The w and h do not influence the blit but, after calling SDL_BlitSurface(), they will be updated with the width and height of the area that was blitted. This might be useful information for follow-up operations.

Note that the w and h values that SDL_BlitSurface() writes to our destination rectangle will not necessarily the same as the values in our source rectangle (or the width and height of our image, if we didn’t supply a source rectangle)

The values may be smaller, and that is because if the source data didn’t entirely fit within the destination surface at the x, y position provided, then our image will have been cropped. The w and h in the destination rectangle contains the values after any cropping has occured.

Diagram comparing our surfaces and rectangles

SDL_BlitSurface() returns a boolean indicating whether the blit was successful. If it was unsuccessful, we can call SDL_GetError() for more information.

We covered blitting in a dedicated lesson earlier in the course:

Let's delete everything in ImageComponent::Render() and start by building the destination rectangle. We can get these coordinates we need for our blitting operation using our new GetEntityScreenSpacePosition() function in the Component base class and the Round function in our Utilities namespace.

Only the x and y values of our destination rectangle are relevant for SDL_BlitSurface(), so we’ll just set w and h to 0:

// ImageComponent.cpp
// ...
#include "Utilities.h"

// ...

void ImageComponent::Render(
  SDL_Surface* Surface
) {
  if (!ImageSurface) return;
  
  auto [x, y]{GetOwnerScreenSpacePosition()};
  SDL_Rect Destination{
    Utilities::Round({x, y, 0, 0})
  };
}

// ...

We now have everything we need for our call to SDL_BlitSurface(). Our arguments are:

  1. A pointer to the SDL_Surface containing our image data. We’re storing this as the ImageSurface member variable. ImageSurface is a shared pointer whilst SDL_BlitSurface() requires a raw pointer. We can get this using the shared pointer’s get() function.
  2. The source rectangle specifying where we want to copy data from we want to copy data from. We want to blit the entire image, so we pass nullptr
  3. The target surface. This is the Surface argument provided to Render(), which is the window surface in our program.
  4. The destination rectangle, which we created above.

Let’s put all of this together. We’ll also check the return value of SDL_BlitSurface() for errors, just in case:

// ImageComponent.cpp
// ...

void ImageComponent::Render(
  SDL_Surface* Surface
) {
  if (!ImageSurface) return;
  auto [x, y]{GetOwnerScreenSpacePosition()};
  SDL_Rect Destination{Utilities::Round({x, y, 0, 0})};
  
  if (SDL_BlitSurface(
    ImageSurface.get(),
    nullptr,
    Surface,
    &Destination
  ) < 0) {
    std::cerr << "Error: Blit failed: "
      << SDL_GetError() << '\n';
  }
}

// ...

Let’s compile and run our project, and confirm that everything is working:

Screenshot showing our image components rendering

Positioning Images

Drawing our images at the entity's exact top-left position works, but what if we want the entity's TransformComponent position to represent its center instead? Or maybe its feet for ground alignment? Maybe our Entity has multiple ImageComponents, which we need to position relative to each other.

To solve either problem, we need a way to draw the image with an offset relative to the entity's main position.

Let's add a Vec2 Offset member to ImageComponent. We'll also need a SetOffset() method so other code can control this offset:

// ImageComponent.h
// ...
#include "Vec2.h" 

class ImageComponent : public Component {
 public:
  // ...

  void SetOffset(const Vec2& NewOffset) { 
    Offset = NewOffset; 
  } 

private:
  // ...
  Vec2 Offset{0, 0}; // Default to no offset 
};

Next, we need to use this offset when rendering. We’ll modify the Render() method in ImageComponent.cpp.

In this case, we’re assuming our offset is defined in screen space, which tends to be more useful for image data, but we could also add a WorldSpaceOffset variable if we wanted to support both.

To offset our image in world space, we add our offset to the screen space position of our entity:

// ImageComponent.cpp
// ...

void ImageComponent::Render(
  SDL_Surface* Surface
) {
  if (!ImageSurface) return;

  auto [x, y]{
    // Before:
    GetOwnerScreenSpacePosition() 
    // After:
    GetOwnerScreenSpacePosition() + Offset 
  };
  
} // ...

Drawing Debug Helpers

Let’s add some debug helpers to our ImageComponent class, too. We’ll draw a small blue rectangle at our rendering position, factoring in the Offset value.

We’ll override the DrawDebugHelpers() function in our header file:

// ImageComponent.h
// ...

class ImageComponent : public Component {
 public:
  // ...
  void DrawDebugHelpers(SDL_Surface*) override; 
  // ...
};

And implement it in our source file:

// ImageComponent.cpp
// ...

void ImageComponent::DrawDebugHelpers(
  SDL_Surface* Surface
){
  if (!ImageSurface) return;

  auto [x, y]{GetOwnerScreenSpacePosition() + Offset};

  SDL_Rect DebugRect{Utilities::Round({
      x - 5, y - 5, 10, 10 })};

  SDL_FillRect(Surface, &DebugRect, SDL_MapRGB(
    Surface->format, 0, 0, 255));
}

// ...

With our offset set to {0, 0}, we should see our TransformComponent and ImageComponent render their debug output in the same position:

Screenshot showing our image components debug helpers rendering

Getting Image Dimensions

Calculating offsets often requires knowing the dimensions of the image being drawn (e.g., to find its center). Let's add helper methods GetSurfaceWidth() and GetSurfaceHeight() to ImageComponent so it can provide this information.

Let’s add the declarations in ImageComponent.h:

// ImageComponent.h
// ...

class ImageComponent : public Component {
 public:
  // ...

  int GetSurfaceWidth() const;
  int GetSurfaceHeight() const;

  // ...
};

And implement them in ImageComponent.cpp. They’ll log an error message and safely return 0 if ImageSurface is nullptr.

// ImageComponent.cpp
// ...

int ImageComponent::GetSurfaceWidth() const {
  if (!ImageSurface) {
    std::cerr << "Warning: Attempted to get "
      "width from null ImageSurface.\n";
    return 0;
  }
  return ImageSurface->w;
}

int ImageComponent::GetSurfaceHeight() const {
  if (!ImageSurface) {
    std::cerr << "Warning: Attempted to get "
      "height from null ImageSurface.\n";
    return 0;
  }
  return ImageSurface->h;
}

// ...

Fantastic! We now have all the pieces to easily control our image positions. Let's go back to Scene.h one more time.

We’ll use the functions we added to update the Offset of the ImageComponent used by our player:

// Scene.h
// ...

class Scene {
 public:
  Scene() {
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>(*this))};

    Player->AddTransformComponent()
          ->SetPosition({2, 1});
    
    ImageComponent* PlayerImage{
      Player->AddImageComponent("player.png")
    };

    PlayerImage->SetOffset({
      PlayerImage->GetSurfaceWidth() * -0.5f,
      PlayerImage->GetSurfaceHeight() * -1.0f
    });

    EntityPtr& Enemy{Entities.emplace_back(
      std::make_unique<Entity>(*this))};

    Enemy->AddTransformComponent()
         ->SetPosition({8, 5});
    Enemy->AddImageComponent("dragon.png");
  }

  // ...
};

This will make the player image appear centered on it’s TransformComponent's position. The dragon will still be drawn from its top-left:

Screenshot showing our image components rendering with an offset

Reminder: Entity Subtypes

For simplicity in these lessons, we’re configuring our entities within our Scene constructor. However we can (and, in larger projects, should) introduce new classes for the various types of entities that we have.

This gives us a natural place to configure those entities, such as the components they should have by default, and the configuration of those components:

// Character.h
#pragma once
#include "Entity.h"
#include "ImageComponent.h"
#include "TransformComponent.h"

class Scene;
class Character : public Entity {
 public:
  Character(Scene& Scene, Vec2 InitialPosition)
    : Entity{Scene}
  {
    AddTransformComponent()
      ->SetPosition(InitialPosition);
      
    ImageComponent* Image{
      AddImageComponent("player.png")};
      
    Image->SetOffset({
      Image->GetSurfaceWidth() * -0.5f,
      Image->GetSurfaceHeight() * -1.0f
    });
  }
};

This let’s us remove low-level noise from the much more important Scene class:

// Scene.h
// ...
#include "Character.h" 
// ...

class Scene {
 public:
  Scene() {
    Entities.emplace_back(
      std::make_unique<Character>(
        *this, Vec2{2, 1}));
      
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>(*this))};

    Player->AddTransformComponent()
          ->SetPosition({2, 1});
    
    ImageComponent* PlayerImage{
      Player->AddImageComponent("player.png")
    };

    PlayerImage->SetOffset({
      PlayerImage->GetSurfaceWidth() * -0.5f,
      PlayerImage->GetSurfaceHeight() * -1.0f
    });

    // ...
  }

  // ...
};

Complete Code

Below, we’ve provided complete versions of the files we changed in this lesson. We’ll continue working on these in the next lesson

#pragma once
#include <SDL.h>
#include <vector>
#include "AssetManager.h"
#include "Entity.h"

using EntityPtr = std::unique_ptr<Entity>;
using EntityPtrs = std::vector<EntityPtr>;

class Scene {
 public:
  Scene() {
    EntityPtr& Player{Entities.emplace_back(
      std::make_unique<Entity>(*this))};

    Player->AddTransformComponent()
          ->SetPosition({2, 1});
    ImageComponent* PlayerImage{
      Player->AddImageComponent("player.png")
    };

    PlayerImage->SetOffset({
      PlayerImage->GetSurfaceWidth() * -0.5f,
      PlayerImage->GetSurfaceHeight() * -1.0f
    });

    EntityPtr& Enemy{Entities.emplace_back(
      std::make_unique<Entity>(*this))};

    Enemy->AddTransformComponent()
         ->SetPosition({8, 5});
    Enemy->AddImageComponent("dragon.png");
  }

  void HandleEvent(SDL_Event& E) {
    for (EntityPtr& Entity : Entities) {
      Entity->HandleEvent(E);
    }
  }

  void Tick(float DeltaTime) {
    for (EntityPtr& Entity : Entities) {
      Entity->Tick(DeltaTime);
    }
  }

  void Render(SDL_Surface* Surface) {
    SDL_GetClipRect(Surface, &Viewport);
    for (EntityPtr& Entity : Entities) {
      Entity->Render(Surface);
    }
  }

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

  AssetManager& GetAssetManager() {
    return Assets;
  }

 private:
  EntityPtrs Entities;
  AssetManager Assets;
  SDL_Rect Viewport;
  float WorldSpaceWidth{14};  // meters
  float WorldSpaceHeight{6};  // meters
};
#pragma once
#include <memory>
#include <vector>
#include <SDL.h>
#include "Component.h"
#include "TransformComponent.h"
#include "ImageComponent.h"

class Scene;
using ComponentPtr = std::unique_ptr<Component>;
using ComponentPtrs = std::vector<ComponentPtr>;

class Entity {
public:
  Entity(Scene& Scene) : OwningScene{Scene} {}
  Scene& GetScene() const { return OwningScene; }

  virtual void HandleEvent(const SDL_Event& E) {
    for (ComponentPtr& C : Components) {
      C->HandleEvent(E);
    }
  }

  virtual void Tick(float DeltaTime) {
    for (ComponentPtr& C : Components) {
      C->Tick(DeltaTime);
    }
  }

  virtual void Render(SDL_Surface* Surface) {
    for (ComponentPtr& C : Components) {
      C->Render(Surface);
    }
    for (ComponentPtr& C : Components) {
      C->DrawDebugHelpers(Surface);
    }
  }

  virtual ~Entity() = default;

  TransformComponent* AddTransformComponent() {
    if (GetTransformComponent()) {
      std::cout << "Error: Cannot have "
        "multiple transform components";
      return nullptr;
    }

    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<
          TransformComponent>(this))};

    NewComponent->Initialize();

    return static_cast<TransformComponent*>(
      NewComponent.get());
  }

  TransformComponent* GetTransformComponent() const {
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{
        dynamic_cast<TransformComponent*>(C.get())
      }) {
        return Ptr;
      }
    }
    return nullptr;
  }

  ImageComponent* AddImageComponent(
    const std::string& FilePath
  ) {
    std::unique_ptr<Component>& NewComponent{
      Components.emplace_back(
        std::make_unique<ImageComponent>(
          this, FilePath))};

    NewComponent->Initialize();

    return static_cast<ImageComponent*>(
      NewComponent.get());
  }

  using ImageComponents =
    std::vector<ImageComponent*>;
  ImageComponents GetImageComponents() const {
    ImageComponents Result;
    for (const ComponentPtr& C : Components) {
      if (auto Ptr{dynamic_cast<
        ImageComponent*>(C.get())}
      ) {
        Result.push_back(Ptr);
      }
    }
    return Result;
  }

  void RemoveComponent(Component* PtrToRemove) {
    for (size_t i{0}; i < Components.size(); ++i) {
      if (Components[i].get() == PtrToRemove) {
        Components.erase(Components.begin() + i);
        return;
      }
    }

    std::cout << "Warning: Attempted to remove "
      "a component not found on this entity.\n";
  }

protected:
  ComponentPtrs Components;
  Scene& OwningScene;
};
#pragma once
#include <SDL.h>

class Entity;
class Scene;
class AssetManager;
class Vec2;

class Component {
 public:
  Component(Entity* Owner) : Owner(Owner) {}
  virtual void Initialize() {}
  virtual void HandleEvent(const SDL_Event& E) {}
  virtual void Tick(float DeltaTime) {}
  virtual void Render(SDL_Surface* Surface) {}
  virtual void DrawDebugHelpers(
    SDL_Surface* Surface) {}
  virtual ~Component() = default;

  Entity* GetOwner() const { return Owner; }
  Scene& GetScene() const;
  AssetManager& GetAssetManager() const;
  Vec2 ToScreenSpace(const Vec2& Pos) const;
  Vec2 GetOwnerPosition() const;
  void SetOwnerPosition(const Vec2& Pos) const;
  Vec2 GetOwnerScreenSpacePosition() const;

private:
  Entity* Owner{nullptr};
};
#include "Component.h"
#include "Entity.h"
#include "Scene.h"

Scene& Component::GetScene() const {
  return GetOwner()->GetScene();
}

AssetManager& Component::GetAssetManager() const {
  return GetScene().GetAssetManager();
}

Vec2 Component::ToScreenSpace(const Vec2& Pos) const {
  return GetScene().ToScreenSpace(Pos);
}

Vec2 Component::GetOwnerPosition() const {
  TransformComponent* Transform{
    GetOwner()->GetTransformComponent()};
  if (!Transform) {
    std::cerr << "Error: attempted to get position"
      " of an entity with no transform component\n";
    return {0, 0};
  }
  return Transform->GetPosition();
}

void Component::SetOwnerPosition(const Vec2& Pos) const {
  TransformComponent* Transform{
    GetOwner()->GetTransformComponent()};
  if (!Transform) {
    std::cerr << "Error: attempted to set position"
      " of an entity with no transform component\n";
  } else {
    Transform->SetPosition(Pos);
  }
}

Vec2 Component::GetOwnerScreenSpacePosition() const {
  return ToScreenSpace(GetOwnerPosition());
}
#pragma once
#include <SDL.h>
#include "Utilities.h"
#include "Vec2.h"
#include "Component.h"

class TransformComponent : public Component {
 public:
  using Component::Component;

  Vec2 GetPosition() const { return Position; }

  void SetPosition(const Vec2& NewPosition) {
    Position = NewPosition;
  }

  void DrawDebugHelpers(SDL_Surface* S) override {
    auto [x, y]{ToScreenSpace(Position)};
    SDL_Rect Square{Utilities::Round({
      x - 10, y - 10, 20, 20
    })};
    SDL_FillRect(S, &Square, SDL_MapRGB(
      S->format, 255, 0, 0));
  }

 private:
  Vec2 Position{0, 0};
};
#pragma once
#include <memory>
#include <string>
#include <SDL.h>
#include "Component.h"
#include "Vec2.h"

class ImageComponent : public Component {
 public:
  using Component::Component;
  ImageComponent(
    Entity* Owner,
    const std::string& FilePath
  );

  void Initialize() override;
  void Render(SDL_Surface* Surface) override;
  void DrawDebugHelpers(SDL_Surface*) override;
  bool LoadNewImage(const std::string& NewPath);
  int GetSurfaceWidth() const;
  int GetSurfaceHeight() const;

  void SetOffset(const Vec2& NewOffset) {
    Offset = NewOffset;
  }

private:
  std::shared_ptr<SDL_Surface> ImageSurface{nullptr};
  std::string ImageFilePath;
  Vec2 Offset{0, 0};
};
#include <SDL.h>
#include "ImageComponent.h"
#include "Entity.h"
#include "AssetManager.h"
#include "Utilities.h"

ImageComponent::ImageComponent(
  Entity* Owner,
  const std::string& FilePath
) : Component(Owner),
    ImageFilePath(FilePath
) {
  ImageSurface = GetAssetManager()
    .LoadSurface(ImageFilePath);
}

void ImageComponent::Render(
  SDL_Surface* Surface
) {
  if (!ImageSurface) return;

  auto [x, y]{
    GetOwnerScreenSpacePosition() + Offset
  };

  SDL_Rect Destination{
    Utilities::Round({x, y, 0, 0})};
  if (SDL_BlitSurface(
    ImageSurface.get(),
    nullptr,
    Surface,
    &Destination
  ) < 0) {
    std::cerr << "Error: Blit failed: "
      << SDL_GetError() << '\n';
  }
}

void ImageComponent::DrawDebugHelpers(
  SDL_Surface* Surface
){
  if (!ImageSurface) return;

  auto [x, y]{
    GetOwnerScreenSpacePosition() + Offset};

  SDL_Rect DebugRect{Utilities::Round({
      x - 5, y - 5, 10, 10 
  })};

  SDL_FillRect(Surface, &DebugRect, SDL_MapRGB(
    Surface->format, 0, 0, 255));
}

bool ImageComponent::LoadNewImage(
  const std::string& NewPath
) {
  ImageFilePath = NewPath;
  ImageSurface = GetAssetManager()
    .LoadSurface(NewPath);
  return ImageSurface != nullptr;
}

int ImageComponent::GetSurfaceWidth() const {
  if (!ImageSurface) {
    std::cerr << "Warning: Attempted to get "
      "width from null ImageSurface.\n";
    return 0;
  }
  return ImageSurface->w;
}

int ImageComponent::GetSurfaceHeight() const {
  if (!ImageSurface) {
    std::cerr << "Warning: Attempted to get "
      "height from null ImageSurface.\n";
    return 0;
  }
  return ImageSurface->h;
}

void ImageComponent::Initialize() {
  Entity* Owner{GetOwner()};
  if (!Owner->GetTransformComponent()) {
    std::cout <<
      "Error: ImageComponent requires"
      " TransformComponent on its Owner\n";

    Owner->RemoveComponent(this);
  }
}
#pragma once
#include <SDL.h>

namespace Utilities{
inline SDL_Rect Round(const SDL_FRect& R) {
  return {
    static_cast<int>(SDL_round(R.x)),
    static_cast<int>(SDL_round(R.y)),
    static_cast<int>(SDL_round(R.w)),
    static_cast<int>(SDL_round(R.h)),
  };
}
}

Summary

We bridged the gap between having loaded images and displaying them in our game world. This involved setting up world dimensions in the Scene, creating a conversion function from world to screen space, and using SDL_BlitSurface() in ImageComponent to draw the image pixels.

We further refined this by adding an offset property to ImageComponent for better control over placement and implemented debug visuals for transforms and image render points.

Key Takeaways:

  • Game scenes typically operate in a world coordinate system (e.g., meters).
  • Rendering requires converting world coordinates to the window's screen coordinates, considering viewport position and scaling.
  • SDL_BlitSurface() copies pixel data; the destination SDL_Rect's x and y define the top-left corner on the target surface.
  • Components can have offsets to adjust their visual representation relative to the owner entity's transform.
  • Helper functions in base classes like Component::ToScreenSpace() and GetOwnerPosition() simplify common tasks for derived components.
  • Debug helpers using DrawDebugHelpers() are invaluable for verifying positioning and transformations.
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
Creating Components
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:

  • 105 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