Grid-Based Placement

Convert the freeform placement to a grid-based system with snapping and single-actor cell limits
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 6
Ryan McCombe
Ryan McCombe
Posted

In the previous lessons, we built the core functionality for dragging, dropping, selecting, and deleting actors in our level editor. Currently, actors can be placed anywhere on the level canvas, overlapping freely. While this works for some game styles, many benefit from more structured placement.

This lesson introduces grid-based positioning. First, we'll implement grid snapping. When dragging or dropping an actor within the level, its position will automatically "snap" to the nearest grid line. This helps designers align objects precisely without tedious manual adjustments.

Then, we'll take it a step further and transform our editor into a true cell-based system. We'll modify the logic so that only one actor can occupy any given grid cell at a time. Dropping an actor onto an already occupied cell will replace the existing actor.

By the end, you'll understand:

  • How to define and configure a grid system.
  • The math behind snapping coordinates to a grid.
  • Modifying tooltip and actor placement logic for snapping.
  • Enforcing single occupancy per grid cell.
Diagram showing actors snapped to a grid

Snapping to Grid Positions

Our first step towards a grid-based system is configuring the grid itself. In Config.h, inside the Config::Editor namespace, we'll add constants defining the snapping distances. HORIZONTAL_GRID_SNAP will be 50, and VERTICAL_GRID_SNAP will be 25, reflecting the dimensions of our game's blocks.

We also need to define how many grid cells make up our level. Let's add GRID_WIDTH and GRID_HEIGHT. We’ll set them to 13 and 6 for our examples, but our program will scale to any values we prefer here.

We'll use the Uint8 type for these. This is an 8-bit unsigned integer, meaning it can hold values from 0 to 255. Using a fixed-size type like Uint8 is important because we intend to serialize these dimensions to a file later; it ensures the data size is predictable. If we needed larger levels, Uint16 would be the next choice.

We’ll also update the level's rendering size - LEVEL_WIDTH and LEVEL_HEIGHT - to be based on these new values. This ensures that the level's rendering area in the editor always matches the space required by the defined grid. Any changes to the grid configuration (number of cells or snap size) will automatically propagate to the level's dimensions.

// Config.h
// ...

#ifdef WITH_EDITOR
namespace Config::Editor {
// Level
inline const int HORIZONTAL_GRID_SNAP{50};
inline const int VERTICAL_GRID_SNAP{25};
inline const Uint8 GRID_WIDTH{13};
inline const Uint8 GRID_HEIGHT{6};
inline const int LEVEL_WIDTH{
  HORIZONTAL_GRID_SNAP * GRID_WIDTH};
inline const int LEVEL_HEIGHT{
  VERTICAL_GRID_SNAP * GRID_HEIGHT};
inline constexpr SDL_Color LEVEL_BACKGROUND{
  50, 50, 50, 255};

// ...
}
// ...
#endif

Now that we have the grid dimensions configured, we need a function within our Level class to perform the actual snapping calculation. This function will take an arbitrary coordinate (x, y) and return the coordinates of the top-left corner of the grid cell that contains it.

Let's declare this function in Editor/Level.h. We'll add a public member function SnapToGridPosition() that accepts two int parameters (x and y) and returns an SDL_Point, which holds the resulting snapped x and y coordinates.

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

namespace Editor {
 // ...

class Level {
 public:
  // ...
  SDL_Point SnapToGridPosition(int x, int y);
  // ...
};
}

Now let's implement SnapToGridPosition() in Editor/Source/Level.cpp.

The logic relies on integer division. When we divide the input coordinate (e.g., x) by the corresponding grid snap size (sx), integer division automatically truncates any remainder, effectively rounding down to the nearest whole number of grid steps.

Multiplying this result back by the snap size (sx) gives us the coordinate of the grid line just below or at the input coordinate. We do this for both x and y using HORIZONTAL_GRID_SNAP and VERTICAL_GRID_SNAP from our configuration, and return the result as an SDL_Point.

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

SDL_Point Level::SnapToGridPosition(
  int x, int y
) {
  using namespace Config::Editor;
  int sx{HORIZONTAL_GRID_SNAP};
  int sy{VERTICAL_GRID_SNAP};
  return {
    (x / sx) * sx,
    (y / sy) * sy,
  };
}

Why does this Logic Snap to a Grid?

The formula (x / sx) * sx works because of how integer division behaves in C++ (and many other languages). When you divide two integers, the result is also an integer, and any fractional part is simply discarded (truncated towards zero).

