Placing Actors in the Level

Build the level container, add logic for placing actors via drag-and-drop, including visual hints.
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 4
Ryan McCombe
Ryan McCombe
Posted

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.

Diagram showing actors being added to the level

Adding a Level

Currently, when we release the mouse after dragging an actor, nothing happens. We need a designated area to drop these actors into – our level canvas. We'll start by creating a Level class responsible for managing the actors placed within this area.

To define the level's visual appearance and size, let's add some configuration constants to Config::Editor. We'll define LEVEL_WIDTH, LEVEL_HEIGHT, and LEVEL_BACKGROUND. We can then update our existing WINDOW_WIDTH, WINDOW_HEIGHT, and ACTOR_MENU_POSITION_X constants to be calculated relative to these new level dimensions, making the layout more robust.

// Config.h
// ...

#ifdef WITH_EDITOR
namespace Config::Editor {
// Level
inline const int LEVEL_WIDTH{650};
inline const int LEVEL_HEIGHT{150};
inline constexpr SDL_Color LEVEL_BACKGROUND{
  50, 50, 50, 255};

// ActorMenu
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

// ...

Let’s add our Level class. It’s starting point uses the same techniques as our other high-level classes. It has the usual HandleEvent(), Tick() , and Render() functions.

We also manage a std::vector of Actor unique pointers, alongside an AddToLevel() function to add new actors to this collection.

We have a HasMouseFocus() that returns a boolean representing whether the mouse is currently hovering over it.

Finally, we’ll store a simple SDL_Rect with some help from our configuration variables. This will represent the area that our Level uses within the window. We’ll walk through each of these functions when we add their definitions to a .cpp file in the next section

// Editor/Level.h
#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} {}

  void HandleEvent(const SDL_Event& E);
  void Tick(float DeltaTime);
  void Render(SDL_Surface* Surface);
  bool HasMouseFocus() const;
  void AddToLevel(ActorPtr NewActor);

 private:
  Scene& ParentScene;
  ActorPtrs Actors;
  SDL_Rect Rect{
    0, 0,
    Config::Editor::LEVEL_WIDTH,
    Config::Editor::LEVEL_HEIGHT
  };
};
}

Handling Events

We'll define the Level methods in Editor/Source/Level.cpp. First up is HandleEvent(). At this stage, the level itself doesn't have complex event handling, but the actors it contains might.

Therefore, the HandleEvent() implementation loops through the Actors vector and calls the HandleEvent() method on each contained actor, allowing them to process the event as needed.

// Editor/Source/Level.cpp
#include "Editor/Level.h"
#include "Editor/Scene.h"

using namespace Editor;

void Level::HandleEvent(const SDL_Event& E) {
  for (ActorPtr& A : Actors) {
    A->HandleEvent(E);
  }
}

Ticking

Similarly, the Level::Tick() function primarily delegates the update logic to its contained actors. It iterates through the Actors vector and calls the Tick() method on each one, passing along the DeltaTime.

// Editor/Source/Level.cpp
// ...

void Level::Tick(float DeltaTime) {
  for (ActorPtr& A : Actors) {
    A->Tick(DeltaTime);
  }
}

Rendering

In Level::Render(), we first establish the level's visual base. We look up Config::Editor::LEVEL_BACKGROUND and call SDL_FillRect() to paint the level's Rect with this color on the provided Surface.

Once the background is drawn, we iterate through our Actors collection. For every actor stored, we invoke its Render() method, ensuring each actor is rendered on top of the background.

// Editor/Source/Level.cpp
// ...

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) {
    A->Render(Surface);
  }
}

Adding Actors

The Level::AddToLevel() function is responsible for adding a new actor (provided as a unique_ptr) to the level's collection.

It simply uses Actors.push_back() to append the actor pointer to the Actors vector, transferring ownership of the dynamically allocated Actor object to the vector.

// Editor/Source/Level.cpp
// ...

void Level::AddToLevel(ActorPtr NewActor) {
  Actors.push_back(std::move(NewActor));
}

Implementing HasMouseFocus()

Our HasMouseFocus() function is a little more complex. Unless we implemented the platform-specific workarounds we mentioned earlier, we need to consider the possibility that our tooltip window has mouse focus.

