Moving, Selecting, and Deleting Actors

Add core interactions: drag actors to reposition them, click to select, and press delete to remove them.
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 5
Ryan McCombe
Ryan McCombe
Posted

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:

  • Clicking an actor in the level selects it.
  • A visual cue (like an outline) will show the selected actor.
  • Clicking the background deselects.

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.

By the end of this lesson, our program will let us drag and drop actors within the level to move them, click to select them, and press the delete key to remove them.

Screenshot showing an actor being selected

Moving Actors

Our drag-and-drop logic currently always creates a copy of the dragged actor when the mouse is released over the level. This makes sense when dragging from the ActorMenu, but if we drag an actor that's already in the Level, we want to move the original, not create another copy.

To differentiate these cases, we need a way for an Actor instance to know where it currently resides. We'll introduce an ActorLocation enum with values Level and Menu, and add a corresponding member variable to the Actor class, defaulting to Menu.

// Editor/Actor.h
// ...
#pragma once
#include <SDL.h>
#include "Image.h"

enum class ActorLocation { Level, Menu };

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

  ActorLocation GetLocation() const {
    return Location;
  }

  void SetLocation(ActorLocation NewLocation) {
    Location = NewLocation;
  }

protected:
  // ...
  ActorLocation Location{ActorLocation::Menu};
};
}

When an actor is created via Clone() and is about to be added to the Level, its origin is effectively changing from the menu template to a placed instance. This is the point where we should update its location status.

Inside the Level::AddToLevel() function, just before pushing the NewActor onto the Actors vector, we'll call its SetLocation() method, passing ActorLocation::Level. This ensures all actors managed by the Level correctly identify themselves as being part of the level.

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

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

The Level::HandleDrop() function is where the decision between cloning and moving happens. We can now use the actor's Location property to guide this decision.

Inside HandleDrop(), we'll add an if/else block based on DragActor->GetLocation(). The if (Location == Menu) branch contains the original code that clones the actor.

The else branch (implicitly Location == Level) simply updates the position of the DragActor itself using SetPosition().

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

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

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

  using enum ActorLocation;
  if (DragActor->GetLocation() == Menu) {
    ActorPtr NewActor{DragActor->Clone()};
    NewActor->SetPosition(
      MouseX - DragOffsetX,
      MouseY - DragOffsetY
    );
    AddToLevel(std::move(NewActor));
  } else {
    DragActor->SetPosition(
      MouseX - DragOffsetX,
      MouseY - DragOffsetY
    );
  }
}

Handling Overlapping Actors

We can now move actors, but a subtle bug emerges when actors overlap. If you click on a pixel where multiple actors are present, the current Level::HandleEvent() loop sends the SDL_MOUSEBUTTONDOWN event to all actors under the cursor.

Each actor that receives the click and is under the cursor will initiate a drag via Actor::HandleEvent(). This leads to multiple ACTOR_DRAG events being pushed, and unpredictable behavior when the mouse is released.

To fix this, we need the event handling process to stop once one actor has successfully "consumed" the click event. We can achieve this by modifying Actor::HandleEvent() to return a boolean value.

If an actor handles the event (i.e., it was clicked and initiated a drag), it should return true. Otherwise, it returns false. The loop in Level::HandleEvent() can then check this return value and break immediately after an actor returns true, preventing subsequent actors from processing the same click.

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

