TransformComponent
and ImageComponent
to support positioning and renderingNow 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:
ToScreenSpace()
coordinate conversion.ImageComponent
surfaces using SDL_BlitSurface()
.ImageComponent
for fine-tuning position.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);
}
}
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");
}
// ...
};
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});
}
};
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();
}
};
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
}
// ...
};
To render world space positions to the screen, we need to perform two conversions:
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
:
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()
.
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:
SDL_Surface
containing the image we want to copySDL_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 surfaceSDL_Surface
where we want the content blitted toSDL_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.
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:
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.nullptr
Surface
argument provided to Render()
, which is the window surface in our program.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:
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
};
}
// ...
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:
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:
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
});
// ...
}
// ...
};
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)),
};
}
}
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:
SDL_BlitSurface()
copies pixel data; the destination SDL_Rect
's x
and y
define the top-left corner on the target surface.Component::ToScreenSpace()
and GetOwnerPosition()
simplify common tasks for derived components.DrawDebugHelpers()
are invaluable for verifying positioning and transformations.Updating our TransformComponent
and ImageComponent
to support positioning and rendering
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games