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.
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.
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
);
}
}
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:
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;
}
}
}
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:
0
size() - 1
)To iterate in reverse order, we our process looks like this:
size() - 1
)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.
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);
}
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;
}
}
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:
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;
}
}
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:
Actor
’s HandleEvent()
pushes an ACTOR_DRAG
event, and the Level
's HandleEvent()
clears the SelectedActor
.ActorTooltip
responds to the ACTOR_DRAG
event by becoming active.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)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.
std::erase_if()
and LambdasA 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 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;
}
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.HandleDrop()
.HandleEvent()
for event consumption.std::views::reverse
) for correct UI interaction order.isVisible
flag for temporary hiding during drags.SelectedActor
pointer for managing selection.Render()
.SDLK_DELETE
key handling for actor removal.std::erase_if()
to modify the actor collection.Add core interactions: drag actors to reposition them, click to select, and press delete to remove them.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games