As such, we can’t reliably use a function like SDL_GetMouseState(), as that may return the mouse position relative to the tooltip, rather than relative to our main editor window.

So instead, we get the global position of our mouse, alongside the position of our main editor window. We then subtract the window position from the mouse’s global position to determine the mouse position relative to that editor window.

If that horizontal value is greater than our LEVEL_WIDTH, the cursor is to the right of our level. If the vertical value is greater than LEVEL_HEIGHT, the cursor is below the level.

Given our Level is positioned at the top left of our scene, we can ensure the mouse isn’t to the left or above our level by ensuring that the cursor is within the Scene using ParentScene.HasMouseFocus()

// Editor/Source/Level.cpp
// ...

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

Adding the Level to the Scene

With the Level class defined and implemented, we need to integrate it into our main Editor::Scene. Just like we did with ActorMenu and ActorTooltip, we add a Level member variable to the Scene class.

We declare Level CurrentLevel; in the private section and initialize it by passing a reference to the scene - *this - to its constructor. We also need to #include the Editor/Level.h header.

Finally, we update the Scene's HandleEvent(), Tick(), and Render() methods to call the corresponding methods on the CurrentLevel instance, ensuring it participates fully in the application loop.

// Editor/Scene.h
// ...
#include "Level.h"
// ...

namespace Editor{
class Scene {
 public:
  // ...
  void HandleEvent(const SDL_Event& E) {
    ActorShelf.HandleEvent(E);
    TooltipWindow.HandleEvent(E);
    CurrentLevel.HandleEvent(E);
  }

  void Tick(float DeltaTime) {
    ActorShelf.Tick(DeltaTime);
    TooltipWindow.Tick(DeltaTime);
    CurrentLevel.Tick(DeltaTime);
  }

  void Render(SDL_Surface* Surface) {
    ActorShelf.Render(Surface);
    TooltipWindow.Render();
    CurrentLevel.Render(Surface);
  }

  // ...

 private:
  Level CurrentLevel{*this};
  // ...
};
}

Running our program, we should now see our Level rendering as a large rectangle using the color and dimensions we set in our config file:

Diagram showing the level background being rendered

Handling Drops

Our ActorTooltip needs to be able to tell the Level to add an actor when a drop occurs. Currently, ActorTooltip only holds a reference to the Scene. To facilitate communication with the Level, we'll add a simple getter function to the Scene class. This GetLevel() method will return a reference to the CurrentLevel member variable, allowing other classes that have access to the Scene (like ActorTooltip) to interact directly with the Level instance.

// Editor/Scene.h
// ...

namespace Editor{
class Scene {
 public:
  // ...

  Level& GetLevel() {
    return CurrentLevel;
  }
  
  // ...
};
}

Now we can modify ActorTooltip::Tick(). Inside the block where it detects the left mouse button release, right after hiding the tooltip, we'll initiate the drop.

We use the ParentScene reference to get the Level via ParentScene.GetLevel(). Then, we call a new method on the level, HandleDrop(), passing the DragActor pointer (the actor being dragged) as an argument. We'll define HandleDrop() in the Level class next.

// Editor/Source/ActorTooltip.cpp
// ...

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

// ...

Why not use an event?

We could have pushed an event for this actor-dropping action rather than directly coupling our ActorTooltip to our Level. The main reason we preferred the direct approach of giving our ActorTooltip access to the Level so that it can immediately trigger the actor drop is that it lets our program be slightly more responsive.

When a Tick() function pushes an event, that event is not processed until the HandleEvent() step of our next frame, meaning it would be a few extra milliseconds before our dropped Actor shows up in our level.

These off-by-one delays are extremely common in games, and are rarely noticed. However, when dealing with user interactions, we want to minimise these delays as much as possible. We expect the user to be focusing directly on the action they’re performing with their mouse, so delays in that area are particularly noticable.

We need to declare the HandleDrop() function that ActorTooltip now calls. Add the function declaration to the public section of the Level class definition in Editor/Level.h.

// Editor/Level.h
// ...

namespace Editor {
class Scene;
class Level {
 public:
  // ...
  void HandleDrop(Actor* DragActor);
  // ...
};
}

Before we implement the logic inside Level::HandleDrop() in Editor/Source/Level.cpp, we need to add some helper methods to our Actor class to support creating copies and setting positions. For now, let's just add the empty function definition to Level.cpp to allow the code to compile.

