In the previous lesson, we set up the foundational classes for our level editor: the main window, the scene manager, and helper classes for images, text, buttons, and assets. We now have a blank window ready to be populated.
This lesson builds upon that foundation by adding the first major UI component: the Actor Menu. This menu will serve as a palette on the right side of the editor, displaying all the available objects (Actors) that users can place into their levels.
We'll create a dedicated ActorMenu class to manage this area's rendering and logic. We will also define a base Actor
class, which will represent any object that can exist in our level, like blocks, enemies, or power-ups.
Finally, we'll implement our first concrete Actor
type – a simple blue block – and add an instance of it to the ActorMenu
, making it visible and ready for interaction in later steps.
ActorMenu
ClassThe right side of our editor will contain a list of available "Actors" (game objects) that can be placed in the level. Let's create a dedicated class, ActorMenu
, to manage this UI element.
As with our Scene
and other core components, it will follow the standard pattern of having HandleEvent()
, Tick()
, and Render()
methods. It also needs access to the main Scene
it belongs to, so we'll store a reference to the ParentScene
.
// Editor/ActorMenu.h
#pragma once
#include <SDL.h>
namespace Editor {
class Scene;
class ActorMenu {
public:
ActorMenu(Scene& ParentScene)
: ParentScene{ParentScene}
{}
void HandleEvent(const SDL_Event& E) {}
void Tick(float DeltaTime) {}
void Render(SDL_Surface* Surface) {}
Scene& GetScene() const {
return ParentScene;
}
private:
Scene& ParentScene;
};
}
Now that we have the ActorMenu
class defined, we need to create an instance of it within our Editor::Scene
. The Scene
will own and manage the ActorMenu
.
We'll add an ActorMenu
member variable to the Scene
class, passing *this
(a reference to the Scene
itself) to the ActorMenu
constructor.
We also need to update the Scene
's HandleEvent()
, Tick()
, and Render()
methods to delegate calls to the corresponding methods in the ActorMenu
instance.
// Editor/Scene.h
#pragma once
#include <SDL.h>
#include "ActorMenu.h"
#include "AssetManager.h"
#include "Window.h"
namespace Editor{
class Scene {
public:
Scene(Window& ParentWindow)
: ParentWindow{ParentWindow}
{}
void HandleEvent(const SDL_Event& E) {
ActorShelf.HandleEvent(E);
}
void Tick(float DeltaTime) {
ActorShelf.Tick(DeltaTime);
}
void Render(SDL_Surface* Surface) {
ActorShelf.Render(Surface);
}
// ...
private:
ActorMenu ActorShelf{*this};
// ...
};
}
ActorMenu
With the ActorMenu
integrated into the Scene
, let's make it visible. We need to define its appearance and position within the editor window.
We'll add some constants to our Config.h
file, specifically within the Config::Editor
namespace. These will define the menu's width, its X-coordinate (anchored to the right edge of the window), its background color, and some padding for spacing elements inside it later.
// Config.h
#pragma once
// ...
#ifdef WITH_EDITOR
namespace Config::Editor {
// Window
// ...
// ActorMenu
inline const int ACTOR_MENU_WIDTH{70};
inline const int ACTOR_MENU_POSITION_X{
WINDOW_WIDTH - ACTOR_MENU_WIDTH};
inline const SDL_Color ACTOR_MENU_BACKGROUND{
15, 15, 15, 255};
inline const int PADDING{10};
}
#endif
// ...
Time to implement ActorMenu::Render(). This function needs to draw the visual representation of the menu panel. We'll start with just the background.
Using the ACTOR_MENU_BACKGROUND
color and the position/size defined by ACTOR_MENU_POSITION_X
, ACTOR_MENU_WIDTH
, and WINDOW_HEIGHT
from Config::Editor
, we call SDL_FillRect()
. To avoid recalculating the menu's rectangle every frame, we'll store it in a private SDL_Rect
member variable initialized once.
// Editor/ActorMenu.h
#pragma once
#include <SDL.h>
#include "Config.h"
namespace Editor {
// ...
class ActorMenu {
public:
// ...
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)
);
}
private:
Scene& ParentScene;
SDL_Rect Rect{
Config::Editor::ACTOR_MENU_POSITION_X, 0,
Config::Editor::ACTOR_MENU_WIDTH,
Config::Editor::WINDOW_HEIGHT
};
};
}
We should now see our menu rendered at the side of our application, using the width and color defined within our configuration file:
Actor
ClassesOur editor will handle various types of objects that can be placed in the level – blocks, enemies, items, etc. We'll call these "Actors". To manage them effectively, we need a common base class, Actor
.
This Actor
class will define the fundamental properties and interface shared by all actor types, such as position (SDL_Rect
), visual representation (Image&
), and core methods like HandleEvent()
, Tick()
, and Render()
.
Because Actor
needs a reference to the Scene
but Scene
will eventually hold Actor
s (via ActorMenu
), we must forward-declare Scene
in Actor.h
to prevent a circular include dependency.
// Editor/Actor.h
#pragma once
#include <SDL.h>
#include "Image.h"
namespace Editor {
class Scene;
class Actor {
public:
Actor(
Scene& ParentScene,
const SDL_Rect& Rect,
Image& Image
) : ParentScene{ParentScene},
Rect{Rect},
Art{Image}
{}
virtual void HandleEvent(const SDL_Event& E){}
void Tick(float DeltaTime) {}
void Render(SDL_Surface* Surface) {
Art.Render(Surface, Rect);
}
protected:
Scene& ParentScene;
SDL_Rect Rect;
Image& Art;
};
}
Let's create our first usable Actor
. We'll define a BlueBlock
class in Editor/Blocks.h
that inherits from our base Actor
class.
We’ll store this in a file called "Blocks" rather than something like "BlueBlock", as we’ll have many block types in the future, and their classes will be quite small so we can just define them all in a single file. However, it would be equally valid to separate our block types into their own files - it’s just a question of personal preference.
This BlueBlock
class represents a simple blue brick. We'll give it fixed dimensions using constexpr static
members. The constructor needs to initialize the base Actor
part, including fetching the "BlueBlock" image from the AssetManager
via the Scene
.
This interaction necessitates the full Scene
definition. To avoid issues with include order and dependencies, we declare the BlueBlock
constructor in the .h file but defer its implementation to Editor/Source/Blocks.cpp
, where including Editor/Scene.h
is straightforward.
// Editor/Blocks.h
#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
);
};
}
In the new Editor/Source/Blocks.cp
p file, we provide the definition for BlueBlock::BlueBlock
(that is, the constructor). Make sure to include Editor/Scene.h
here.
The constructor's main task is to initialize the base Actor
class correctly, passing along the scene, setting the block's rectangle dimensions using its static constants, and getting the corresponding Image
reference from the AssetManager
.
// Editor/Source/Blocks.cpp
#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
}
{}
Our ActorMenu
needs to store the different Actor
types that the user can select. Since these will be instances of classes derived from Actor
(like BlueBlock
), we need to store them in a way that handles polymorphism.
We'll use a std::vector
to hold pointers to Actor
objects. To manage memory automatically and correctly handle ownership, we'll use std::unique_ptr<Actor>
. We add using aliases (ActorPtr
, ActorPtrs
) for clarity and declare a std::vector<ActorPtr>
named Actors
as a member of ActorMenu
.
// Editor/ActorMenu.h
// ...
#include <vector>
#include <memory>
#include "Actor.h"
namespace Editor {
class Scene;
using ActorPtr = std::unique_ptr<Actor>;
using ActorPtrs = std::vector<ActorPtr>;
class ActorMenu {
public:
// ...
private:
ActorPtrs Actors;
// ...
};
}
Now that ActorMenu
has a container for Actors
, we need to make sure these actors participate in the application loop. We must iterate through the Actors
vector in the ActorMenu
's HandleEvent()
, Tick()
, and Render()
methods.
In each of these methods, we add a range-based for loop that goes through each ActorPtr
in the Actors
vector and calls the corresponding method - HandleEvent()
, Tick()
, and Render()
- on the pointed-to Actor
object.
Note that rendering actors should happen after rendering the menu's background, to ensure they get rendered on top of the background.
// Editor/ActorMenu.h
// ...
namespace Editor {
// ...
class ActorMenu {
public:
// ...
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);
}
}
private:
ActorPtrs Actors;
// ...
};
}
We have the ActorMenu
, the Actor
base class, the BlueBlock
derived class, and the mechanism to store and process actors. Now, let's actually create an instance of BlueBlock
and add it to the ActorMenu
.
We'll do this in the ActorMenu
's constructor.
Using Actors.emplace_back()
and std::make_unique<BlueBlock>()
, we create a new BlueBlock
in dynamic memory, managed by a unique_ptr
. We pass the required Scene
reference using GetScene()
and an initial SDL_Rect
defining its position within the menu, using the PADDING
configuration for spacing.
We’ll set the width and height in the SDL_Rect
to 0
, but the value doesn’t matter as the BlueBlock
constructor will set the correct dimensions itself before forwarding it to the Actor
base constructor.
// Editor/ActorMenu.h
// ...
#include "Blocks.h"
namespace Editor {
// ...
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
}
)
);
}
// ...
};
}
If you run the program after these changes, the blue block should appear inside the actor menu panel.
This confirms that the ActorMenu
is being rendered, the BlueBlock
is being created correctly, added to the Actors
vector, and its Render()
method is being called by loop within the ActorMenu::Render()
function.
We've created several new files and modified existing ones to implement the ActorMenu
and the first Actor
.
Below are the full contents of Editor/ActorMenu.h
, Editor/Actor.h
, Editor/Blocks.h
, and Editor/Source/Blocks.cpp
reflecting the changes made in this lesson.
#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
}
)
);
}
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 <SDL.h>
#include "Image.h"
namespace Editor {
class Scene;
class Actor {
public:
Actor(
Scene& ParentScene,
const SDL_Rect& Rect,
Image& Image
) : ParentScene{ParentScene},
Rect{Rect},
Art{Image}
{}
virtual void HandleEvent(const SDL_Event& E){}
void Tick(float DeltaTime) {}
void Render(SDL_Surface* Surface) {
Art.Render(Surface, Rect);
}
protected:
Scene& ParentScene;
SDL_Rect Rect;
Image& Art;
};
}
#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
);
};
}
#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
} {}
We also made modifications to Editor/Scene.h
to include and manage the ActorMenu
, and to Config.h
to add layout constants. The updated versions of these files are shown below.
#pragma once
#include <SDL.h>
#include "ActorMenu.h"
#include "AssetManager.h"
#include "Window.h"
namespace Editor{
class Scene {
public:
Scene(Window& ParentWindow)
: ParentWindow{ParentWindow}
{}
void HandleEvent(const SDL_Event& E) {
ActorShelf.HandleEvent(E);
}
void Tick(float DeltaTime) {
ActorShelf.Tick(DeltaTime);
}
void Render(SDL_Surface* Surface) {
ActorShelf.Render(Surface);
}
AssetManager& GetAssets() {
return Assets;
}
bool HasMouseFocus() const {
return ParentWindow.HasMouseFocus();
}
Window& GetWindow() const {
return ParentWindow;
}
private:
ActorMenu ActorShelf{*this};
Window& ParentWindow;
AssetManager Assets;
};
}
#pragma once
#include <iostream>
#include <SDL.h>
#include <string>
#include <vector>
namespace Config {
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 {
// Window
inline const std::string WINDOW_TITLE{"Editor"};
inline const int WINDOW_WIDTH{730};
inline const int WINDOW_HEIGHT{300};
inline const SDL_Color WINDOW_BACKGROUND{35, 35, 35, 255};
// ActorMenu
inline const int ACTOR_MENU_WIDTH{70};
inline const int ACTOR_MENU_POSITION_X{
WINDOW_WIDTH - ACTOR_MENU_WIDTH};
inline const SDL_Color ACTOR_MENU_BACKGROUND{
15, 15, 15, 255};
inline const int PADDING{10};
}
#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
}
We've added the first major piece of the editor's user interface: the Actor Menu. Here's what we did:
ActorMenu
Class: Created a new class to manage the menu panel, including its rendering and event handling. Added an instance of it to the Scene
.Config.h
for the menu's width, position, background color, and padding.ActorMenu::Render()
to draw the background rectangle.Actor
Hierarchy: Introduced a base Actor
class for all placeable items, using forward declaration for Scene
to prevent include cycles.BlueBlock
Actor: Created the first concrete Actor
subclass, BlueBlock
, defining its dimensions and implementing its constructor in a separate .cpp file to handle dependencies correctly.std::vector<std::unique_ptr<Actor>>
within ActorMenu
to store and manage actor instances polymorphically and safely.ActorMenu
's methods to process the contained actors and added a BlueBlock
instance to the menu in the constructor.The editor now displays the menu on the right with a blue block ready to be used in future lessons.
This lesson focuses on creating the UI panel for Actors and adding the first concrete Actor type.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games