Positioning and Rendering Entities
Updating our TransformComponent
and ImageComponent
to support positioning and rendering
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 usingSDL_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:
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:

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.

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
:

Rendering Images
In the previous section, we successfully loaded our images into SDL_Surface
s 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()
.
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:
- A pointer to the
SDL_Surface
containing our image data. We're storing this as theImageSurface
member variable.ImageSurface
is a shared pointer whilstSDL_BlitSurface()
requires a raw pointer. We can get this using the shared pointer'sget()
function. - 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
- The target surface. This is the
Surface
argument provided toRender()
, which is the window surface in our program. - 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:

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 ImageComponent
s, 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:

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:

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
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 destinationSDL_Rect
'sx
andy
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()
andGetOwnerPosition()
simplify common tasks for derived components. - Debug helpers using
DrawDebugHelpers()
are invaluable for verifying positioning and transformations.
Image and Entity Scaling
Add width, height, and scaling modes to our entities and images