Large games tend to be complex projects, developed over many years. Because of this, developers often spend as much time building the tools that help build the game as they do building anything that players will directly see.
In this project, we apply the techniques we learned in the previous few chapters around window and mouse management and serialization and deserialization to create a basic level editor tool
On the right of our tool, we’ll have a menu where users have a list of objects - or "actors" - they can choose from. They can drag and drop actors from this menu onto the level on the left to add instances of them to the level. The footer will also allow them to save levels to their hard drive, and load levels they previously saved
The techniques we cover will be applicable to a huge range of tools but, as the project continues, we’ll direct it more towards creating levels for a breakout game. Later in the course, we’ll add the game that loads and plays these levels
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.
We've built the basic structure and the actor menu. Now, let's implement the ability to interact with the actors in that menu. The goal is to allow users to click and drag an actor type to eventually place it in the level.
This lesson covers the first part of drag-and-drop: initiating the drag and providing visual feedback. We'll modify our Actor
class to recognize when it's been clicked.
Upon detecting a click, the Actor
will dispatch a custom SDL_Event
. We'll use SDL_RegisterEvents()
to define this event type.
Then, we'll introduce a new ActorTooltip
class. This class will manage a second, specialized SDL_Window
. This window will appear when the custom drag event is received, display the image of the actor being dragged, and follow the mouse cursor faithfully using SDL_GetGlobalMouseState()
and SDL_SetWindowPosition()
.
We'll carefully select window flags to ensure it behaves like a proper tooltip.
The drag-and-drop mechanism is halfway there; we can pick up actors, but they vanish when released. Let's build the "drop" part.
First, we'll define a Level
class. This class represents the main canvas of our editor, responsible for holding and rendering the actors that make up the game level. It will have its own bounds and background.
Next, we'll connect the dragging action to the level. When the user releases the mouse button while dragging (monitored by ActorTooltip
), we need to check if the drop occurs over the Level
. If it does, we'll create a duplicate of the dragged actor. This requires adding a Clone()
capability to our Actor
class hierarchy using virtual
functions. The newly cloned actor is then positioned and added to the Level
's collection.
We'll also implement feedback mechanisms. If the user drags the actor outside the valid level area, the tooltip will become semi-transparent, and the mouse cursor will change to indicate the drop won't work there.
We've got actors into our level, but now we need to manage them. This lesson builds on the drag-and-drop foundation to allow moving, selecting, and deleting actors within the level canvas.
First, we'll differentiate between dragging a new actor from the menu (which creates a copy) and dragging an existing actor in the level (which should move it). We'll introduce a way for actors to know their location (Menu
or Level
).
Next, we'll implement selection:
Lastly, we'll hook up the delete key. When an actor is selected, pressing Delete will remove it from the level, requiring updates to event handling and the actor container.
In the previous lessons, we built the core functionality for dragging, dropping, selecting, and deleting actors in our level editor. Currently, actors can be placed anywhere on the level canvas, overlapping freely. While this works for some game styles, many benefit from more structured placement.
This lesson introduces grid-based positioning. First, we'll implement grid snapping. When dragging or dropping an actor within the level, its position will automatically "snap" to the nearest grid line. This helps designers align objects precisely without tedious manual adjustments.
Then, we'll take it a step further and transform our editor into a true cell-based system. We'll modify the logic so that only one actor can occupy any given grid cell at a time. Dropping an actor onto an already occupied cell will replace the existing actor.
By the end, you'll understand:
In the previous lessons, we've built the core mechanics of our level editor: creating a window, managing actors, implementing drag-and-drop with grid snapping, and handling selection and deletion. However, any level we design currently vanishes the moment we close the application.
This lesson tackles persistence. We'll add controls to the editor's footer buttons to save the current level layout to a file and buttons to load previously saved levels. This involves defining new custom SDL events to signal these actions.
We'll then implement binary serialization using SDL's SDL_RWops
structure and related functions (SDL_WriteU8()
, SDL_WriteLE32()
, etc.).
We'll carefully design a binary format for our level files, deciding what data needs saving (version, grid size, actor types, positions) and how to represent it efficiently.
Finally, we'll implement the Save()
logic within our Level
class, writing the editor's state to a file according to our defined format. We'll also extend actors with a virtual Serialize()
method, allowing different actor types to save their specific data, preparing the groundwork for loading in the next lesson.
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.
Our entities can now detect collisions thanks to the CollisionComponent
, but they still pass through each other like ghosts. This lesson bridges the gap from detection to reaction. We'll implement two fundamental collision responses:
We'll approach this by adding a virtual HandleCollision()
function to our Entity()
base class, allowing different entity types to define their unique reactions.
Building on our basic SDL window setup, this lesson introduces interactivity by focusing on mouse input.
We'll explore how SDL represents mouse actions through its event system. You'll learn to detect when the user moves the mouse, clicks buttons, or even moves the cursor into or out of the application window.
Key topics include:
SDL_MOUSEMOTION
events to get cursor coordinates.SDL_MOUSEBUTTONDOWN
, SDL_MOUSEBUTTONUP
).SDL_WINDOWEVENT_ENTER
, SDL_WINDOWEVENT_LEAVE
).