namespace Editor {
class Scene;
class Actor {
 public:
  // ...
  virtual void HandleEvent(const SDL_Event& E);
  virtual bool HandleEvent(const SDL_Event& E);
  // ...
}

We need to update the implementation of Actor::HandleEvent() in Actor.cpp to match the new bool return type declared in the header. The logic remains largely the same, but now we return values.

Inside the if block that detects a valid left-click on the actor, after pushing the ACTOR_DRAG event, we will return true. If the if condition is not met (meaning this actor didn't handle the click), the function reaches the end, where we return false.

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

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

Now that Actor::HandleEvent() returns whether it consumed the event, we’ll update the loop in Level::HandleEvent() in Level.cpp to use this information.

Inside the for loop that iterates through the Actors vector, we checks the return value immediately after calling the function. If true, the break statement exits the loop immediately.

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

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

Our program should now handle the overlapping actor scenario slightly more gracefully. However, it still has a small issue. In the scenario where the cursor was overlapping multiple actors, it is the first actor in the Actors array that returns true that gets moved.

Later actors don’t get notified of the click at all. This creates a slightly weird experience, because those later actors are rendered on top of earlier actors. So, with our current logic, we move the actor that is visually on the bottom of the set of actors the user clicked on. In the following example, that would be the block at index 2:

Diagram showing overlapping actors

We want to maintain the behaviour that actors added later to our level appear on top of those added earlier, so let’s not change our Render() logic. Instead, we’ll fix this problem by changing the order in which actors get to handle events. That is, actors that occur later in our Actors array get the opportunity to handle events before those that occur earlier.

We can implement this by having our level’s HandleEvent() function iterate through our actors in reverse order.

In C++20 and later, the easiest way to iterate an array in reverse order is using std::views::reverse from the <ranges> header:

// Editor/Source/Level.cpp
// ...
#include <ranges>
// ...

void Level::HandleEvent(const SDL_Event& E) {
  using namespace std::views;
  for (ActorPtr& A : reverse(Actors)) {
    if (A->HandleEvent(E)) {
      break;
    }
  }
}

Reverse Iteration Prior to C++20

Prior to C++20, we can iterate through an array in reverse order using a custom for loop. As a reminder, the algorithm to forward-iterate through an array looks like this:

  • Start at index 0
  • Increment the index on every iteration
  • Stop at the last index (size() - 1)

To iterate in reverse order, we our process looks like this:

  • Start at the last index (size() - 1)
  • Decrement the index on every iteration
  • Stop at index 0

It looks like this:

for (int i = Actors.size() - 1; i >= 0; --i) {
  if (Actors[i]->HandleEvent(E)) {
    break;
  }
}

Note that we’re using an int here rather than a size_t. We should be careful decrementing unsigned integers like a size_t towards 0, as we will cause wrap-around behavior if we go too far.

Another common way to achieve reverse iteration before C++20 is by using reverse iterators provided by standard containers like std::vector. Every vector has rbegin() and rend() methods that return iterators pointing to the last element and one position before the first element, respectively.

We can use these in a standard for loop or a range-based for loop (if iterating over the iterators directly isn't needed). This approach is often considered more idiomatic C++ than manual index manipulation.

for (
  auto it{Actors.rbegin()};
  it != Actors.rend();
  ++it
 ) {
  // Note: 'it' is an iterator, so we
  // need *it to get the unique_ptr
  if ((*it)->HandleEvent(E)) {
    break;
  }
}

We cover views and iterators in much more detail in our advanced course.

Actor Visbility

The current dragging experience for actors within the level shows the actor fixed in place while a separate tooltip follows the cursor. This isn't as intuitive as making the original actor vanish during the drag.

We can improve this by controlling the actor's visibility. Let's introduce an isVisible boolean member to the Actor class, initialized to true. We'll provide getter and setter functions for this state.

The Actor::Render() function will then be modified to check isVisible at the beginning. If it's false, the function will simply return without drawing anything, effectively hiding the actor.

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

namespace Editor {
class Scene;
class Actor {
 public:
  // ...
  void Render(SDL_Surface* Surface) {
    if (GetIsVisible()) {
      Art.Render(Surface, Rect);
    }
  }

  bool GetIsVisible() const {
    return isVisible;
  }

  void SetIsVisible(bool NewVisibility) {
    isVisible = NewVisibility;
  }

protected:
  // ...
  bool isVisible{true};
};
}

Hiding the actor should occur when a drag starts, but only if the actor being dragged is part of the Level. When dragging from the ActorMenu, we’re cloning the actor, so showing both the original actor and something representing the clone in the tooltip is natural.

The drag initiation point is in Actor::HandleEvent(). Therefore, inside the if block processing the SDL_MOUSEBUTTONDOWN event, we add a nested if check. If the actor's location isn't the menu, we call SetIsVisible(false) to hide it just before the code that pushes the ACTOR_DRAG event.

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

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

When the drag operation ends (i.e., the user releases the mouse button), the actor that was being dragged needs to become visible again in its new (or original, if dropped outside the level) position. This logic belongs where the drop is finalized.

In Level::HandleDrop(), regardless of whether the drop was valid (inside the level) or not, the first action should be to restore visibility. We add DragActor->SetIsVisible(true); at the very beginning of the function, ensuring the actor reappears immediately upon mouse release.

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

void Level::HandleDrop(Actor* DragActor) {
  DragActor->SetIsVisible(true);
}

Selecting Actors

To enable deletion, users first need a way to specify which actor they want to delete. This calls for a selection mechanism. We'll allow only one actor to be selected at a time.

The Level class is the natural place to manage this state. We'll add a member variable Actor* SelectedActor, initialized to nullptr, to store a raw pointer to the currently selected actor instance within the Actors vector.

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

namespace Editor {
// ...

class Level {
// ...

 private:
  // ...
  Actor* SelectedActor{nullptr};
};
}

A simple way to handle selection is to make the actor the user just interacted with the selected one. Since dropping an actor (either cloned from the menu or moved within the level) signifies the end of an interaction with that actor, Level::HandleDrop() is a good place to set the selection.

Inside HandleDrop(), after either cloning and positioning NewActor or just positioning DragActor, we'll set SelectedActor to point to that actor.

For the cloning case, we’ll use SelectedActor = NewActor.get(), getting the raw pointer from the unique_ptr. For the moving case, we can just use SelectedActor = DragActor.

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

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

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

  using enum ActorLocation;
  if (DragActor->GetLocation() == Menu) {
    ActorPtr NewActor{DragActor->Clone()};
    NewActor->SetPosition(
      MouseX - DragOffsetX,
      MouseY - DragOffsetY
    );
    SelectedActor = NewActor.get();
    AddToLevel(std::move(NewActor));
  } else {
    DragActor->SetPosition(
      MouseX - DragOffsetX,
      MouseY - DragOffsetY
    );
    SelectedActor = DragActor;
  }
}

Rendering Selection Indicators

Users need visual feedback to know which actor is currently selected. We’ll see more examples of drawing indicators like these later in the lesson, but a common technique is to draw an outline or highlight around the selected object.

We can implement a simple outline by drawing a slightly larger rectangle filled with a distinct color (e.g., white) before drawing the selected actor itself. This creates a border effect.

The logic for this belongs in Level::Render(). Inside the loop that iterates through the Actors vector, before calling A->Render(Surface), we'll add a check.

If SelectedActor is not null, and the current actor A is the SelectedActor, and the SelectedActor is visible, we calculate the dimensions for the outline rectangle (e.g., one pixel larger on each side) and fill it using SDL_FillRect() with our chosen highlight color.

Then, the normal A->Render(Surface) call proceeds, drawing the actor on top of this rectangle.

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

If we run our program, we should now see a white outline around the actor we currently have selected:

Screenshot showing an actor being selected

Clearing Selection

We need a way for the user to deselect the current actor. A common interaction pattern is to deselect by clicking anywhere that isn't another selectable object. Since our only selectable objects are actors within the level, clicking the level background or any other UI element should clear the selection.

We can implement this easily in Level::HandleEvent(). After the loop that dispatches events to actors, we add a check for SDL_MOUSEBUTTONDOWN with SDL_BUTTON_LEFT. If this event occurs, we unconditionally set SelectedActor = nullptr. If the click was on an actor, the selection will be immediately re-established later in frame update process by the HandleDrop() logic.

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

void Level::HandleEvent(const SDL_Event& E) {
  using namespace std::views;
  for (ActorPtr& A : reverse(Actors)) {
    if (A->HandleEvent(E)) {
      break;
    }
  }
  
  if (
    E.type == SDL_MOUSEBUTTONDOWN &&
    E.button.button == SDL_BUTTON_LEFT
  ) {
    SelectedActor = nullptr;
  }
}

Reviewing Mouse Handling

It may not be obvious why the previous code works, as we’re clearing the selection regardless of where the user clicks. What if they clicked on an actor?

Our implementation works because, if the user clicked on an actor, it is the HandleDrop() function that is causing that actor to become selected. HandleDrop() is invoked from the ActorTooltip's Tick() function, which happens after the Level's HandleEvent() function cleared the selection.

When the user left-clicks on an actor, our program behaves in the following way:

  • The Actor’s HandleEvent() pushes an ACTOR_DRAG event, and the Level's HandleEvent() clears the SelectedActor.
  • The ActorTooltip responds to the ACTOR_DRAG event by becoming active.
  • In the ActorTooltip's Tick() function, if the user is no longer holding down the left mouse button, we call the Level's HandleDrop() function.
  • HandleDrop() sets the SelectedActor to the actor that the user dragged (or left-clicked on)

Deleting Actors

With selection implemented, we can now add the deletion functionality. The standard way to trigger deletion is by pressing the 'Delete' key on the keyboard while an object is selected.

We'll modify Level::HandleEvent() again. We add an else if condition after the left-click check. This new condition checks for E.type == SDL_KEYDOWN, ensures E.key.keysym.sym == SDLK_DELETE, and verifies that SelectedActor is not nullptr.

If all are true, we know a deletion is requested for the selected actor.

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

void Level::HandleEvent(const SDL_Event& E) {
  using namespace std::views;
  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
  ) {
    
    // TODO: delete the selected actor
    // from the Actors array

    SelectedActor = nullptr;
  }
}

To erase our selected actor, we can find its index in the Actors array, and then use the erase(begin() + index) approach to remove the element at that index:

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

void Level::HandleEvent(const SDL_Event& E) {
  using namespace std::views;
  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
  ) {
    
    for (size_t i{0}; i < Actors.size(); ++i) {
      if (Actors[i].get() == SelectedActor) {
        Actors.erase(Actors.begin() + i);
        break;
      }
    }

    SelectedActor = nullptr;
  }
}

