Placing Actors in the Level

Build the level container, add logic for placing actors via drag-and-drop, including visual hints.

Ryan McCombe
Published

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.

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:

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

// ...

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:

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

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:

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.

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:

Complete Code

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

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

We also updated our Actor class:

Finally, our updated ActorTooltip class is provided below:

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.
Next Lesson
Lesson 92 of 129

Moving, Selecting, and Deleting Actors

Add core interactions: drag actors to reposition them, click to select, and press delete to remove them.

Have a question about this lesson?
Purchase the course to ask your own questions