For example, let’s imagine x is 20 and our horizontal snapping value sx is 50. In that scenario, (x / sx) * sx becomes (20 / 50) * 50). (20 / 50) returns 0, and 0 * 50 returns 0.

So, if our original x value was 20, then the x returned from SnapToGridPosition() will be 0. Here are some more examples, so we can see the grid snapping behaviour where values are returned in multiples of 50:

  • If x is 49, then 49 / 50 is 0, and 0 * 50 returns 0
  • If x is 51, then 51 / 50 is 1, and 1 * 50 returns 50
  • If x is 75, then 75 / 50 is 1, and 1 * 50 returns 50
  • If x is 124, then 124 / 50 is 2, and 2 * 50 returns 100

As you can see, this process finds the largest multiple of the snap size (sx) that is less than or equal to the input coordinate (x).

Snapping the Tooltip to the Grid

Now that our Level can calculate snapped positions, let's apply this to the ActorTooltip. We want the tooltip to snap to the grid, but only when the mouse cursor is actually hovering over the level area. When the cursor is outside the level (e.g., over the actor menu or elsewhere on the screen entirely), the tooltip should follow the mouse smoothly as before.

We can achieve this conditional behavior in ActorTooltip::PositionWindow(). We already have an if statement checking ParentScene.GetLevel().HasMouseFocus().

The logic for smooth, non-snapped positioning (using the DragOffset) belongs in the else block – the case where the mouse is not over the level.

Let's refactor the code. We’ll move the lines that calculate the position using DragOffset and the corresponding SDL_SetWindowPosition() call inside the else block.

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

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

    SDL_SetWindowPosition(
      SDLWindow,
      x - DragOffsetX,
      y - DragOffsetY
    );
    SDL_SetWindowOpacity(SDLWindow, 0.5);
    SDL_SetCursor(DenyCursor);
  }
}

Now, let's add the code to the if block for when the tooltip should snap. We face a slight complication: the tooltip's position is set using global screen coordinates - SDL_SetWindowPosition() - but our SnapToGridPosition() function works with coordinates relative to the level area within our main window.

To bridge this, we first need the screen coordinates of our main window's top-left corner. We can get this from the ParentScene, storing the result in WinX and WinY.

Next, we calculate the mouse's position relative to this window by subtracting WinX and WinY from the global mouse coordinates (x, y). We pass these relative coordinates (x - WinX, y - WinY) to SnapToGridPosition().

This gets what the grid-snapped tooltip position would be if our main window was positioned at (0, 0) within the overall screen. We’ll store those values at GridX, GridY.

However, our main window is unlikely to be positioned at (0, 0) so, when setting the final position of our tooltip, we need to add the window’s actual position back to our final result. So, we call SDL_SetWindowPosition() , passing WinX + GridX and WinY + GridY.

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

void ActorTooltip::PositionWindow() {
  int x, y;
  SDL_GetGlobalMouseState(&x, &y);

  if (ParentScene.GetLevel().HasMouseFocus()) {
    SDL_SetWindowOpacity(SDLWindow, 1);
    SDL_SetCursor(SDL_GetDefaultCursor());
    auto [WinX, WinY]{
      ParentScene.GetWindow().GetPosition()
    };

    auto [GridX, GridY]{
      ParentScene.GetLevel().SnapToGridPosition(
        x - WinX, y - WinY
      )
    };

    SDL_SetWindowPosition(
      SDLWindow, WinX + GridX, WinY + GridY
    );
  } else {
} }

Don’t worry if this transformation between coordinate systems doesn’t entirely make sense at this point. We have dedicated lessons on coordinate systems, and transformations between them, coming later in the course.

If we run our program now, we should see our tooltip snapping to grid positions as we drag our actor over the level. However, once we drop the actor, its position in the level is not snapped to that same grid, so let’s address that next.

Snapping Actors to the Grid

Our final step for snapping is to ensure the actors themselves land on the grid coordinates when dropped. This requires updating the Level::HandleDrop() function.

We previously used the mouse position and a drag offset to determine the drop location. Now, we want to ignore the drag offset and use only the snapped grid position.

We’ll modify HandleDrop() to delete the code that gets DragOffset. After getting the window-relative MouseX and MouseY, we’ll call SnapToGridPosition() to calculate GridX and GridY.

