We've learned how to save our level designs, translating the in-memory state of actors and grid settings into a binary file using SDL's file I/O capabilities. Now, we need the ability to bring those saved designs back to life in the editor.
This lesson focuses entirely on deserialization – the process of reading the binary data and reconstructing the original objects. We'll implement the Level::Load()
function, carefully using SDL_ReadU8()
, SDL_ReadLE32()
, and other SDL_RWops
functions to read data in the sequence it was saved.
A major focus will be the Factory Pattern. Since we only store a type identifier (like 1
for BlueBlock
), we need a way to invoke the correct constructor. We'll achieve this by:
Construct()
methods to actor subclasses.std::function
).This ensures our loading logic is flexible and can handle different actor types correctly.
By the end of this lesson, we’ll have completed our level editor project!
With the serialization logic in place, we can now turn our attention to the reverse process: deserialization. The Level::Load()
function is where we'll read the binary data from a .bin file and reconstruct the level state within the editor.
The first step mirrors saving: we need to open the file. We construct the filename using the LoadedLevel
number and attempt to open it using SDL_RWFromFile()
, this time with the mode "rb" for reading in binary.
If the file doesn't exist or cannot be opened, SDL_RWFromFile()
returns nullptr
, which we check for. Before reading any data, we also call Actors.clear()
to remove any existing actors from the level currently loaded in the editor.
// Editor/Source/Level.cpp
// ...
void Level::Load() {
Actors.clear();
std::string FileName{std::format(
"Assets/Level{}.bin", LoadedLevel
)};
SDL_RWops* Handle{SDL_RWFromFile(
FileName.c_str(), "rb")};
if (!Handle) {
CheckSDLError("Loading Level");
return;
}
// TODO: Deserialize stuff
SDL_RWclose(Handle);
}
Our deserializing code must closely align with our serialization code. We must deserialize our objects respecting the size, order, and endianness in which they were serialized.
Our serialized data has three Uint8
values representing the version, grid width and grid height in that order, followed by a little-endian Uint32
representing the number of actors in the level.
Let’s deserialize them in that order, and log out what we found:
// Editor/Source/Level.cpp
// ...
void Level::Load() {
Uint8 FileVersion{SDL_ReadU8(Handle)};
if (FileVersion != VERSION) {
// This file is from a different version of
// our software - react as needed
}
Uint8 GridWidth{SDL_ReadU8(Handle)};
Uint8 GridHeight{SDL_ReadU8(Handle)};
Uint32 ActorCount{SDL_ReadLE32(Handle)};
std::cout << std::format( "Loading a version "
"{} level ({}x{}) with {} actors\n",
FileVersion, GridWidth,
GridHeight, ActorCount
);
// TODO: Load Actors
SDL_RWclose(Handle);
}
Currently, our loading code exists but isn't called automatically when the editor starts. To make the editor load Level 1 immediately on launch, we can call Load()
directly from the Level
class's constructor.
Since the LoadedLevel
member defaults to 1
, adding Load()
inside the constructor body will trigger the loading sequence for "Assets/Level1.bin" as soon as the Level
object is created within the Scene
.
// Editor/Level.h
// ...
namespace Editor {
// ...
class Level {
public:
Level(Scene& ParentScene)
: ParentScene{ParentScene} {
Load();
}
// ...
};
}
If a "Level1.bin" file exists from a previous save, its data should now be read and logged.
Loading a version 1 level (13x6) with 2 actors
To load our actors, let’s use some of the techniques we covered in our lesson on function pointers. Each specific Actor
subtype that can exist in our serialized data will have a function that is responsible for deserializing that data, constructing the object based on that data, and returning a pointer to it.
The natural place to define these functions is alongside the types themselves, so we’ll add them as a static functions to our Actor
subtypes, like BlueBlock
and GreenBlock
.
Let’s start with BlueBlock
. To construct a BlueBlock
, we need two things:
Scene
. Our Level
’s Load()
function can pass this when it’s calling our factory function.SDL_Rect
representing its position and size. The position (x
, y
) can be calculated from the grid row and grid column values in the serialized data. The size (w
, h
) is the same for all BlueBlock
instances, and is currently stored as static WIDTH
and HEIGHT
values.All of our block types will need to calculate this SDL_Rect
, so let’s add a helper function to the Actor
base class that they can all use:
// Editor/Actor.h
// ...
namespace Editor {
class Scene;
class Actor {
public:
// ...
static SDL_Rect GeneratePositionRectangle(
SDL_RWops* Handle, int Width, int Height
) {
using namespace Config::Editor;
Uint8 GridRow{SDL_ReadU8(Handle)};
Uint8 GridCol{SDL_ReadU8(Handle)};
SDL_Rect R{0, 0, Width, Height};
R.x = GridCol * HORIZONTAL_GRID_SNAP;
R.y = GridRow * VERTICAL_GRID_SNAP;
return R;
}
// ...
};
}
Back in BlueBlock
, let’s add our static
Construct()
function:
// Editor/Blocks.h
// ...
namespace Editor{
class BlueBlock : public Actor {
public:
// ...
static std::unique_ptr<Actor> Construct(
SDL_RWops* Handle,
Scene& ParentScene
) {
return std::make_unique<BlueBlock>(
ParentScene,
GeneratePositionRectangle(
Handle, WIDTH, HEIGHT));
}
};
}
GreenBlock::Construct()
will be similar - we’ll also just grab the contrived SomeNumber
and SomeArray
data from the file and apply it to the object we create:
// Editor/Blocks.h
// ...
namespace Editor{
// ...
class GreenBlock : public Actor {
public:
// ...
static std::unique_ptr<Actor> Construct(
SDL_RWops* Handle,
Scene& ParentScene
) {
auto NewActor{std::make_unique<GreenBlock>(
ParentScene,
GeneratePositionRectangle(
Handle, WIDTH, HEIGHT))};
NewActor->SomeNumber = SDL_ReadLE16(Handle);
Uint32 ArraySize{SDL_ReadLE32(Handle)};
NewActor->SomeArray.resize(ArraySize);
for (int i{0}; i < ArraySize; ++i) {
NewActor->SomeArray[i] = SDL_ReadLE32(Handle);
}
return NewActor;
}
};
}
Back in Level::Load()
, let’s use these factories to recreate our full level. We’ll store our loaders as std::function
wrappers. We only need them in our Level.cpp
file, so let’s add them in an anonymous namespace in that file, above our Load()
function:
// Editor/Source/Level.cpp
// ...
#include <functional>
// ...
namespace{
using namespace Config;
using ActorLoaderFunc = std::function<
std::unique_ptr<Actor>(SDL_RWops*, Scene&)>;
std::vector<ActorLoaderFunc> ActorLoaders{
{}, // Index 0
BlueBlock::Construct, // Index 1
GreenBlock::Construct, // Index 2
};
}
// ...
The effect of putting ActorLoaders
in an anonymous namespace is that it can now only be accessed from within this same source file, Level.cpp
. This prevents us from polluting our global scope with symbols that are only relevant to one area of our program.
The indices of this ActorLoaders
array corresponds to the numeric actor types in our serialized data. As a reminder, our ActorType
enum is set up like this:
enum class ActorType : Uint8 {
Actor = 0,
BlueBlock = 1,
GreenBlock = 2,
};
So, we store the loader for BlueBlock
at index 1, and GreenBlock
at index 2. Our program doesn’t allow basic Actor
instances to be added to the level, so we’ll never need to deserialize those. As such, we just store an empty std::functional
at the base Actor
index (0
).
When our design requires us to keep two values in sync, we should generally try to improve our approach. Inevitably we, or someone on our team, will make some change that breaks that link, thereby introducing a bug.
We’ll learn better techniques for implementing these patterns later, but one simple improvement is to use a std::unordered_map
as an alternative to a std::vector
. This lets make the mapping from ActorType
values to their corresponding std::functional
factory more explicit:
// Editor/Source/Level.cpp
// ...
#include <unordered_map>
// ...
namespace{
using namespace Config;
using ActorLoaderFunc = std::function<
std::unique_ptr<Actor>(SDL_RWops*, Scene&)>;
using enum ActorType;
std::unordered_map<
ActorType, ActorLoaderFunc
> ActorLoaders{
{Actor, {}},
{BlueBlock, BlueBlock::Construct},
{GreenBlock, GreenBlock::Construct}
};
}
// ...
Our Load()
function can use this std::unordered_map
in much the same way it uses a std::vector
, using the []
operator to access the correct loader. The only difference is that it needs to cast the Uint8
representing the actor’s type back to the Config::ActorType
enum value it originated from:
Uint8 Type{SDL_ReadU8(Handle)};
auto Loader{ActorLoaders[
// When ActorLoaders is a vector:
Type
// When ActorLoaders is an unordered_map:
static_cast<ActorType>(Type)
]};
We cover maps in more detail later in the course.
All that’s left to do is update our Load()
function to loop ActorCount
times, grab the type of each actor, call the corresponding std::functional
to create it, and then add it to the level:
// Editor/Source/Level.cpp
// ...
void Level::Load() {
for (size_t i{0}; i < ActorCount; ++i) {
Uint8 ActorType{SDL_ReadU8(Handle)};
auto Loader{ActorLoaders[ActorType]};
if (Loader) {
ActorPtr NewActor{Loader(Handle, ParentScene)};
NewActor->SetLocation(ActorLocation::Level);
AddToLevel(std::move(NewActor));
} else {
std::cout << "Error: No Loader for Actor "
"Type " << ActorType << '\n';
}
}
SDL_RWclose(Handle);
}
With all our plumbing in place, we can just add new actors to our editor as needed. In the Complete Code section below, we’ve added RedBlock
, OrangeBlock
, and YellowBlock
actors by following the exact same pattern.
ActorType
enumActor
Construct()
, Clone()
, GetActorType()
, and the constructor. This can be done manually of using our preprocessor macro if we created oneActorMenu
.With that, our program is now be complete! We can create levels which get saved in the Assets/
directory alongside our program executable. We can also load those levels later, and see them restored in our Editor
for us to modify.
Complete versions of the files we changed in this lesson are provided below:
#pragma once
#include <iostream>
#include <SDL.h>
#include <string>
#include <vector>
namespace UserEvents{
#ifdef WITH_EDITOR
inline Uint32 ACTOR_DRAG{SDL_RegisterEvents(1)};
inline Uint32 LOAD_LEVEL{SDL_RegisterEvents(1)};
inline Uint32 SAVE_LEVEL{SDL_RegisterEvents(1)};
inline Uint32 SAVE_AND_PLAY_LEVEL{
SDL_RegisterEvents(1)};
inline Uint32 LEVEL_EDIT{SDL_RegisterEvents(1)};
#endif
inline Uint32 LAUNCH_LEVEL{
SDL_RegisterEvents(1)};
}
namespace Config {
enum class ActorType : Uint8 {
Actor = 0,
BlueBlock = 1,
GreenBlock = 2,
CyanBlock = 3,
OrangeBlock = 4,
RedBlock = 5,
YellowBlock = 6,
};
inline const std::vector BUTTON_COLORS{
SDL_Color{15, 15, 15, 255}, // Normal
SDL_Color{15, 155, 15, 255}, // Hover
SDL_Color{225, 15, 15, 255}, // Active
SDL_Color{60, 60, 60, 255} // Disabled
};
inline constexpr SDL_Color FONT_COLOR{
255, 255, 255, 255};
inline const std::string FONT{
"Assets/Rubik-SemiBold.ttf"};
}
#ifdef WITH_EDITOR
namespace Config::Editor {
inline const Uint8 VERSION{1};
// Level
inline const int HORIZONTAL_GRID_SNAP{50};
inline const int VERTICAL_GRID_SNAP{25};
inline const Uint8 GRID_WIDTH{13};
inline const Uint8 GRID_HEIGHT{6};
inline const int LEVEL_WIDTH{
HORIZONTAL_GRID_SNAP * GRID_WIDTH};
inline const int LEVEL_HEIGHT{
VERTICAL_GRID_SNAP * GRID_HEIGHT};
inline constexpr SDL_Color LEVEL_BACKGROUND{
50, 50, 50, 255};
// ActorMenu
inline const int ACTOR_MENU_WIDTH{70};
inline const int ACTOR_MENU_POSITION_X{LEVEL_WIDTH};
inline const SDL_Color ACTOR_MENU_BACKGROUND{
15, 15, 15, 255};
inline const int PADDING{10};
// Window
inline const std::string WINDOW_TITLE{"Editor"};
inline const int WINDOW_WIDTH{
LEVEL_WIDTH + ACTOR_MENU_WIDTH};
inline const int WINDOW_HEIGHT{LEVEL_HEIGHT + 50};
inline const SDL_Color WINDOW_BACKGROUND{
35, 35, 35, 255};
}
#endif
inline void CheckSDLError(
const std::string& Msg) {
#ifdef CHECK_ERRORS
const char* error = SDL_GetError();
if (*error != '\0') {
std::cerr << Msg << " Error: "
<< error << '\n';
SDL_ClearError();
}
#endif
}
#pragma once
#include <SDL.h>
#include "Image.h"
enum class ActorLocation { Level, Menu };
namespace Editor {
class Scene;
class Actor {
public:
Actor(
Scene& ParentScene,
const SDL_Rect& Rect,
Image& Image
) : ParentScene{ParentScene},
Rect{Rect},
Art{Image}
{}
bool HasMouseFocus() const;
virtual bool HandleEvent(const SDL_Event& E);
void Tick(float DeltaTime) {}
void Render(SDL_Surface* Surface) {
if (isVisible) {
Art.Render(Surface, Rect);
}
}
const SDL_Rect& GetRect() const {
return Rect;
}
const SDL_Point& GetDragOffset() const {
return DragOffset;
}
const Image& GetArt() const {
return Art;
}
SDL_Point GetPosition() const {
return {Rect.x, Rect.y};
}
void SetPosition(int x, int y) {
Rect.x = x;
Rect.y = y;
}
virtual std::unique_ptr<Actor> Clone() const {
return std::make_unique<Actor>(*this);
}
ActorLocation GetLocation() const {
return Location;
}
void SetLocation(ActorLocation NewLocation) {
Location = NewLocation;
}
bool GetIsVisible() const {
return isVisible;
}
void SetIsVisible(bool NewVisibility) {
isVisible = NewVisibility;
}
virtual Config::ActorType GetActorType() const {
return Config::ActorType::Actor;
}
virtual void Serialize(SDL_RWops* Handle) const {
using namespace Config::Editor;
Uint8 ActorType{static_cast<Uint8>(GetActorType())};
SDL_WriteU8(Handle, ActorType);
Uint8 GridRow = Rect.y / VERTICAL_GRID_SNAP;
SDL_WriteU8(Handle, GridRow);
Uint8 GridCol = Rect.x / HORIZONTAL_GRID_SNAP;
SDL_WriteU8(Handle, GridCol);
}
static SDL_Rect GeneratePositionRectangle(
SDL_RWops* Handle, int Width, int Height
) {
using namespace Config::Editor;
Uint8 GridRow{SDL_ReadU8(Handle)};
Uint8 GridCol{SDL_ReadU8(Handle)};
SDL_Rect R{0, 0, Width, Height};
R.x = GridCol * HORIZONTAL_GRID_SNAP;
R.y = GridRow * VERTICAL_GRID_SNAP;
return R;
}
protected:
Scene& ParentScene;
SDL_Rect Rect;
Image& Art;
SDL_Point DragOffset{0, 0};
ActorLocation Location{ActorLocation::Menu};
bool isVisible{true};
};
}
#pragma once
#include <SDL.h>
#include <vector>
#include <memory>
#include "Actor.h"
#include "Blocks.h"
#include "Config.h"
namespace Editor {
class Scene;
using ActorPtr = std::unique_ptr<Actor>;
using ActorPtrs = std::vector<ActorPtr>;
class ActorMenu {
public:
ActorMenu(Scene& ParentScene)
: ParentScene{ParentScene}
{
using namespace Config::Editor;
Actors.emplace_back(
std::make_unique<BlueBlock>(
GetScene(),
SDL_Rect{
ACTOR_MENU_POSITION_X + PADDING,
PADDING,
0, 0
}
)
);
Actors.emplace_back(
std::make_unique<GreenBlock>(
GetScene(),
SDL_Rect{
ACTOR_MENU_POSITION_X + PADDING,
BlueBlock::HEIGHT + PADDING * 2,
0, 0
}
)
);
Actors.emplace_back(
std::make_unique<RedBlock>(
GetScene(),
SDL_Rect{
ACTOR_MENU_POSITION_X + PADDING,
BlueBlock::HEIGHT * 2 + PADDING * 3,
0, 0
}
)
);
Actors.emplace_back(
std::make_unique<OrangeBlock>(
GetScene(),
SDL_Rect{
ACTOR_MENU_POSITION_X + PADDING,
BlueBlock::HEIGHT * 3 + PADDING * 4,
0, 0
}
)
);
Actors.emplace_back(
std::make_unique<YellowBlock>(
GetScene(),
SDL_Rect{
ACTOR_MENU_POSITION_X + PADDING,
BlueBlock::HEIGHT * 4 + PADDING * 5,
0, 0
}
)
);
}
void HandleEvent(const SDL_Event& E) {
for (ActorPtr& A : Actors) {
A->HandleEvent(E);
}
}
void Tick(float DeltaTime) {
for (ActorPtr& A : Actors) {
A->Tick(DeltaTime);
}
}
void Render(SDL_Surface* Surface) {
using namespace Config::Editor;
auto [r, g, b, a]{ACTOR_MENU_BACKGROUND};
SDL_FillRect(
Surface,
&Rect,
SDL_MapRGB(Surface->format, r, g, b)
);
for (ActorPtr& A : Actors) {
A->Render(Surface);
}
}
Scene& GetScene() const {
return ParentScene;
}
private:
Scene& ParentScene;
ActorPtrs Actors;
SDL_Rect Rect{
Config::Editor::ACTOR_MENU_POSITION_X, 0,
Config::Editor::ACTOR_MENU_WIDTH,
Config::Editor::WINDOW_HEIGHT
};
};
}
#pragma once
#include "Actor.h"
namespace Editor{
class BlueBlock : public Actor {
public:
static constexpr int WIDTH{50};
static constexpr int HEIGHT{25};
BlueBlock(
Scene& ParentScene, SDL_Rect Rect
);
std::unique_ptr<Actor> Clone() const override {
return std::make_unique<BlueBlock>(*this);
}
Config::ActorType GetActorType() const override {
return Config::ActorType::BlueBlock;
}
static std::unique_ptr<Actor> Construct(
SDL_RWops* Handle,
Scene& ParentScene
) {
return std::make_unique<BlueBlock>(
ParentScene,
GeneratePositionRectangle(
Handle, WIDTH, HEIGHT));
}
};
class GreenBlock : public Actor {
public:
static constexpr int WIDTH{50};
static constexpr int HEIGHT{25};
GreenBlock(Scene& ParentScene,
SDL_Rect Rect);
Config::ActorType GetActorType() const override {
return Config::ActorType::GreenBlock;
}
std::unique_ptr<Actor> Clone() const override {
return std::make_unique<
GreenBlock>(*this);
}
Sint16 SomeNumber{32};
std::vector<Sint32> SomeArray{1, 2, 3};
void Serialize(SDL_RWops* Handle) const override {
Actor::Serialize(Handle);
SDL_WriteLE16(Handle, SomeNumber);
SDL_WriteLE32(Handle, SomeArray.size());
for (Sint32 Num : SomeArray) {
SDL_WriteLE32(Handle, Num);
}
}
static std::unique_ptr<Actor> Construct(
SDL_RWops* Handle,
Scene& ParentScene
) {
auto NewActor{std::make_unique<GreenBlock>(
ParentScene,
GeneratePositionRectangle(
Handle, WIDTH, HEIGHT))};
NewActor->SomeNumber = SDL_ReadLE16(Handle);
Uint32 ArraySize{SDL_ReadLE32(Handle)};
NewActor->SomeArray.resize(ArraySize);
for (int i{0}; i < ArraySize; ++i) {
NewActor->SomeArray[i] = SDL_ReadLE32(Handle);
}
return NewActor;
}
};
class CyanBlock : public Actor {
public:
static constexpr int WIDTH{50};
static constexpr int HEIGHT{25};
CyanBlock(Scene& ParentScene,
SDL_Rect Rect);
Config::ActorType
GetActorType() const override {
return Config::ActorType::CyanBlock;
}
std::unique_ptr<Actor>
Clone() const override {
return std::make_unique<
CyanBlock>(*this);
}
void Serialize(
SDL_RWops* Handle) const override {
Actor::Serialize(Handle);
}
static std::unique_ptr<Actor> Construct(
SDL_RWops* Handle,
Scene& ParentScene
) {
using namespace Config::Editor;
return std::make_unique<CyanBlock>(
ParentScene,
GeneratePositionRectangle(
Handle, WIDTH, HEIGHT));
}
};
class OrangeBlock : public Actor {
public:
static constexpr int WIDTH{50};
static constexpr int HEIGHT{25};
OrangeBlock(Scene& ParentScene,
SDL_Rect Rect);
Config::ActorType
GetActorType() const override {
return Config::ActorType::OrangeBlock;
}
std::unique_ptr<Actor>
Clone() const override {
return std::make_unique<
OrangeBlock>(*this);
}
void Serialize(
SDL_RWops* Handle) const override {
Actor::Serialize(Handle);
}
static std::unique_ptr<Actor> Construct(
SDL_RWops* Handle,
Scene& ParentScene
) {
using namespace Config::Editor;
return std::make_unique<OrangeBlock>(
ParentScene,
GeneratePositionRectangle(
Handle, WIDTH, HEIGHT));
}
};
class RedBlock : public Actor {
public:
static constexpr int WIDTH{50};
static constexpr int HEIGHT{25};
RedBlock(Scene& ParentScene,
SDL_Rect Rect);
Config::ActorType
GetActorType() const override {
return Config::ActorType::RedBlock;
}
std::unique_ptr<Actor>
Clone() const override {
return std::make_unique<
RedBlock>(*this);
}
void Serialize(
SDL_RWops* Handle) const override {
Actor::Serialize(Handle);
}
static std::unique_ptr<Actor> Construct(
SDL_RWops* Handle,
Scene& ParentScene
) {
using namespace Config::Editor;
return std::make_unique<RedBlock>(
ParentScene,
GeneratePositionRectangle(
Handle, WIDTH, HEIGHT));
}
};
class YellowBlock : public Actor {
public:
static constexpr int WIDTH{50};
static constexpr int HEIGHT{25};
YellowBlock(Scene& ParentScene,
SDL_Rect Rect);
Config::ActorType
GetActorType() const override {
return Config::ActorType::YellowBlock;
}
std::unique_ptr<Actor>
Clone() const override {
return std::make_unique<YellowBlock>(*this);
}
void Serialize(
SDL_RWops* Handle) const override {
Actor::Serialize(Handle);
}
static std::unique_ptr<Actor> Construct(
SDL_RWops* Handle,
Scene& ParentScene
) {
using namespace Config::Editor;
return std::make_unique<YellowBlock>(
ParentScene,
GeneratePositionRectangle(
Handle, WIDTH, HEIGHT));
}
};
}
#include "Editor/Blocks.h"
#include "Editor/Scene.h"
using namespace Editor;
BlueBlock::BlueBlock(
Scene& ParentScene, SDL_Rect Rect)
: Actor{
ParentScene,
SDL_Rect{Rect.x, Rect.y, WIDTH, HEIGHT},
ParentScene.GetAssets().BlueBlock} {}
GreenBlock::GreenBlock(
Scene& ParentScene, SDL_Rect Rect)
: Actor{
ParentScene,
SDL_Rect{Rect.x, Rect.y, WIDTH, HEIGHT},
ParentScene.GetAssets().GreenBlock} {}
CyanBlock::CyanBlock(
Scene& ParentScene, SDL_Rect Rect)
: Actor{
ParentScene,
SDL_Rect{Rect.x, Rect.y, WIDTH, HEIGHT},
ParentScene.GetAssets().CyanBlock} {}
OrangeBlock::OrangeBlock(
Scene& ParentScene, SDL_Rect Rect)
: Actor{
ParentScene,
SDL_Rect{Rect.x, Rect.y, WIDTH, HEIGHT},
ParentScene.GetAssets().OrangeBlock} {}
RedBlock::RedBlock(
Scene& ParentScene, SDL_Rect Rect)
: Actor{
ParentScene,
SDL_Rect{Rect.x, Rect.y, WIDTH, HEIGHT},
ParentScene.GetAssets().RedBlock} {}
YellowBlock::YellowBlock(
Scene& ParentScene, SDL_Rect Rect)
: Actor{
ParentScene,
SDL_Rect{Rect.x, Rect.y, WIDTH, HEIGHT},
ParentScene.GetAssets().YellowBlock} {}
#pragma once
#include <SDL.h>
#include <memory>
#include <vector>
#include "Actor.h"
namespace Editor {
class Scene;
using ActorPtr = std::unique_ptr<Actor>;
using ActorPtrs = std::vector<ActorPtr>;
class Level {
public:
Level(Scene& ParentScene)
: ParentScene{ParentScene} {
Load();
}
void HandleEvent(const SDL_Event& E);
void HandleDrop(Actor* DragActor);
void Tick(float DeltaTime);
void Render(SDL_Surface* Surface);
bool HasMouseFocus() const;
void AddToLevel(ActorPtr NewActor);
SDL_Point SnapToGridPosition(int x, int y);
void DeleteAtPosition(
int x, int y, const Actor* Unless);
void Load();
void Save();
void SaveAndPlay();
private:
Scene& ParentScene;
ActorPtrs Actors;
Actor* SelectedActor{nullptr};
int LoadedLevel{1};
SDL_Rect Rect{
0, 0,
Config::Editor::LEVEL_WIDTH,
Config::Editor::LEVEL_HEIGHT
};
};
}
// Editor/Source/Level.cpp
// ...
#include <ranges>
#include <format>
#include "Editor/Level.h"
#include <functional>
#include "Editor/Scene.h"
using namespace Editor;
void Level::HandleEvent(const SDL_Event& E) {
using namespace std::views;
using namespace UserEvents;
for (ActorPtr& A : reverse(Actors)) {
if (A->HandleEvent(E)) {
break;
}
}
if (
E.type == SDL_MOUSEBUTTONDOWN &&
E.button.button == SDL_BUTTON_LEFT
) {
SelectedActor = nullptr;
} else if (
E.type == SDL_KEYDOWN &&
E.key.keysym.sym == SDLK_DELETE &&
SelectedActor
) {
std::erase_if(Actors,
[&](ActorPtr& Actor){
return Actor.get() == SelectedActor;
}
);
SelectedActor = nullptr;
} else if (E.type == LOAD_LEVEL) {
LoadedLevel = E.user.code;
Load();
} else if (E.type == SAVE_LEVEL) {
Save();
} else if (E.type == SAVE_AND_PLAY_LEVEL) {
SaveAndPlay();
}
}
void Level::Tick(float DeltaTime) {
for (ActorPtr& A : Actors) {
A->Tick(DeltaTime);
}
}
void Level::Render(SDL_Surface* Surface) {
auto [r, g, b, a]{
Config::Editor::LEVEL_BACKGROUND
};
SDL_FillRect(Surface, &Rect, SDL_MapRGB(
Surface->format, r, g, b));
for (ActorPtr& A : Actors) {
if (SelectedActor &&
A.get() == SelectedActor &&
SelectedActor->GetIsVisible()
) {
auto [x, y, w, h]{
SelectedActor->GetRect()
};
SDL_Rect Rect{x - 1, y - 1, w + 2, h + 2};
SDL_FillRect(Surface, &Rect, SDL_MapRGB(
Surface->format, 255, 255, 255)
);
}
A->Render(Surface);
}
}
void Level::HandleDrop(Actor* DragActor) {
DragActor->SetIsVisible(true);
if (!HasMouseFocus()) {
return;
}
int MouseX, MouseY;
SDL_GetMouseState(&MouseX, &MouseY);
auto [GridX, GridY]{
SnapToGridPosition(
MouseX,
MouseY
)
};
DeleteAtPosition(GridX, GridY, DragActor);
using enum ActorLocation;
if (DragActor->GetLocation() == Menu) {
ActorPtr NewActor{DragActor->Clone()};
NewActor->SetPosition(GridX, GridY);
SelectedActor = NewActor.get();
AddToLevel(std::move(NewActor));
} else {
DragActor->SetPosition(GridX, GridY);
SelectedActor = DragActor;
}
SDL_Event E{UserEvents::LEVEL_EDIT};
SDL_PushEvent(&E);
}
void Level::AddToLevel(ActorPtr NewActor) {
NewActor->SetLocation(ActorLocation::Level);
Actors.push_back(std::move(NewActor));
}
bool Level::HasMouseFocus() const {
if (!ParentScene.HasMouseFocus()) {
return false;
}
int x, y;
SDL_GetGlobalMouseState(&x, &y);
auto [WinX, WinY]{
ParentScene.GetWindow().GetPosition()
};
if (
x >= WinX + Config::Editor::LEVEL_WIDTH ||
y >= WinY + Config::Editor::LEVEL_HEIGHT
) {
return false;
}
return true;
}
SDL_Point Level::SnapToGridPosition(
int x, int y
) {
using namespace Config::Editor;
int sx{HORIZONTAL_GRID_SNAP};
int sy{VERTICAL_GRID_SNAP};
return {
(x / sx) * sx,
(y / sy) * sy,
};
}
void Level::DeleteAtPosition(
int x, int y, const Actor* Unless
) {
std::erase_if(Actors,
[&](const ActorPtr& Actor){
auto [ax, ay]{Actor->GetPosition()};
return
ax == x &&
ay == y &&
Actor.get() != Unless;
}
);
}
namespace{
using namespace Config;
using ActorLoaderFunc = std::function<
std::unique_ptr<Actor>(SDL_RWops*, Scene&)>;
std::vector<ActorLoaderFunc> ActorLoaders{
{},
BlueBlock::Construct,
GreenBlock::Construct,
CyanBlock::Construct,
OrangeBlock::Construct,
RedBlock::Construct,
YellowBlock::Construct,
};
}
void Level::Load() {
Actors.clear();
std::string FileName{std::format(
"Assets/Level{}.bin", LoadedLevel
)};
SDL_RWops* Handle{SDL_RWFromFile(
FileName.c_str(), "rb")};
if (!Handle) {
CheckSDLError("Loading Level");
return;
}
Uint8 FileVersion{SDL_ReadU8(Handle)};
if (FileVersion != VERSION) {
// This file is from a different version of
// our software - react as needed
}
Uint8 GridWidth{SDL_ReadU8(Handle)};
Uint8 GridHeight{SDL_ReadU8(Handle)};
Uint32 ActorCount{SDL_ReadLE32(Handle)};
std::cout << std::format( "Loading a version "
"{} level ({}x{}) with {} actors\n",
FileVersion, GridWidth,
GridHeight, ActorCount
);
for (size_t i{0}; i < ActorCount; ++i) {
Uint8 ActorType{SDL_ReadU8(Handle)};
auto Loader{ActorLoaders[ActorType]};
if (Loader) {
ActorPtr NewActor{Loader(Handle, ParentScene)};
NewActor->SetLocation(ActorLocation::Level);
AddToLevel(std::move(NewActor));
} else {
std::cout << "Error: No Loader for Actor "
"Type " << ActorType << '\n';
}
}
SDL_RWclose(Handle);
}
void Level::Save() {
std::string FileName{
std::format("Assets/Level{}.bin", LoadedLevel)
};
SDL_RWops* Handle{SDL_RWFromFile(
FileName.c_str(), "wb")
};
if (!Handle) {
CheckSDLError("Saving Level");
Handle = SDL_RWFromFile(
FileName.c_str(), "w+b"
);
}
using namespace Config::Editor;
SDL_WriteU8(Handle, VERSION);
SDL_WriteU8(Handle, GRID_WIDTH);
SDL_WriteU8(Handle, GRID_HEIGHT);
SDL_WriteLE32(Handle, Actors.size());
for (const ActorPtr& A : Actors) {
A->Serialize(Handle);
}
SDL_RWclose(Handle);
}
void Level::SaveAndPlay() {
Save();
SDL_Event E{UserEvents::LAUNCH_LEVEL};
E.user.code = LoadedLevel;
SDL_PushEvent(&E);
}
The remaining project files, which were not changed in this lesson, are available below:
#include "Editor/Actor.h"
#include "Editor/Scene.h"
using namespace Editor;
bool Actor::HandleEvent(const SDL_Event& E) {
if (
E.type == SDL_MOUSEBUTTONDOWN &&
E.button.button == SDL_BUTTON_LEFT &&
HasMouseFocus()
) {
DragOffset.x = E.button.x - Rect.x;
DragOffset.y = E.button.y - Rect.y;
if (Location != ActorLocation::Menu) {
SetIsVisible(false);
}
SDL_Event DragEvent{UserEvents::ACTOR_DRAG};
DragEvent.user.data1 = this;
SDL_PushEvent(&DragEvent);
return true;
}
return false;
}
bool Actor::HasMouseFocus() const {
if (!ParentScene.HasMouseFocus()) {
return false;
}
int x, y;
SDL_GetMouseState(&x, &y);
if (
x < Rect.x ||
x > Rect.x + Rect.w ||
y < Rect.y ||
y > Rect.y + Rect.h
) { return false; }
return true;
}
#include "Editor/ActorTooltip.h"
#include "Editor/Scene.h"
using namespace Editor;
ActorTooltip::ActorTooltip(Scene& ParentScene)
: ParentScene{ParentScene} {
SDLWindow = SDL_CreateWindow(
"Tooltip",
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
100,
100,
SDL_WINDOW_HIDDEN
| SDL_WINDOW_TOOLTIP
| SDL_WINDOW_BORDERLESS
| SDL_WINDOW_SKIP_TASKBAR
| SDL_WINDOW_ALWAYS_ON_TOP
);
CheckSDLError("Creating Tooltip Window");
DenyCursor = SDL_CreateSystemCursor(
SDL_SYSTEM_CURSOR_NO
);
CheckSDLError("Creating DenyCursor");
}
ActorTooltip::~ActorTooltip() {
if (!SDL_WasInit(SDL_INIT_VIDEO)) return;
if (SDLWindow) {
SDL_DestroyWindow(SDLWindow);
}
if (DenyCursor) {
SDL_FreeCursor(DenyCursor);
}
}
void ActorTooltip::Render() {
if (!isVisible) return;
DragActor->GetArt().Render(
GetSurface(),
SDL_Rect{
0, 0,
DragActor->GetRect().w,
DragActor->GetRect().h
});
SDL_UpdateWindowSurface(SDLWindow);
}
void ActorTooltip::Tick(float DeltaTime) {
if (!isVisible) return;
auto Buttons{
SDL_GetGlobalMouseState(
nullptr, nullptr)};
if (!(Buttons & SDL_BUTTON_LEFT)) {
SetIsVisible(false);
ParentScene.GetLevel().HandleDrop(DragActor);
} else {
PositionWindow();
}
}
void ActorTooltip::PositionWindow() {
int x, y;
SDL_GetGlobalMouseState(&x, &y);
if (ParentScene.GetLevel().HasMouseFocus()) {
SDL_SetWindowOpacity(SDLWindow, 1);
SDL_SetCursor(SDL_GetDefaultCursor());
auto [WinX, WinY]{
ParentScene.GetWindow().GetPosition()
};
auto [GridX, GridY]{
ParentScene.GetLevel().SnapToGridPosition(
x - WinX, y - WinY
)
};
SDL_SetWindowPosition(
SDLWindow, WinX + GridX, WinY + GridY
);
} else {
auto [DragOffsetX, DragOffsetY]{
DragActor->GetDragOffset()
};
SDL_SetWindowPosition(
SDLWindow, x - DragOffsetX, y - DragOffsetY
);
SDL_SetWindowOpacity(SDLWindow, 0.5);
SDL_SetCursor(DenyCursor);
}
}
void ActorTooltip::HandleEvent(
const SDL_Event& E) {
using namespace UserEvents;
if (E.type == ACTOR_DRAG) {
DragActor = static_cast<Actor*>(
E.user.data1
);
SDL_SetWindowSize(
SDLWindow,
DragActor->GetRect().w,
DragActor->GetRect().h
);
SetIsVisible(true);
}
}
void ActorTooltip::SetIsVisible(bool Visible) {
isVisible = Visible;
if (isVisible) {
SDL_ShowWindow(SDLWindow);
} else {
SDL_HideWindow(SDLWindow);
SDL_SetCursor(SDL_GetDefaultCursor());
SDL_SetWindowOpacity(SDLWindow, 1);
}
}
#include "Editor/Button.h"
#include "Editor/Scene.h"
using namespace Editor;
void Button::HandleEvent(const SDL_Event& E) {
using enum ButtonState;
if (E.type == SDL_MOUSEBUTTONDOWN &&
E.button.button == SDL_BUTTON_LEFT &&
State == Hover
) {
HandleLeftClick();
} else if (
E.type == SDL_MOUSEMOTION &&
ParentScene.HasMouseFocus()
) {
SDL_Point Pos{E.motion.x, E.motion.y};
bool Hovering(SDL_PointInRect(&Pos, &Rect));
if (State == Normal && Hovering) {
State = Hover;
} else if (State == Hover && !Hovering) {
State = Normal;
}
}
}
void Button::Render(SDL_Surface* Surface) {
using namespace Config;
auto [r, g, b, a]{
BUTTON_COLORS[static_cast<int>(State)]};
SDL_FillRect(Surface, &Rect, SDL_MapRGB(
Surface->format, r, g, b
));
ButtonText.Render(Surface, &Rect);
}
#pragma once
#include <SDL.h>
#include "Actor.h"
namespace Editor{
class Scene;
class ActorTooltip {
public:
ActorTooltip(Scene& ParentScene);
~ActorTooltip();
ActorTooltip(const ActorTooltip&) = delete;
ActorTooltip& operator=(const ActorTooltip&)
= delete;
void Render();
void Tick(float DeltaTime);
void PositionWindow();
void HandleEvent(const SDL_Event& E);
void SetIsVisible(bool NewVisibility);
SDL_Surface* GetSurface() const {
return SDL_GetWindowSurface(SDLWindow);
}
private:
bool isVisible{false};
SDL_Window* SDLWindow{nullptr};
Actor* DragActor{nullptr};
Scene& ParentScene;
SDL_Cursor* DenyCursor{nullptr};
};
}
#pragma once
#include "Image.h"
namespace Editor {
struct AssetManager {
Image BlueBlock{"Assets/Brick_Blue_A.png"};
Image GreenBlock{"Assets/Brick_Green_A.png"};
Image CyanBlock{"Assets/Brick_Cyan_A.png"};
Image OrangeBlock{"Assets/Brick_Orange_A.png"};
Image RedBlock{"Assets/Brick_Red_A.png"};
Image YellowBlock{"Assets/Brick_Yellow_A.png"};
};
}
#pragma once
#include <SDL.h>
#include <string>
#include "Text.h"
enum class ButtonState {
Normal = 0,
Hover = 1,
Active = 2,
Disabled = 3
};
namespace Editor{
class Scene;
class Button {
public:
Button(
Scene& ParentScene,
const std::string& Text,
SDL_Rect Rect
) : ButtonText{Text, 20},
Rect{Rect},
ParentScene{ParentScene} {}
virtual void HandleLeftClick() {}
void HandleEvent(const SDL_Event& E);
void Render(SDL_Surface* Surface);
void Tick(float DeltaTime) {}
ButtonState GetState() const {
return State;
}
void SetState(ButtonState NewState) {
State = NewState;
}
private:
Scene& ParentScene;
ButtonState State{ButtonState::Normal};
Text ButtonText;
SDL_Rect Rect;
};
}
#pragma once;
#include "Button.h"
namespace Editor{
using namespace Config::Editor;
class LoadLevelButton : public Button {
public:
constexpr static int WIDTH{40};
LoadLevelButton(Scene& ParentScene, int Level)
: Level{Level},
Button {
ParentScene,
std::to_string(Level),
{
PADDING * Level + WIDTH * (Level-1),
LEVEL_HEIGHT + PADDING,
WIDTH, 30
}
}
{
if (Level == 1) {
SetState(ButtonState::Active);
}
}
void HandleEvent(const SDL_Event& E) {
Button::HandleEvent(E);
if (E.type == UserEvents::LOAD_LEVEL) {
if (E.user.code == Level) {
SetState(ButtonState::Active);
} else {
SetState(ButtonState::Normal);
}
}
}
void HandleLeftClick() override {
SDL_Event E{UserEvents::LOAD_LEVEL};
E.user.code = Level;
SDL_PushEvent(&E);
}
private:
int Level;
};
class PlayLevelButton : public Button {
public:
constexpr static int WIDTH{70};
PlayLevelButton(Scene& ParentScene)
: Button {
ParentScene,
"PLAY",
{
LEVEL_WIDTH - WIDTH - PADDING,
LEVEL_HEIGHT + PADDING,
WIDTH, 30
}
}
{}
void HandleLeftClick() override {
using namespace UserEvents;
SDL_Event E{SAVE_AND_PLAY_LEVEL};
SDL_PushEvent(&E);
}
};
class SaveLevelButton : public Button {
public:
constexpr static int WIDTH{70};
SaveLevelButton(Scene& ParentScene)
: Button {
ParentScene,
"SAVE",
{
LEVEL_WIDTH - WIDTH
- PlayLevelButton::WIDTH - PADDING * 2,
LEVEL_HEIGHT + PADDING,
WIDTH, 30
}
}
{
SetState(ButtonState::Disabled);
}
void HandleEvent(const SDL_Event& E) {
Button::HandleEvent(E);
using namespace UserEvents;
if (E.type == LEVEL_EDIT) {
SetState(ButtonState::Normal);
} else if (
E.type == LOAD_LEVEL ||
E.type == SAVE_LEVEL ||
E.type == SAVE_AND_PLAY_LEVEL
) {
SetState(ButtonState::Disabled);
}
}
void HandleLeftClick() override {
SDL_Event E{UserEvents::SAVE_LEVEL};
SDL_PushEvent(&E);
}
};
}
#pragma once
#include <SDL.h>
#include <SDL_image.h>
#include <string>
#include "Config.h"
namespace Editor{
class Image {
public:
Image() = default;
Image(const std::string& Path)
: ImageSurface{IMG_Load(Path.c_str())
} {
CheckSDLError("Loading Image");
}
void Render(
SDL_Surface* Surface, SDL_Rect Rect
) const {
SDL_BlitScaled(
ImageSurface, nullptr, Surface, &Rect);
}
Image(Image&& Other) noexcept
: ImageSurface(Other.ImageSurface) {
Other.ImageSurface = nullptr;
}
~Image() {
if (ImageSurface) {
SDL_FreeSurface(ImageSurface);
}
}
Image(const Image&) = delete;
Image& operator=(const Image&) = delete;
private:
SDL_Surface* ImageSurface{nullptr};
};
}
#pragma once
#include <SDL.h>
#include "ActorMenu.h"
#include "ActorTooltip.h"
#include "AssetManager.h"
#include "Level.h"
#include "Window.h"
#include "EditorButtons.h"
namespace Editor{
class Scene {
public:
Scene(Window& ParentWindow)
: ParentWindow{ParentWindow}
{}
void HandleEvent(const SDL_Event& E) {
ActorShelf.HandleEvent(E);
TooltipWindow.HandleEvent(E);
CurrentLevel.HandleEvent(E);
LoadButton1.HandleEvent(E);
LoadButton2.HandleEvent(E);
LoadButton3.HandleEvent(E);
SaveButton.HandleEvent(E);
PlayButton.HandleEvent(E);
}
void Tick(float DeltaTime) {
ActorShelf.Tick(DeltaTime);
TooltipWindow.Tick(DeltaTime);
CurrentLevel.Tick(DeltaTime);
LoadButton1.Tick(DeltaTime);
LoadButton2.Tick(DeltaTime);
LoadButton3.Tick(DeltaTime);
SaveButton.Tick(DeltaTime);
PlayButton.Tick(DeltaTime);
}
void Render(SDL_Surface* Surface) {
ActorShelf.Render(Surface);
TooltipWindow.Render();
CurrentLevel.Render(Surface);
LoadButton1.Render(Surface);
LoadButton2.Render(Surface);
LoadButton3.Render(Surface);
SaveButton.Render(Surface);
PlayButton.Render(Surface);
}
AssetManager& GetAssets() {
return Assets;
}
bool HasMouseFocus() const {
return ParentWindow.HasMouseFocus();
}
Window& GetWindow() const {
return ParentWindow;
}
Level& GetLevel() {
return CurrentLevel;
}
private:
ActorMenu ActorShelf{*this};
ActorTooltip TooltipWindow{*this};
Level CurrentLevel{*this};
Window& ParentWindow;
AssetManager Assets;
LoadLevelButton LoadButton1{*this, 1};
LoadLevelButton LoadButton2{*this, 2};
LoadLevelButton LoadButton3{*this, 3};
SaveLevelButton SaveButton{*this};
PlayLevelButton PlayButton{*this};
};
}
#pragma once
#include <SDL.h>
#include <SDL_ttf.h>
#include <string>
#include "Config.h"
namespace Editor{
class Text {
public:
Text(
const std::string& InitialText,
int FontSize
) : Content(InitialText) {
Font = TTF_OpenFont(
Config::FONT.c_str(), FontSize);
CheckSDLError("Opening Font");
SetText(InitialText);
}
~Text() {
if (!SDL_WasInit(SDL_INIT_VIDEO)) {
return;
}
if (TextSurface) {
SDL_FreeSurface(TextSurface);
}
if (Font) {
TTF_CloseFont(Font);
}
}
Text(const Text&) = delete;
Text& operator=(const Text&) = delete;
void SetText(const std::string& NewText) {
Content = NewText;
if (TextSurface) {
SDL_FreeSurface(TextSurface);
}
TextSurface = TTF_RenderText_Blended(Font,
Content.c_str(), Config::FONT_COLOR);
CheckSDLError("Creating Text Surface");
}
void Render(
SDL_Surface* Surface, SDL_Rect* Rect
) {
if (!TextSurface) return;
int TextW{TextSurface->w};
int TextH{TextSurface->h};
// Center the text
SDL_Rect Destination {
Rect->x + (Rect->w - TextW) / 2,
Rect->y + (Rect->h - TextH) / 2,
TextW, TextH
};
SDL_BlitSurface(
TextSurface, nullptr,
Surface, &Destination
);
}
private:
std::string Content;
TTF_Font* Font{nullptr};
SDL_Surface* TextSurface{nullptr};
};
}
#pragma once
#include <SDL.h>
#include "Config.h"
namespace Editor{
class Window {
public:
Window() {
SDLWindow = SDL_CreateWindow(
Config::Editor::WINDOW_TITLE.c_str(),
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
Config::Editor::WINDOW_WIDTH,
Config::Editor::WINDOW_HEIGHT,
0
);
CheckSDLError("Creating Editor Window");
}
~Window() {
if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
SDL_DestroyWindow(SDLWindow);
}
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
void Render() {
auto [r, g, b, a]{
Config::Editor::WINDOW_BACKGROUND
};
SDL_FillRect(
GetSurface(), nullptr,
SDL_MapRGB(GetSurface()->format, r, g, b));
}
void Update() {
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface() const {
return SDL_GetWindowSurface(SDLWindow);
}
bool HasMouseFocus() const {
int x, y, w, h, MouseX, MouseY;
SDL_GetWindowPosition(SDLWindow, &x, &y);
SDL_GetWindowSize(SDLWindow, &w, &h);
SDL_GetGlobalMouseState(&MouseX, &MouseY);
if (
MouseX < x ||
MouseX > x + w ||
MouseY < y ||
MouseY > y + h
) {
return false;
}
return true;
}
SDL_Point GetPosition() const {
int x, y;
SDL_GetWindowPosition(SDLWindow, &x, &y);
return {x, y};
}
SDL_Point GetSize() const {
int x, y;
SDL_GetWindowSize(SDLWindow, &x, &y);
return {x, y};
}
private:
SDL_Window* SDLWindow{nullptr};
};
}
#include <SDL.h>
#include <SDL_image.h>
#include <SDL_ttf.h>
#include "Config.h"
#ifdef WITH_EDITOR
#include "Editor/Scene.h"
#include "Editor/Window.h"
#endif
int main(int argc, char** argv) {
SDL_Init(SDL_INIT_VIDEO);
CheckSDLError("SDL_Init");
IMG_Init(IMG_INIT_PNG);
CheckSDLError("IMG_Init");
TTF_Init();
CheckSDLError("TTF_Init");
#ifdef WITH_EDITOR
Editor::Window EditorWindow;
Editor::Scene EditorScene{EditorWindow};
#endif
Uint64 LastTick{SDL_GetPerformanceCounter()};
SDL_Event E;
while (true) {
while (SDL_PollEvent(&E)) {
#ifdef WITH_EDITOR
EditorScene.HandleEvent(E);
#endif
if (
E.type == SDL_QUIT ||
E.type == SDL_WINDOWEVENT &&
E.window.event == SDL_WINDOWEVENT_CLOSE
) {
TTF_Quit();
IMG_Quit();
SDL_Quit();
return 0;
}
}
Uint64 CurrentTick{SDL_GetPerformanceCounter()};
float DeltaTime{
static_cast<float>(CurrentTick - LastTick) /
SDL_GetPerformanceFrequency()
};
LastTick = CurrentTick;
#ifdef WITH_EDITOR
EditorScene.Tick(DeltaTime);
EditorWindow.Render();
EditorScene.Render(EditorWindow.GetSurface());
EditorWindow.Update();
#endif
}
return 0;
}
cmake_minimum_required(VERSION 3.16)
set(CMAKE_CXX_STANDARD 20)
project(Editor VERSION 1.0.0)
add_executable(Editor
"main.cpp"
"Editor/Source/Button.cpp"
"Editor/Source/Blocks.cpp"
"Editor/Source/Actor.cpp"
"Editor/Source/ActorTooltip.cpp"
"Editor/Source/Level.cpp"
)
target_compile_definitions(
Editor PUBLIC
WITH_EDITOR
CHECK_ERRORS
)
target_include_directories(
Editor PUBLIC ${PROJECT_SOURCE_DIR}
)
add_subdirectory(external/SDL)
add_subdirectory(external/SDL_image)
add_subdirectory(external/SDL_ttf)
target_link_libraries(Editor PRIVATE
SDL2
SDL2_image
SDL2_ttf
)
if (WIN32)
target_link_libraries(
Editor PRIVATE SDL2main
)
endif()
set(AssetDirectory "${PROJECT_SOURCE_DIR}/Assets")
add_custom_command(
TARGET Editor POST_BUILD
COMMAND
${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:SDL2>"
"$<TARGET_FILE:SDL2_image>"
"$<TARGET_FILE:SDL2_ttf>"
"$<TARGET_FILE_DIR:Editor>"
COMMAND
${CMAKE_COMMAND} -E copy_directory_if_different
"${AssetDirectory}"
"$<TARGET_FILE_DIR:Editor>/Assets"
VERBATIM
)
In this final part of the editor's core implementation, we focused on deserialization – loading the saved level data back into the editor. We implemented the Level::Load()
method, using SDL_RWops
to read the binary file created in the previous lesson.
We emphasized matching the read operations (SDL_ReadU8()
, SDL_ReadLE32()
, etc) to the write operations used during serialization. To handle reconstructing actors of unknown types, we introduced static factory methods (Construct
) within each actor class. These factories read the necessary data from the file handle and returned a unique_ptr<Actor>
. A std::vector<std::function>
was used to map the serialized actor type ID to the corresponding factory function.
Key Takeaways:
SDL_RWFromFile()
.SDL_RWFromFile()
.std::function
allows storing pointers to these static methods for dynamic dispatch.Complete the save/load cycle by implementing level deserialization using SDL_RWops and actor factories.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games