Note that, in general, we should be extremely cautious about modifying the structure of an array (such as removing items) whilst iterating over that same array. These structural modifications can easily break our iteration logic by, for example, changing the size() of the array.

In this case, deleting an array object whilst iterating the array is safe, because we immediately stop iterating (using break) as soon as we resize our array.

Advanced: std::erase_if() and Lambdas

A commonly-used and more elegant way of conditionally erasing items from arrays is the std::erase_if() function, added in C++20. To use it, we first need to define a function that returns a boolean indicating whether an item meets the criteria for deletion:

bool isSelected(
  Actor* SelectedActor,
  const ActorPtr& ActorInArray
) {
  return SelectedActor == ActorInArray.get();
}

We then provide the std::erase_if() function with a reference to our array, and the function that will determine if each element in the array should be deleted:

std::erase_if(
  Actors,
  isSelected
);

This code won’t compile yet, as std::erase_if() will only call our function with a single argument - the actor in the array that it is currently evaluating for deletion. Our isSelected function also needs the SelectedActor to compare it against.

We could solve this in two ways. First, we could change isSelected to be a member function of the Level class. In that case, it can just access the SelectedActor from the Level object - it doesn’t need the argument:

class Level {
  bool isSelected(
    Actor* SelectedActor,
    const ActorPtr& ActorInArray
  ) {
    return SelectedActor == ActorInArray.get();
  }
}