We’ll then update both SetPosition() calls (inside the if for cloned actors and the else for moved actors) to use GridX, GridY as the target coordinates.

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

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

  int MouseX, MouseY;
  SDL_GetMouseState(&MouseX, &MouseY);
  auto [GridX, GridY]{
    SnapToGridPosition(
      MouseX, MouseY
    )
  };

  using enum ActorLocation;
  if (DragActor->GetLocation() == Menu) {
    ActorPtr NewActor{DragActor->Clone()};
    NewActor->SetPosition(GridX, GridY);
    SelectedActor = NewActor.get();
    AddToLevel(std::move(NewActor));
  } else {
    DragActor->SetPosition(GridX, GridY);
    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;
}

SDL_Point Level::SnapToGridPosition(
  int x, int y
) {
  using namespace Config::Editor;
  int sx{HORIZONTAL_GRID_SNAP};
  int sy{VERTICAL_GRID_SNAP};
  return {
    (x / sx) * sx,
    (y / sy) * sy,
  };
}

Running our program, we should now see we can easily position our actors in an organized grid:

Diagram showing actors snapped to a grid

One Actor Per Grid Position

Our editor now snaps actors to a grid, but it still allows multiple actors to occupy the same grid cell. For many tile or grid-based games, this is undesirable; each cell should hold at most one actor. We need to enforce this constraint.

The desired behavior is: when an actor is dropped onto a grid cell, any actor already present in that cell should be removed first. However, if the user simply clicks an actor and releases (effectively dropping it back into its original cell), we shouldn't delete and replace it.

To implement this, we'll add a helper function to the Level class specifically for deleting an actor at a given grid position, with an exception for the actor being dropped.

Let's declare DeleteAtPosition() in the public section of Editor/Level.h. The Unless parameter will hold a pointer to the actor currently being dragged/dropped, preventing it from deleting itself if dropped in its own previous location.

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

namespace Editor {
// ...

class Level {
 public:
  // ...
  void DeleteAtPosition(
    int x, int y, const Actor* Unless);
  // ...
};
}

Now, let's implement DeleteAtPosition() in Editor/Source/Level.cpp. We need to search through our Actors vector to find if any actor's position matches the provided x and y.

We can use a traditional for loop with an index. Inside the loop, we’ll get the position (ax, ay) of the current actor Actors[i]. We’ll check if ax == x and ay == y. We’ll check if the raw pointer Actors[i].get() is not equal to the Unless pointer.

If all conditions are true, we've found a different actor at the target location that needs to be removed. We use Actors.erase() to remove it and immediately break from the loop, as there can be at most one actor per position now.

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

void Level::DeleteAtPosition(
  int x, int y, const Actor* Unless
) {
  for (size_t i{0}; i < Actors.size(); ++i) {
    auto [ax, ay]{Actors[i]->GetPosition()};
    if (ax == x && ay == y
        && Actors[i].get() != Unless
    ) {
      Actors.erase(Actors.begin() + i);
      break;
    }
  }
}

Advanced: std::erase_if() and Lambdas

Solving this problem using the std::erase_if() and lambda approach we introduced in the previous lesson would look like this:

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

void Level::DeleteAtPosition(
  int x, int y, const Actor* Unless
) {
  std::erase_if(Actors,
    [&](const ActorPtr& Actor){
      auto [ax, ay]{Actor->GetPosition()};
      return
        ax == x &&
        ay == y &&
        Actor.get() != Unless;
     }
  );
}

Finally, we integrate the deletion logic into the drop handling process. In Level::HandleDrop(), right after calculating the target grid coordinates GridX, GridY, we need to call our new deletion function.

We’ll add our call to DeleteAtPosition() to ensure that, before we either clone DragActor to (GridX, GridY) or move DragActor to (GridX, GridY), any other actor currently occupying that cell is removed.

The DragActor pointer is passed as the Unless argument, preventing self-deletion when moving an actor back to its original cell or just clicking it.

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

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

  int MouseX, MouseY;
  SDL_GetMouseState(&MouseX, &MouseY);
  auto [GridX, GridY]{
    SnapToGridPosition(
      MouseX,
      MouseY
    )
  };
  DeleteAtPosition(GridX, GridY, DragActor);
  
}

If we compile and run our program, we should now verify that, when we drop an actor into a grid position, any other actor that was previously in that grid position gets replaced.

Complete Code

Our updated Level class and Config.h file are provided below:

#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 HORIZONTAL_GRID_SNAP{50};
inline const int VERTICAL_GRID_SNAP{25};
inline const Uint8 GRID_WIDTH{13};
inline const Uint8 GRID_HEIGHT{6};
inline const int LEVEL_WIDTH{
  HORIZONTAL_GRID_SNAP * GRID_WIDTH};
inline const int LEVEL_HEIGHT{
  VERTICAL_GRID_SNAP * GRID_HEIGHT};
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 <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);
  SDL_Point SnapToGridPosition(int x, int y);
  void DeleteAtPosition(
    int x, int y, const Actor* Unless);

 private:
  Scene& ParentScene;
  ActorPtrs Actors;
  Actor* SelectedActor{nullptr};

  SDL_Rect Rect{
    0, 0,
    Config::Editor::LEVEL_WIDTH,
    Config::Editor::LEVEL_HEIGHT
  };
};
}
#include <ranges>
#include "Editor/Level.h"
#include "Editor/Scene.h"

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 [GridX, GridY]{
    SnapToGridPosition(
      MouseX,
      MouseY
    )
  };
  DeleteAtPosition(GridX, GridY, DragActor);

  using enum ActorLocation;
  if (DragActor->GetLocation() == Menu) {
    ActorPtr NewActor{DragActor->Clone()};
    NewActor->SetPosition(GridX, GridY);
    SelectedActor = NewActor.get();
    AddToLevel(std::move(NewActor));
  } else {
    DragActor->SetPosition(GridX, GridY);
    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;
}

SDL_Point Level::SnapToGridPosition(
  int x, int y
) {
  using namespace Config::Editor;
  int sx{HORIZONTAL_GRID_SNAP};
  int sy{VERTICAL_GRID_SNAP};
  return {
    (x / sx) * sx,
    (y / sy) * sy,
  };
}

void Level::DeleteAtPosition(
  int x, int y, const Actor* Unless
) {
  std::erase_if(Actors,
    [&](const ActorPtr& Actor){
      auto [ax, ay]{Actor->GetPosition()};
      return
        ax == x &&
        ay == y &&
        Actor.get() != Unless;
     }
  );
}

We also updated the PositionWindow() function of our ActorTooltip:

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

  if (ParentScene.GetLevel().HasMouseFocus()) {
    SDL_SetWindowOpacity(SDLWindow, 1);
    SDL_SetCursor(SDL_GetDefaultCursor());
    auto [WinX, WinY]{
      ParentScene.GetWindow().GetPosition()
    };

    auto [GridX, GridY]{
      ParentScene.GetLevel().SnapToGridPosition(
        x - WinX, y - WinY
      )
    };

    SDL_SetWindowPosition(
      SDLWindow, WinX + GridX, WinY + GridY
    );
  } else {
    auto [DragOffsetX, DragOffsetY]{
      DragActor->GetDragOffset()
    };

    SDL_SetWindowPosition(
      SDLWindow, x - DragOffsetX, y - DragOffsetY
    );
    SDL_SetWindowOpacity(SDLWindow, 0.5);
    SDL_SetCursor(DenyCursor);
  }
}

void ActorTooltip::HandleEvent(
  const SDL_Event& E) {
  using namespace UserEvents;
  if (E.type == ACTOR_DRAG) {
    DragActor = static_cast<Actor*>(
      E.user.data1
    );
    SDL_SetWindowSize(
      SDLWindow,
      DragActor->GetRect().w,
      DragActor->GetRect().h
    );
    SetIsVisible(true);
  }
}

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

Summary

This lesson transitioned our level editor from freeform placement to a structured grid-based system. We implemented grid snapping for precise alignment and enforced a rule allowing only one actor per grid cell, replacing any existing actor upon dropping a new one.

Key steps:

  • Defined grid parameters in configuration.
  • Used fixed-width integers (Uint8) for grid dimensions to prepare for serialization.
  • Implemented a SnapToGridPosition() function using integer division math: (coord/snap)×snap(\text{coord} / \text{snap}) \times \text{snap}.
  • Updated ActorTooltip::PositionWindow() to handle coordinate space conversions (global mouse -> window relative -> snapped -> window relative -> screen) for snapped tooltip positioning over the level.
  • Modified Level::HandleDrop() to use snapped coordinates and ignore the drag offset.
  • Added Level::DeleteAtPosition() to remove existing actors from a target cell before placing a new one.
  • Integrated the deletion call into HandleDrop() to enforce single occupancy per cell.
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