// Editor/Source/Actor.cpp
// ...

void Level::HandleDrop(Actor* DragActor) {
  // ...
}

Cloning Actors

To place the dropped actor correctly, we need the ability to set its x and y coordinates. Internally, the Actor instances keep track of their position using an SDL_Rect called Rect.

Let's introduce a public SetPosition() function in Editor/Actor.h which updates Rect.x and Rect.y. We can also add a convenient SDL_Point GetPosition() const getter that returns {Rect.x, Rect.y}.

// Editor/Actor.h
// ...

namespace Editor {
class Scene;
class Actor {
 public:
 // ...

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

  // ...
};
}

The second problem we have is that the ActorTooltip doesn’t exactly know what type of actor it is dragging, therefore we don’t know what type of actor we need to add to our level. DragActor is just a base Actor pointer.

We can solve this by polymorphism. Let’s add a virtual Clone() method to our Actor that will create an object of the correct type, but return a std::unique_ptr to the base type:

// Editor/Actor.h
// ...

namespace Editor {
class Scene;
class Actor {
 public:
  // ...
  virtual std::unique_ptr<Actor> Clone() const {
    return std::make_unique<Actor>(*this);
  }

  // ...
};
}

Our Actor subtypes can now override this Clone() function to ensure the pointer being returned points to an actor of their specific derived type. For example, our BlueBlock's Clone() function will construct a BlueBlock instead of a base Actor but, to maintain our polymorphic behaviour, it still must return it as a base Actor pointer:

// Editor/Blocks.h
// ...

namespace Editor{
class BlueBlock : public Actor {
public:
  // ...

  std::unique_ptr<Actor> Clone() const override {
    return std::make_unique<BlueBlock>(*this);
  }
};
}

Now we can fully implement Level::HandleDrop() in Editor/Source/Level.cpp. First, we get the mouse position relative to the window using SDL_GetMouseState(). We also retrieve the DragOffset from the DragActor that was being dragged.

The core step is creating the new actor. Because Clone() is virtual, this call invokes the correct implementation - e.g., BlueBlock::Clone() - based on the actual type of DragActor, returning a unique_ptr to the newly created copy.

We then set the position of this NewActor using Actor::SetPosition(), adjusting the drop location by the stored DragOffset.

Finally, we add the NewActor to the level's collection using AddToLevel().

// Editor/Source/Level.cpp
// ...

void Level::HandleDrop(Actor* DragActor) {
  int MouseX, MouseY;
  SDL_GetMouseState(&MouseX, &MouseY);
  auto [DragOffsetX, DragOffsetY]{
    DragActor->GetDragOffset()
  };

  ActorPtr NewActor{DragActor->Clone()};
  NewActor->SetPosition(
    MouseX - DragOffsetX,
    MouseY - DragOffsetY
  );
  AddToLevel(std::move(NewActor));
}

// ...

Note that we’re calling SDL_GetMouseState() here, which will return the mouse position relative to the window that has focus. As such, unless we’re making the tooltip window non-focusable using platform-specific APIs, we should ensure that our ActorTooltip hides itself before it calls HandleDrop().

Running our program, we should now be able to drag actors from our menu to place copies of them in our level:

Diagram showing actors being added to the level

Our drop-handling logic currently has two problems:

  1. We can drag actors from anywhere in our window to create a copy, not just from our menu. We’ll fix this in the next lesson where, if we drag an actor that is already in the level, our action will move that actor rather than copy it
  2. We can drop actors anywhere, not just in our level rectangle. That lets us place actors anywhere in the window. We can even drop actors outside of the window, which will cause them to be added to our level at position x = 0, y = 0. We’ll fix that in the next section
Diagram showing actors being dragged outside the level

Visual Glitches and Off-By-One Issues

There may also be an additional small visual glitch when we drop an actor, as our tooltip disappears a few milliseconds before the actor that it dropped is rendered to the screen.

This is because, when we hide the tooltip, the window disappears almost immediately, but the actor isn’t rendered in that location until slightly later. Specifically, it isn’t rendered until we call SDL_UpdateWindowSurface() for our primary window, which happens at the end of the current iteration of the application loop.