Alternatively, we can use a binding function, such as std::bind_front(), to provide the argument before handing it to std::erase_if():

std::erase_if(
  Actors,
  std::bind_front(isSelected, SelectedActor)
);

These are both techniques we covered earlier in the course:

In most cases, a more advanced technique would be used to solve this problem, called a lambda. Lambdas allow us to define small, concise blocks of logic.

They’re similar to a function definition but, unlike functions, lambdas can be defined within the bodies of other functions. They’re ideal for creating simple callable objects to control the behaviour of other algorithms, such as std::erase_if().

Solving our problem using a lambda would look like this:

std::erase_if(
  Actors,
  [&](const ActorPtr& Actor){
    return Actor.get() == SelectedActor;
  }
);

We cover functional programming techniques and lambdas in much more detail in our advanced course.

Complete Code

Complete versions of the files we changed in this section are provided below. We updated the Actor class with new Location and isVisible members, and we updated it’s HandleEvent() function to return a bool:

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

protected:
  Scene& ParentScene;
  SDL_Rect Rect;
  Image& Art;
  SDL_Point DragOffset{0, 0};
  ActorLocation Location{ActorLocation::Menu};
  bool isVisible{true};
};
}
// Editor/Source/Actor.cpp
// ...
#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;
}

Over in the Level class, we added a new SelectedActor member, and made large updates to the HandleEvent(), HandleDrop(), and Render() functions.

