Level Editor Starting Point

Establish the core structure for our level editor, including window, scene, and asset management.
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Abstract art representing a video game map editor
Level Editor: Part 1
Ryan McCombe
Ryan McCombe
Posted

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

Screenshot showing our completed level editor

In this introduction, we’ll quickly add a lot of files and classes to serve as a starting point. There’s a lot of code here, but it’s all files and techniques we’re already familiar with from previous projects.

We’ll give a quick tour of the starting point in this lesson, and then slow down in future sections as we start to implement newer concepts.

Application Loop

As usual, our starting point is main.cpp, which orchestrates the application's lifecycle in much the same way we did in previous projects. It begins by initializing the necessary SDL subsystems using SDL_Init(), IMG_Init(), and TTF_Init(), along with error checking.

Next, it instantiates the main Editor::Window and the corresponding Editor::Scene. The primary while loop continuously polls for events, calculates frame timing (DeltaTime), updates the editor's state via EditorScene.Tick(), and handles rendering through the EditorWindow and EditorScene.

#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;
}

Preprocessor Definition: WITH_EDITOR

We use preprocessor directives, specifically #ifdef WITH_EDITOR, to manage which parts of the code get compiled. This project structure anticipates having both the editor and the actual game coexist within the same codebase eventually. We’ll add the game as a later project in the course.

The editor itself is a tool for developers or designers, not part of the final product shipped to players. Therefore, we enclose all editor-specific initialization, includes, and logic within these #ifdef blocks.

If the WITH_EDITOR symbol is defined when compiling, the editor code is included. If it's not defined, the compiler skips these sections, resulting in a build that contains only the game logic (once we add it).

Tick Function: DeltaTime

Our main loop calculates DeltaTime, representing the time elapsed between frames, using SDL_GetPerformanceCounter() and SDL_GetPerformanceFrequency(). This value is passed to the EditorScene.Tick() function, which will eventually pass it along to all the objects it is managing.

In this initial stage of the editor project, none of our components actively use DeltaTime for frame-rate independent movement or animation. We're primarily dealing with static UI elements and event-driven interactions.

However, we include the DeltaTime calculation and pass it through our Tick() functions anyway. It anticipates potential future needs, either for custom editor features you might add or for the game logic we'll integrate later, which will require time-based updates.

Quit Event

Our quit detection is a little more elaborate than previous projects, because our level editor will have multiple windows. We’ll have our main window, and a small utility window for displaying tooltips.

By default, SDL only pushes an SDL_QUIT even when every window is closed. Users will have no way to directly close our tooltip window, so we’ve modified our application loop to end when any window is closed. We do this by checking not just for SDL_QUIT events, but also for SDL_WINDOWEVENT_CLOSE events

We covered utility windows, multiple-window applications, and the SDL_WINDOWEVENT_CLOSE in our earlier lesson:

Window Class

The Editor/Window.h file introduces our Window class, designed specifically for the editor's main window. Our Window class is very similar to what we have been using previously. It handles creating the SDL_Window in its constructor and destroying it in the destructor, managing the resource lifecycle.

To keep the editor code distinct and well-organized, we've placed this class (and others related to the editor) inside an Editor namespace. Correspondingly, the header and source files reside in an Editor/ subdirectory within our project structure.

#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};
};
}

Getters

Two helpful additions to the Editor::Window class are the GetPosition() and GetSize() methods. They act as convenient wrappers around the underlying SDL functions SDL_GetWindowPosition() and SDL_GetWindowSize().

This information is exposed because other parts of our editor system, particularly those dealing with mouse input relative to the window or UI element placement, will need access to the window's current state.

They return the requested information packaged within an SDL_Point structure. This built-in SDL type is a basic container for two integers, x and y. We’ll use this type any time a function needs to return two such integers but, of course, we can design our API in any way we want

The HasMouseFocus() Function

In the future, our project will contain multiple main windows - one for the level editor, and one for the game. Because of this, we need a way to check which window certain events, such as SDL_MouseMotionEvents, correspond to.

Ideally, we’d prefer to use the SDL_GetMouseFocus() function to understand which window has mouse focus. However, when we have utility windows such as a tooltip, the tooltip window may have mouse focus rather than our main window.

