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.
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
};
};
}
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);
}
}
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);
}
}
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);
}
}
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));
}
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;
}
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:
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();
}
}
// ...
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) {
// ...
}
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:
Our drop-handling logic currently has two problems:
x = 0
, y = 0
. We’ll fix that in the next sectionThere 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.
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:
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.
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.
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.
Level
doesn’t have mouse focus, we’ll make our tooltip semi-transparent and change our cursor to the DenyCursor
.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:
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);
}
}
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:
Level
class as an actor container.ActorTooltip
mouse release to Level::HandleDrop()
.Clone()
method.Build the level container, add logic for placing actors via drag-and-drop, including visual hints.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games