We can delay hiding the tooltip but, unless we implemented the platform-specific workarounds introduced in the previous lesson, our tooltip window will have mouse focus until we hide it.

If we delay hiding the tooltip, functions like SDL_GetMousePosition(), will return the mouse position relative to that tooltip, rather than our main window. We’ll leave this problem for now as it’s fairly minor.

Rejecting Drops Outside of the Level

Currently, our HandleDrop() function adds the actor regardless of where the mouse is when the button is released. We only want to add actors if the drop occurs within the bounds of the level area.

Fortunately, we already implemented the Level::HasMouseFocus() method, which checks if the mouse cursor is over the level rectangle. We can simply add a check at the beginning of Level::HandleDrop().

If HasMouseFocus() returns false, we can effectively ignore the drop, as it occured outside the Level. We return immediately, preventing the actor from being cloned or added.

// Editor/Source/Level.cpp
// ...

void Level::HandleDrop(Actor* DragActor) {
  if (!HasMouseFocus()) {
    return;
  }
  
}

If we run our program, we can now no-longer drop actors if our mouse pointer is outside the bounds of our level rectangle. This still allows us to drop actors partially outside of our level:

Diagram showing actors being dropped partially outside the level

If our design was to allow the free-form placement of actors within the level, we may want to spend more time handling this more gracefully. For example, we’d reorder our scene rendering such that the ActorMenu is rendered after (and therefore, on top of) the Level, and we’d treat the footer similarly once we come to implement it.

However, in our case, we’re eventually going to restrict the position of actors to a predefined grid, so we’ll leave this quirk for now.

Indicating that Drops will be Rejected

During drag-and-drop operations, it can be helpful to indicate to the user that, if they drop the actor in the current location of their mouse, that action will have no effect.

Two common ways of indicating this is to make the tooltip window semi-transparent, or changing the cursor to something indicating that their action will be denied.

Screenshot showing a drop position being denied

Let’s implement both the opacity and cursor change. We’ll start by adding an SDL_Cursor* member to our ActorTooltip:

// Editor/ActorTooltip.h
// ...

namespace Editor{
class Scene;
class ActorTooltip {
  // ...

private:
  // ...
  SDL_Cursor* DenyCursor{nullptr};
};
}

When we initialize our ActorTooltip, we’ll create the DenyCursor. We’ll request the system’s default cursor for denied actions, using SDL_SYSTEM_CURSOR_NO.

We’ll also free our cursor in the destructor:

// Editor/Source/ActorTooltip.cpp
// ...

ActorTooltip::ActorTooltip(Scene& ParentScene)
  : ParentScene{ParentScene}
{
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); } }

Let’s apply our cursor change and opacity update in the PositionWindow() function, which is called on every Tick() if our tooltip is visible.

  • If our Level doesn’t have mouse focus, we’ll make our tooltip semi-transparent and change our cursor to the DenyCursor.
  • If our Level does have focus, we’ll revert our window to be fully opaque, and our cursor back to the default:
// Editor/Source/ActorTooltip.cpp
// ...

void ActorTooltip::PositionWindow() {
if (ParentScene.GetLevel().HasMouseFocus()) { SDL_SetWindowOpacity(SDLWindow, 1); SDL_SetCursor(SDL_GetDefaultCursor()); } else { SDL_SetWindowOpacity(SDLWindow, 0.5); SDL_SetCursor(DenyCursor); } }

When the drag operation ends and the tooltip is hidden, we need to ensure the cursor and window opacity are reset to their default states, regardless of where the mouse was when the button was released. Otherwise, the user might be left with the deny cursor or a semi-transparent (though hidden) window state carrying over.

In ActorTooltip::SetIsVisible(), inside the else block where Visible is false, we’ll add lines to explicitly set the cursor back to the default and reset the window opacity to fully opaque right after calling SDL_HideWindow().

// Editor/Source/ActorTooltip.cpp
// ...

void ActorTooltip::SetIsVisible(bool Visible) {
  isVisible = Visible;
  if (isVisible) {
    SDL_ShowWindow(SDLWindow);
  } else {
    SDL_HideWindow(SDLWindow);
    SDL_SetCursor(SDL_GetDefaultCursor());
    SDL_SetWindowOpacity(SDLWindow, 1);
  }
}