As such, our HasMouseFocus() window, and some of our future code, uses indirect ways to determine if the mouse is hovering over a specific window.

Preventing a Window from Gaining Mouse Focus

We’d prefer to solve this problem by preventing our utility windows from gaining mouse focus, but this is not an option in SDL2. We’ll suggest some ways of doing this using platform-specific APIs later in the project, when we add our utility window.

Scene Class

The Editor::Scene class acts as the main container and manager for the content displayed within our Editor::Window. It holds references to assets and will eventually manage all the UI elements and actors within the editor.

It includes the standard HandleEvent(), Tick(), and Render() methods, forming the core interface for interacting with the scene from the main application loop. It also holds an AssetManager instance to provide access to shared resources like images.

It also keeps a reference to its parent Window. This allows the scene to query the window for information, such as whether it currently has mouse focus via the scene's own HasMouseFocus() method.

Our scene takes up the full area of its parent window, so if our ParentWindow has mouse focus, then the scene inherently also has mouse focus. As such, Sceen::HasMouseFocus() just offloads the work to the equivalent function on the ParentWindow.

#pragma once
#include <SDL.h>
#include "AssetManager.h"
#include "Window.h"

namespace Editor{
class Scene {
 public:
  Scene(Window& ParentWindow)
  : ParentWindow{ParentWindow}
  {}

  void HandleEvent(const SDL_Event& E) {

  }

  void Tick(float DeltaTime) {

  }

  void Render(SDL_Surface* Surface) {

  }

  AssetManager& GetAssets() {
    return Assets;
  }

  bool HasMouseFocus() const {
    return ParentWindow.HasMouseFocus();
  }

  Window& GetWindow() const {
    return ParentWindow;
  }

 private:
  Window& ParentWindow;
  AssetManager Assets;
};
}

Image Class

The Editor::Image class is our wrapper around SDL_Surface for loading and rendering image files. It's functionally very similar to image classes we have used in previous projects, handling image loading via IMG_Load() in its constructor and cleanup via SDL_FreeSurface() in its destructor.

As with other editor components, it resides in the Editor namespace. One subtle but important change is in the Render() method's signature: the SDL_Rect Rect parameter is now passed by copy, not by reference (SDL_Rect* or SDL_Rect&).

This change is necessary because SDL_BlitScaled() can modify the destination rectangle pointer (&Rect in this case) if the blit operation results in clipping. By passing the SDL_Rect by copy, we ensure that the original rectangle object used by the caller of Render() remains unchanged, preventing unexpected side effects.

We covered blitting and this cropping behaviour in detail here:

#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};
};
}

Text Class

The Editor::Text class provides functionality for rendering text using SDL_ttf. It's very similar to text rendering classes we've implemented before, managing a TTF_Font and an SDL_Surface to hold the rendered text.

Its constructor takes the initial string and font size. It uses the font file path and color defined in our Config namespace (Config::FONT and Config::FONT_COLOR) to load the font and render the initial text surface using TTF_RenderText_Blended().

The Render() method takes a destination SDL_Surface and an SDL_Rect*. It calculates the position needed to center the text surface within the provided rectangle and then blits the text using SDL_BlitSurface(). A SetText() method allows updating the displayed text dynamically.

#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};
};
}

Button Class

Editor/Button.h defines a general-purpose Button class for our editor's interface. Each button has a defined rectangular area (Rect), a text label managed by an Editor::Text instance, and a visual state represented by the ButtonState enum.

The button automatically manages hover detection, switching between ButtonState::Normal and ButtonState::Hover. A click action is tied to the virtual HandleLeftClick() function, designed to be overridden by specific button types.

Furthermore, the button's state can be explicitly set using SetState(). This allows us to visually indicate an Active state (e.g., a button corresponding to the level that is currenty loaded) or a Disabled state, where the button becomes unresponsive to mouse events and changes color to indicate unavailability.

#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;
};
}

The behaviour of our Button class is implemented in Editor/Source/Button.cpp. The Render() function is straightforward: it looks up the appropriate color for the current ButtonState in Config::BUTTON_COLORS, fills the button's rectangle (Rect) with that color, and overlays the text label using ButtonText.Render().