We also updated AddToLevel() to ensure the Actor we add has its Location set to ActorLocation::Level:

#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;
  Actor* SelectedActor{nullptr};
  SDL_Rect Rect{
    0, 0,
    Config::Editor::LEVEL_WIDTH,
    Config::Editor::LEVEL_HEIGHT
  };
};
}
// Editor/Source/Level.cpp
// ...

#include "Editor/Level.h"
#include "Editor/Scene.h"
#include <ranges>

using namespace Editor;

void Level::HandleEvent(const SDL_Event& E) {
  using namespace std::views;
  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;
  }
}

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 [DragOffsetX, DragOffsetY]{
    DragActor->GetDragOffset()
  };

  using enum ActorLocation;
  if (DragActor->GetLocation() == Menu) {
    ActorPtr NewActor{DragActor->Clone()};
    NewActor->SetPosition(
      MouseX - DragOffsetX,
      MouseY - DragOffsetY
    );
    SelectedActor = NewActor.get();
    AddToLevel(std::move(NewActor));
  } else {
    DragActor->SetPosition(
      MouseX - DragOffsetX,
      MouseY - DragOffsetY
    );
    SelectedActor = DragActor;
  }
}

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

Summary

This lesson added core manipulation features for actors placed in the level. By introducing an ActorLocation enum, we enabled the editor to correctly interpret drag-and-drop: cloning from the menu, but moving within the level.

We tackled the issue of clicking stacked actors by making Actor::HandleEvent() return a status and processing actors in reverse rendering order, ensuring the visually top actor reacts.

We enhanced the dragging feel by hiding the source actor during the move. A selection system was built: clicking selects an actor (highlighted with an outline), and clicking away clears the selection.

Lastly, we implemented deletion logic: if an actor is selected, pressing the 'Delete' key removes it from the Level's Actors container, demonstrating keyboard event handling and vector modification.

Highlights:

  • ActorLocation enum for state-based behavior.
  • Conditional cloning vs. moving in HandleDrop().
  • Boolean return from HandleEvent() for event consumption.
  • Reverse iteration (std::views::reverse) for correct UI interaction order.
  • isVisible flag for temporary hiding during drags.
  • SelectedActor pointer for managing selection.
  • Drawing selection outlines in Render().
  • SDLK_DELETE key handling for actor removal.
  • Using std::erase_if() to modify the actor collection.
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