In this lesson, we’ll start building our ImageComponent
, converting it from its current placeholder state to something much more useful. We will walk through using SDL_image
to load assets, storing image data using SDL_Surface
, and handling the associated resource management. You will learn:
SDL_Surface*
.Currently, we have Entity
objects, and they can have a TransformComponent
to know where they are in the world. But right now, they're completely invisible.
Our current ImageComponent
is just a placeholder that prints text:
// ImageComponent.h
#pragma once
#include "Component.h"
class ImageComponent : public Component {
public:
using Component::Component;
void Initialize() override;
void Render(SDL_Surface* Surface) override;
};
// ImageComponent.cpp
#include "ImageComponent.h"
#include "Entity.h"
void ImageComponent::Render(
SDL_Surface* Surface
) {
TransformComponent* Transform{
GetOwner()->GetTransformComponent()
};
std::cout << "ImageComponent rendering at: "
<< Transform->GetPosition() << '\n';
}
void ImageComponent::Initialize() {
Entity* Owner{GetOwner()};
if (!Owner->GetTransformComponent()) {
std::cout << "Error: ImageComponent requires"
" TransformComponent on its Owner\n";
// Request removal
Owner->RemoveComponent(this);
}
}
It's time to give it the power to actually load and display images.
We'll be using the SDL_image
library for this, which helps us load common formats like PNG and JPG. You should have SDL_image
set up from earlier in the course.
SDL_Init()
and SDL_Quit()
Before we use SDL_image
, we should initialize. We do this by calling IMG_Init()
and telling it which image formats we want to support (like PNG and JPG) as a bitmask:
IMG_Init(IMG_INIT_PNG | IMG_INIT_JPG);
The IMG_Init
function returns a bitmask of the formats it initialized. We can compare this return value to the bitmask we requested to check if anything went wrong.
We can call SDL_GetError()
or, equivalently, IMG_GetError()
for an explanation of the most recent error:
int IMG_FLAGS{IMG_INIT_PNG | IMG_INIT_JPG};
if (!(IMG_Init(IMG_FLAGS) & IMG_FLAGS)) {
std::cerr << "IMG_Init failed! "
<< "IMG_Error: " << IMG_GetError() << '\n';
} else {
std::cout << "IMG_Init successful\n";
}
We cover bitmasks, the |
operator, and the &
operator in more detail here:
When our program no longer need SDL_image
, we should call IMG_Quit()
to let it clean up any resources it was using. Let’s add IMG_Init()
and IMG_Quit()
to our main.cpp
:
// main.cpp
#include <SDL.h>
#include <SDL_image.h>
#include "Window.h"
#include "Scene.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
int IMG_FLAGS{IMG_INIT_PNG | IMG_INIT_JPG};
if (!(IMG_Init(IMG_FLAGS) & IMG_FLAGS)) {
std::cerr << "IMG_Init failed! "
<< "IMG_Error: " << IMG_GetError() << '\n';
} else {
std::cout << "IMG_Init successful\n";
}
Window GameWindow;
Scene GameScene;
Uint64 LastTick{SDL_GetPerformanceCounter()};
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
GameScene.HandleEvent(Event);
if (Event.type == SDL_QUIT) {
IMG_Quit();
SDL_Quit();
return 0;
}
}
}
return 0;
}
Now that our program is handling the initialization and cleanup of SDL_image
, let’s start working on our image component. First things first, our ImageComponent
needs a place to store the actual image data once it's loaded.
SDL provides the SDL_Surface
structure for this purpose. It essentially holds the raw pixel data for an image in the computer's main memory. We'll use a raw pointer, SDL_Surface*
, to point to this data.
Because we're managing this resource ourselves (the SDL_Surface
is created dynamically), we also need to be responsible for cleaning it up when the ImageComponent
is destroyed. This means we'll need a destructor - ~ImageComponent()
.
Let's modify ImageComponent.h
to add the SDL_Surface* ImageSurface
member, and also a std::string ImageFilePath
to remember which file we loaded.
We'll update the constructor to take the file path as an argument and declare that all-important destructor:
// ImageComponent.h
#pragma once
#include <string>
#include <SDL.h>
#include "Component.h"
class ImageComponent : public Component {
public:
using Component::Component;
// Constructor takes entity owner and image file path
ImageComponent(
Entity* Owner,
const std::string& FilePath
);
~ImageComponent() override;
void Initialize() override;
void Render(SDL_Surface* Surface) override;
private:
// Pointer to the loaded surface data
SDL_Surface* ImageSurface{nullptr};
// Store the path used to load this image
std::string ImageFilePath;
};
Now, let's jump over to ImageComponent.cpp
to implement the constructor. The first step in the constructor is to actually load the image file specified by FilePath
.
We use the IMG_Load()
function for this. It takes the file path as a C-style string, so we use .c_str()
and, if successful, returns a pointer to a newly created SDL_Surface
containing the image data.
It's important to check if IMG_Load()
succeeded. If it returns nullptr
, something went wrong (maybe the file doesn't exist, or it's a format SDL_image
couldn't handle).
We should print an error message so we know about the problem. If it does succeed, we store the returned pointer in our ImageSurface
member.
// ImageComponent.cpp
#include "ImageComponent.h"
#include <iostream>
ImageComponent::ImageComponent(
Entity* Owner,
const std::string& FilePath
) : Component(Owner),
ImageFilePath(FilePath)
{
// Load the image from the file path
ImageSurface = IMG_Load(FilePath.c_str());
// Check if IMG_Load returned a valid surface pointer
if (!ImageSurface) {
// If null, loading failed. Print an error.
std::cerr
<< "Failed to load image: " << FilePath
<< " Error: " << IMG_GetError() << '\n';
} else {
// Loading succeeded!
std::cout << "Loaded image: "
<< FilePath << '\n';
}
}
Now for the cleanup crew! We need to implement the destructor ~ImageComponent()
. Its main job is to free the memory used by the SDL_Surface
we loaded.
We do this using SDL_FreeSurface()
. We should ensure we don’t call it too early, or that we don’t call it with a pointer that was previously freed. It is, however, safe to call SDL_FreeSurface()
with a nullptr
.
// ImageComponent.cpp
// ...
// Implement the destructor
ImageComponent::~ImageComponent() {
SDL_FreeSurface(ImageSurface);
// Set pointer to null after freeing to be safe
ImageSurface = nullptr;
}
Render()
Let's replace our placeholder Render()
implementation. We’ll implement rendering for real in the next lesson but, for now, Render()
will just print a message confirming everything is ready:
// ImageComponent.cpp
// ...
void ImageComponent::Render(
SDL_Surface* Surface
) {
// Only proceed if we have a surface loaded
if (!ImageSurface) return;
// Check for transform component (needed for position)
TransformComponent* Transform{
GetOwner()->GetTransformComponent()};
if (Transform) {
// Just print a message for now
std::cout << "ImageComponent ("
<< ImageFilePath
<< ") ready to render at: "
<< Transform->GetPosition() << '\n';
} else {
// Handle case where transform is missing
std::cout << "ImageComponent ("
<< ImageFilePath
<< ") ready, but no TransformComponent found\n";
}
}
Great! Our ImageComponent
knows how to load and clean up its image. Now we need the Entity
class to be able to create it.
Let's hop over to Entity.h
and update the AddImageComponent()
method so it takes the std::string FilePath
as an argument, and forward it to the ImageComponent
constructor:
// Entity.h
// ...
class Entity {
public:
// ...
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());
}
// ...
};
The final step is to actually use this in our Scene
. Let’s visit Scene.h
and modify the constructor to create entities and call the new AddImageComponent()
method, providing the filenames for the images we want to load.
Make sure you have files named "player.png" and "dragon.png" (or whatever you choose) in a place your program can find them (usually the same directory as the executable):
// Scene.h
#pragma once
#include <SDL.h>
#include <vector>
#include <memory>
#include <string> // For file paths
#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>())};
Player->AddTransformComponent();
Player->AddImageComponent("player.png");
EntityPtr& Enemy{Entities.emplace_back(
std::make_unique<Entity>())};
Enemy->AddTransformComponent();
Enemy->AddImageComponent("dragon.png");
}
// ...
};
If we compile and run our program, we shouldn't see any images drawn yet, but we should see messages confirming "player.png" and "crate.png" were loaded successfully (or error messages if something went wrong).
We should also see a message telling us the components are ready to render on every iteration of our game loop:
IMG_Init successful
Loaded image: player.png
IMG_Init successful
Loaded image: dragon.png
ImageComponent (player.png) ready to render at: { x = 0, y = 0 }
ImageComponent (dragon.png) ready to render at: { x = 0, y = 0 }
ImageComponent (player.png) ready to render at: { x = 0, y = 0 }
ImageComponent (dragon.png) ready to render at: { x = 0, y = 0 }
...
Note: Feel free to skip this section if you’re already familiar and comfortable with copy semantics. In the next lesson, we’ll replace the code we write here with a more advanced design that does not require custom copy semantics.
When we have components that are manually managing memory, such as our SDL_Surface
, we need to consider a boring but important topic: how those objects are copied.
If we were to simply copy an ImageComponent
(e.g., ImageComponent Copy{Original};
), C++'s default behavior would perform a shallow copy. It copies the values of each member variable directly. For the ImageSurface
pointer, this means both the Original
and the Copy
would end up holding the exact same memory address, pointing to the same SDL_Surface
.
This is a recipe for disaster. When the first component (say, Copy
) gets destroyed, its destructor will call SDL_FreeSurface()
, deleting the image data. Now, the Original
component has a dangling pointer – it points to memory that's no longer valid. That will be problematic the next time Original
tries to use that pointer.
And there is a second problem: later, when Original
is destroyed, it will try to call SDL_FreeSurface()
on that same invalid address, likely leading to a crash (a double free error).
To handle this correctly, we need to define our own copying behavior. This usually involves implementing the copy constructor and the copy assignment operator.
This principle is sometimes called the Rule of Three, a C++ rule of thumb which states that if our type defines a custom destructor, a custom copy constructor, or a custom copy assignment operator, it probably needs to define all three of them.
We covered copy semantics and the Rule of Three in our introductory course:
For ImageComponent
, we want a deep copy: that is, when an ImageComponent
is copied, the new copy should load its own, independent SDL_Surface
from the ImageFilePath
.
First, let's declare these two special functions in ImageComponent.h
. We'll also declare a private helper function, LoadSurfaceInternal()
, which will contain the logic for freeing an old surface (if any) and loading a new one. This helps avoid repeating code:
// ImageComponent.h
// ...
class ImageComponent : public Component {
public:
// ...
// Copy Semantics
ImageComponent(const ImageComponent& Other);
ImageComponent& operator=(
const ImageComponent& Other);
private:
// ...
// Helper to load/reload the surface
void LoadSurfaceInternal();
};
Now, let's implement LoadSurfaceInternal()
in ImageComponent.cpp
. It first checks if ImageSurface
currently points to something; if so, it frees it.
Then, if ImageFilePath
isn't empty, it tries to load the image using IMG_Load()
and updates ImageSurface
.
// ImageComponent.cpp
// ...
void ImageComponent::LoadSurfaceInternal() {
// 1. Free existing surface if there is one
if (ImageSurface) {
SDL_FreeSurface(ImageSurface);
ImageSurface = nullptr;
}
// 2. Load new surface if path is not empty
if (!ImageFilePath.empty()) {
ImageSurface = IMG_Load(
ImageFilePath.c_str());
if (!ImageSurface) {
std::cerr
<< "Failed to load image: "
<< ImageFilePath
<< " Error: " << IMG_GetError() << '\n';
} else {
std::cout << "Loaded image: "
<< ImageFilePath << '\n';
}
}
}
Let's update our existing constructor to use this new helper function:
// ImageComponent.cpp
// ...
ImageComponent::ImageComponent(
Entity* Owner,
const std::string& FilePath
) : Component(Owner),
ImageFilePath(FilePath)
{
LoadSurfaceInternal();
}
Now we can implement the copy constructor. It's called when a new ImageComponent
is created as a copy of an existing one (which we’ll call Other
). It needs to copy the basic members (just ImageFilePath
, in this case), and then call LoadSurfaceInternal()
to perform the deep copy of the SDL_Surface
:
// ImageComponent.cpp
// ...
// Copy Constructor
ImageComponent::ImageComponent(
const ImageComponent& Other
)
: Component(Other.GetOwner()),
ImageFilePath(Other.ImageFilePath)
{
// Now load our *own* copy of the surface
LoadSurfaceInternal();
}
Next, the copy assignment operator. This is called when an existing component is assigned the value of another (ExistingComponent = OtherComponent;
).
It must handle self-assignment (assigning an object to itself), copy the basic members, and then use LoadSurfaceInternal()
to release its old surface and load a fresh copy based on the Other
component's file path. It should return a reference to itself, which it can retrieve using *this
.
// ImageComponent.cpp
// ...
// Copy Assignment Operator Implementation
ImageComponent& ImageComponent::operator=(
const ImageComponent& Other
) {
// 1. Check for self-assignment
if (this == &Other) {
return *this;
}
// 2. Copy base class members (the Owner
// entity, in this case)
Component::operator=(Other);
// 3. Copy this class's members
ImageFilePath = Other.ImageFilePath;
// 4. Handle the resource (deep copy)
// LoadSurfaceInternal() conveniently freeing
// the old surface and loading the new one.
LoadSurfaceInternal();
// 5. Return reference to self
return *this;
}
It's worth noting again that just because our ImageComponent
is now copyable doesn't mean our Entity
class is. The Entity
holds std::unique_ptr<Component>
, and unique_ptr
is explicitly non-copyable to enforce unique ownership.
Making Entity
itself copyable would involve writing its own copy constructor and assignment operator. These would need to loop through the source entity's components, create new copies of each component (using the component copy constructors we just wrote), create new unique_ptr
s pointing to these copies, and carefully set the Owner
pointer inside each new component to point to the new entity being created.
It's quite involved, so often game developers prefer creating entities from templates or factories instead of deep-copying existing ones.
With our copy operations defined, ImageComponent
now handles copying correctly, ensuring each copy gets its own independent image data.
Note: Feel free to skip this section if you’re already familiar and comfortable with move semantics. In the next lesson, we’ll replace the code we write here with a more advanced design that does not require custom move semantics.
We've made copying safe with deep copies, but reloading the image file every time we copy can be slow. Think about scenarios where you might be moving components around.
For example, our components are stored in a std::vector
within our Entity
class. When the std::vector
needs to move to a new memory location, it needs to do a lot of work behind the scenes. It will need to copy all of our image components to that new location using the copy semantics we provided, and then delete the original versions in the old location.
If the original component (the source) is just going to be destroyed or discarded immediately after the "copy", reloading the image seems wasteful.
This is where move semantics come in handy. Instead of making a deep copy, we can move the resources. We cover move semantics in detail in a dedicated lesson in our advanced course:
For our ImageComponent
, move semantics involves the new component (the destination) simply taking ownership of the existing SDL_Surface*
pointer from the source component. After all, the source component about to be deleted, so it no longer needs them.
The source object's pointer is then set to nullptr
so it doesn't try to free the resource later. We also move other data like the ImageFilePath
. This is typically much faster than copying because we avoid file I/O and memory allocation.
C++ provides two special functions for this: the move constructor and the move assignment operator. They take rvalue references (&&
) as parameters. This &&
syntax signals that the object it references is no longer needed and is likely going to be deleted soon. As such, we can steal its resources if doing so is more efficient than copying them.
Let's declare our move constructor and move assignment operator in ImageComponent.h
:
// ImageComponent.h
// ...
class ImageComponent : public Component {
public:
// ...
// Move Semantics
ImageComponent(
ImageComponent&& Other) noexcept;
ImageComponent& operator=(
ImageComponent&& Other) noexcept;
// ...
};
Note: we explain the noexcept
keyword at the end of this section
Now for the implementation in ImageComponent.cpp
. The move constructor - ImageComponent(ImageComponent&& Other)
- initializes the new object by stealing the ImageSurface
pointer from Other
.
To move other members, like the ImageFilePath
, we can use std::move()
. This is more efficient because it means we’ll use move semantics if the type has implemented it, and fall back to the slower copy semantics if not.
Crucially, we then set Other.ImageSurface
to nullptr
so Other
's destructor won't free the surface we just took.
// ImageComponent.cpp
#include <utility> // for std::move
// ...
// Move Constructor Implementation
ImageComponent::ImageComponent(
ImageComponent&& Other) noexcept
: Component(Other.GetOwner()),
// Steal the resource pointer
ImageSurface(Other.ImageSurface),
// Move the file path string efficiently
ImageFilePath(std::move(
Other.ImageFilePath))
{
// Important: Null out Other's pointer so its
// destructor doesn't free the surface we just took!
Other.ImageSurface = nullptr;
}
The move assignment operator - operator=(ImageComponent&& Other)
- is similar but handles assigning to an existing object. It must:
SDL_FreeSurface(ImageSurface)
before taking the new one.ImageSurface
pointerstd::move()
if neededOther.ImageSurface
to nullptr
.*this
.// ImageComponent.cpp
// ...
// Move Assignment Operator Implementation
ImageComponent& ImageComponent::operator=(
ImageComponent&& Other) noexcept {
// 1. Check for self-assignment
if (this == &Other) {
return *this;
}
// 2. Release *our* current resource *before*
// taking Other's
SDL_FreeSurface(ImageSurface);
// 3. Steal Other's resource pointer
ImageSurface = Other.ImageSurface;
// 4. Move other members
Component::operator=(std::move(Other));
ImageFilePath = std::move(Other.ImageFilePath);
// 5. Null out Other's pointer
Other.ImageSurface = nullptr;
// 6. Return reference to self
return *this;
}
With move semantics implemented, operations that can take advantage of moving temporary or expiring objects will now be much more efficient for our ImageComponent
, avoiding unnecessary file loading and memory management overhead.
noexcept
Moving objects is an inherently destructive operation - we’re stealing resources from an existing object, leaving that object in a broken, dilapidated state. This state often called the moved-from state.
If our move process fails, we could have a problem. It could have damaged the original object, but not fully created the new object yet. In this context, "failing" means throwing an exception, which is one way that functions can signal that something went wrong. We cover exceptions in more detail here:
Move operations are often used by standard containers like std::vector
when they resize. These containers sometimes need to guarantee that operations won't fail halfway through (strong exception guarantee). If a move operation could potentially throw an exception, the container might decide to use the slower (but safer) copy operation instead.
To signal that our move semantics are safe to use - that is, they cannot throw an exception - we can mark them as noexcept
. This allows containers and algorithms to use the faster move path confidently.
We cover noexcept
in a dedicated lesson in our advanced course:
When our program is implementing a memory-ownership model, we should respect that when moving objects. Moving is destructive so, just like we wouldn’t delete
an object we don’t own, we shouldn’t move it, either.
In this case, we have an ownership model where an Entity
owns its Component
s, and we’re using std::unique_ptr
to help implement that. To respect this ownership model, only the Entity
that owns a component should ever move it.
This ensures that the Entity
can implement the logic it needs to ensure it continues to work correctly after one if its components gets moved. If the component is moving to a new Owner
, for example, the current owner would want to remove it from it’s Components
array.
Below, we’ve provided the Image.h
and ImageComponent.cpp
complete with all the changes we made in this lesson. We’ll continue working on these in the next lesson.
// ImageComponent.h
#pragma once
#include <string>
#include <SDL.h>
#include "Component.h"
class ImageComponent : public Component {
public:
using Component::Component;
ImageComponent(
Entity* Owner,
const std::string& FilePath
);
// Copy Semantics
ImageComponent(const ImageComponent& Other);
ImageComponent& operator=(
const ImageComponent& Other);
// Move Semantics
ImageComponent(
ImageComponent&& Other) noexcept;
ImageComponent& operator=(
ImageComponent&& Other) noexcept;
~ImageComponent() override;
void Initialize() override;
void Render(SDL_Surface* Surface) override;
private:
SDL_Surface* ImageSurface{nullptr};
std::string ImageFilePath;
void LoadSurfaceInternal();
};
#include "ImageComponent.h"
#include "Entity.h"
#include "SDL_image.h"
ImageComponent::ImageComponent(
Entity* Owner,
const std::string& FilePath
) : Component(Owner),
ImageFilePath(FilePath) {
ImageSurface = IMG_Load(FilePath.c_str());
if (!ImageSurface) {
std::cerr
<< "Failed to load image: " << FilePath
<< " Error: " << IMG_GetError() << '\n';
} else {
std::cout << "Loaded image: "
<< FilePath << '\n';
}
}
ImageComponent::~ImageComponent() {
SDL_FreeSurface(ImageSurface);
ImageSurface = nullptr;
}
// Copy Constructor
ImageComponent::ImageComponent(
const ImageComponent& Other)
: Component(Other.GetOwner()),
ImageFilePath(Other.ImageFilePath) {
LoadSurfaceInternal();
}
// Copy Assignment
ImageComponent& ImageComponent::operator=(
const ImageComponent& Other
) {
if (this == &Other) {return *this;}
Component::operator=(Other);
ImageFilePath = Other.ImageFilePath;
LoadSurfaceInternal();
return *this;
}
// Move Constructor
ImageComponent::ImageComponent(
ImageComponent&& Other) noexcept
: Component(Other.GetOwner()),
ImageSurface(Other.ImageSurface),
ImageFilePath(std::move(
Other.ImageFilePath)) {
Other.ImageSurface = nullptr;
}
// Move Assignment
ImageComponent& ImageComponent::operator=(
ImageComponent&& Other) noexcept {
if (this == &Other) { return *this; }
Component::operator=(std::move(Other));
SDL_FreeSurface(ImageSurface);
ImageSurface = Other.ImageSurface;
ImageFilePath = std::move(Other.ImageFilePath);
Other.ImageSurface = nullptr;
return *this;
}
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";
}
}
void ImageComponent::Initialize() {
Entity* Owner{GetOwner()};
if (!Owner->GetTransformComponent()) {
std::cout <<
"Error: ImageComponent requires"
" TransformComponent on its Owner\n";
Owner->RemoveComponent(this);
}
}
void ImageComponent::LoadSurfaceInternal() {
if (ImageSurface) {
SDL_FreeSurface(ImageSurface);
ImageSurface = nullptr;
}
if (!ImageFilePath.empty()) {
ImageSurface = IMG_Load(
ImageFilePath.c_str());
if (!ImageSurface) {
std::cerr
<< "Failed to load image: "
<< ImageFilePath
<< " Error: " << IMG_GetError() << '\n';
} else {
std::cout << "Loaded image: "
<< ImageFilePath << '\n';
}
}
}
We also made minor changes to the main.cpp
, Scene.h
, and Entity.h
files. We’ve provided these files below, with the changes highlighted.
#include <SDL.h>
#include "SDL_image.h"
#include "Window.h"
#include "Scene.h"
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
int IMG_FLAGS{IMG_INIT_PNG | IMG_INIT_JPG};
if (!(IMG_Init(IMG_FLAGS) & IMG_FLAGS)) {
std::cerr << "IMG_Init failed! "
<< "IMG_Error: " << IMG_GetError() << '\n';
} else {
std::cout << "IMG_Init successful\n";
}
Window GameWindow;
Scene GameScene;
Uint64 LastTick{SDL_GetPerformanceCounter()};
SDL_Event Event;
while (true) {
while (SDL_PollEvent(&Event)) {
GameScene.HandleEvent(Event);
if (Event.type == SDL_QUIT) {
IMG_Quit();
SDL_Quit();
return 0;
}
}
Uint64 CurrentTick{SDL_GetPerformanceCounter()};
float DeltaTime{static_cast<float>(
CurrentTick - LastTick) /
SDL_GetPerformanceFrequency()
};
LastTick = CurrentTick;
// Tick
GameScene.Tick(DeltaTime);
// Render
GameWindow.Render();
GameScene.Render(GameWindow.GetSurface());
// Swap
GameWindow.Update();
}
return 0;
}
// Scene.h
#pragma once
#include <SDL.h>
#include <vector>
#include <string>
#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>())};
Player->AddTransformComponent();
Player->AddImageComponent("player.png");
EntityPtr& Enemy{Entities.emplace_back(
std::make_unique<Entity>())};
Enemy->AddTransformComponent();
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) {
for (EntityPtr& Entity : Entities) {
Entity->Render(Surface);
}
}
private:
EntityPtrs Entities;
};
// Entity.h
#pragma once
#include <memory>
#include <vector>
#include <SDL.h>
#include "Component.h"
#include "TransformComponent.h"
#include "InputComponent.h"
#include "Commands.h"
#include "ImageComponent.h"
using ComponentPtr = std::unique_ptr<Component>;
using ComponentPtrs = std::vector<ComponentPtr>;
class Entity {
public:
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);
}
}
virtual ~Entity() = default;
virtual void HandleCommand(
std::unique_ptr<Command> Cmd
) {
Cmd->Execute(this);
}
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;
}
InputComponent* AddInputComponent() {
if (GetInputComponent()) {
std::cout << "Error: Cannot have "
"multiple input components";
return nullptr;
}
std::unique_ptr<Component>& NewComponent{
Components.emplace_back(
std::make_unique<
InputComponent>(this))};
NewComponent->Initialize();
return static_cast<InputComponent*>(
NewComponent.get());
}
InputComponent* GetInputComponent() const {
for (const ComponentPtr& C : Components) {
if (auto Ptr{
dynamic_cast<InputComponent*>(C.get())
}) {
return Ptr;
}
}
return nullptr;
}
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";
}
private:
ComponentPtrs Components;
};
In this lesson, we brought our ImageComponent
to life! We integrated the SDL_image
library to load image files like PNGs and JPGs into SDL_Surface
objects held by the component.
We tackled the crucial task of manual resource management for the raw SDL_Surface*
, implementing a destructor to call SDL_FreeSurface()
and carefully defining copy and move operations (following the Rule of Five) to ensure correct behavior and prevent memory errors like dangling pointers or double frees.
Display graphics by creating an ImageComponent that loads files and renders them via SDL_image.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games