The HandleEvent() method filters incoming SDL events. It calls HandleLeftClick() on a left mouse click, but only if the button's state is currently Hover. When handling SDL_MOUSEMOTION, it performs an important preliminary check: ParentScene.HasMouseFocus().

This ensures that hover effects are only triggered when the mouse cursor is genuinely interacting with the scene containing the button, preventing incorrect highlighting if multiple windows are present. If the scene has focus, it then tests if the mouse position falls within the button's Rect to update the State between Normal and Hover.

#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);
}

The Config Namespace

We use a Config.h file, placed in the project root, to centralize various configuration values and helper functions used throughout the application. Constants like button colors (BUTTON_COLORS), font settings (FONT, FONT_COLOR), and window properties are defined here using inline variables or constexpr.

Notice that editor-specific settings, like the window title and dimensions, are nested within a Config::Editor namespace. This separates configuration relevant only to the editor tool from potentially shared configuration (like font choices) that the game might also use later. These editor-specific values are also wrapped in #ifdef WITH_EDITOR.

The file also includes the CheckSDLError() helper function that we’ve been using in previous projects. Wrapped in an #ifdef CHECK_ERRORS, this function provides a way to check for and log SDL errors after critical SDL function calls during development builds, without adding overhead to release builds.

#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 {
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};
}
#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
}

Asset Manager

The Editor/AssetManager.h file defines a simple AssetManager struct within the Editor namespace. Its purpose is to load and hold shared Image resources needed by the editor UI.

Instead of having multiple different UI elements each load their own copy of "Assets/Brick_Blue_A.png", they can all access the single BlueBlock instance stored in the AssetManager. This avoids redundant memory usage for identical SDL_Surface objects and centralizes asset loading.

#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"};
};
}

Assets and DLL Files

As with previous projects, we need to ensure the SDL DLL files are provided alongside our executable file. We also need to ensure that the image files listed in Editor::AssetManager and the font specified in Config::FONT are also in that location.

The Config and AssetManager files above, as well as the screenshots within the lessons, are using the following assets:

Our starting point should compile and run successfully, opening a blank window:

Screenshot showing our empty window

CMakeLists.txt

If you’re using CMake, a CMakeLists.txt file is provided below that will automatically copy the DLL files to the same location as the executable. Files stored in an "Assets" directory within the project folder will also be copied to an "Assets" directory in the same location as the executable.

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"

  # These files will be added later
  # "Editor/Source/Blocks.cpp"
  # "Editor/Source/Level.cpp"
  # "Editor/Source/Actor.cpp"
  # "Editor/Source/ActorTooltip.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
)

Summary

In this lesson, we laid the essential groundwork for our level editor. We established the main application structure, including SDL initialization and the core event/update/render loop in main.cpp.

We created foundational classes within an Editor namespace:

  • Window: Manages the main editor window.
  • Scene: Orchestrates the content within the window.
  • Image & Text: Handle graphical and text rendering.
  • Button: Provides a base for interactive UI elements.
  • AssetManager: Loads and shares image resources.

We also set up a Config.h for centralized settings and introduced conditional compilation (#ifdef WITH_EDITOR) to separate editor code from potential future game code. Techniques for handling multiple windows, like the HasMouseFocus() check and specific quit conditions, were implemented. Our project now compiles and runs, displaying a blank window, ready for the next steps.

Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Ryan McCombe
Ryan McCombe
Posted
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

Get Started for Free
Project: Level Editor
sdl2-promo.jpg
This lesson is part of the course:

Game Dev with SDL2

Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games

This course includes:

  • 118 Lessons
  • 92% Positive Reviews
  • Regularly Updated
  • Help and FAQs
Free and Unlimited Access

Professional C++

Unlock the true power of C++ by mastering complex features, optimizing performance, and learning expert workflows used in professional development

Screenshot from Warhammer: Total War
Screenshot from Tomb Raider
Screenshot from Jedi: Fallen Order
Contact|Privacy Policy|Terms of Use
Copyright © 2025 - All Rights Reserved