Running our program, we should now see our tooltip and cursor react to where we’re trying to drop our actors:

Screenshot showing a drop position being denied

Complete Code

The Level class we created in this lesson is available below:

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

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

 private:
  Scene& ParentScene;
  ActorPtrs Actors;
  SDL_Rect Rect{
    0, 0,
    Config::Editor::LEVEL_WIDTH,
    Config::Editor::LEVEL_HEIGHT
  };
};
}
#include "Editor/Level.h"
#include "Editor/Scene.h"

using namespace Editor;

void Level::HandleEvent(const SDL_Event& E) {
  for (ActorPtr& A : Actors) {
    A->HandleEvent(E);
  }
}

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) {
    A->Render(Surface);
  }
}

void Level::HandleDrop(Actor* DragActor) {
  if (!HasMouseFocus()) {
    return;
  }

  int MouseX, MouseY;
  SDL_GetMouseState(&MouseX, &MouseY);
  auto [DragOffsetX, DragOffsetY]{
    DragActor->GetDragOffset()
  };

  ActorPtr NewActor{DragActor->Clone()};
  NewActor->SetPosition(
    MouseX - DragOffsetX, MouseY - DragOffsetY);
  AddToLevel(std::move(NewActor));
}

void Level::AddToLevel(ActorPtr NewActor) {
  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;
}

We also updated our Config file to store some variables for our level, and we added a Level instance to our Scene:

#pragma once
#include <iostream>
#include <SDL.h>
#include <string>
#include <vector>

namespace UserEvents{
#ifdef WITH_EDITOR
  inline Uint32 ACTOR_DRAG{
    SDL_RegisterEvents(1)};
#endif
}

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 {
// Level
inline const int LEVEL_WIDTH{650};
inline const int LEVEL_HEIGHT{150};
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 "ActorMenu.h"
#include "ActorTooltip.h"
#include "AssetManager.h"
#include "Level.h"
#include "Window.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);
  }

  void Tick(float DeltaTime) {
    ActorShelf.Tick(DeltaTime);
    TooltipWindow.Tick(DeltaTime);
    CurrentLevel.Tick(DeltaTime);
  }

  void Render(SDL_Surface* Surface) {
    ActorShelf.Render(Surface);
    TooltipWindow.Render();
    CurrentLevel.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;
};
}

We also updated our Actor class:

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

  bool HasMouseFocus() const;
  virtual void HandleEvent(const SDL_Event& E);

  void Tick(float DeltaTime) {}

  void Render(SDL_Surface* Surface) {
    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);
  }

protected:
  Scene& ParentScene;
  SDL_Rect Rect;
  Image& Art;
  SDL_Point DragOffset{0, 0};
};
}
#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);
  }
};
}

Finally, our updated ActorTooltip class is provided below:

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

  auto [DragOffsetX, DragOffsetY]{
    DragActor->GetDragOffset()
  };

  SDL_SetWindowPosition(
    SDLWindow,
    x - DragOffsetX,
    y - DragOffsetY
  );

  if (ParentScene.GetLevel().HasMouseFocus()) {
    SDL_SetWindowOpacity(SDLWindow, 1);
    SDL_SetCursor(SDL_GetDefaultCursor());
  } else {
    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);
  }
}

Summary

This part focused on completing the drag-and-drop cycle. We created the Level class, the designated area where actors are placed and managed. Configuration values were added for its size and appearance.

The ActorTooltip was updated to detect mouse button release. Upon release, it communicates with the Level (via a new Scene::GetLevel() getter) to handle the drop. The core of the drop mechanism relies on a new virtual Clone() function added to the Actor class, enabling the creation of correct subtype instances without the Level needing explicit knowledge of them.

We also implemented user feedback. The system now checks if the current mouse position during a drag is within the Level boundaries. If not, the tooltip fades using SDL_SetWindowOpacity(), and the cursor changes via SDL_CreateSystemCursor() and SDL_SetCursor(), indicating that a drop action would be invalid there.

In summary:

  • Introduced the Level class as an actor container.
  • Connected ActorTooltip mouse release to Level::HandleDrop().
  • Implemented actor duplication using a virtual Clone() method.
  • Added checks to ensure drops only happen within the level area.
  • Provided visual cues (cursor, opacity) for valid/invalid drop zones.
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