Creating the Grid

Building a two-dimensional grid of interactive minesweeper cells
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

Free, Unlimited Access
Abstract art representing computer programming
Ryan McCombe
Ryan McCombe
Posted

In this lesson, we'll focus on creating the foundational structure for our Minesweeper game. We'll implement the grid system and individual cells, setting the stage for bomb placement and game logic in future lessons.

We'll start by setting up the necessary parameters in our global configuration, then move on to creating the MinesweeperCell and MinesweeperGrid classes.

Finally, we'll implement basic interaction functionality, allowing players to clear cells and prepare our system for future bomb placement.

Updating Globals

We'll begin by updating our Globals.h file with new variables that will define the structure and appearance of our game:

  • GRID_COLUMNS and GRID_ROWS determine the number of cells in each dimension. We're creating an 8-column by 4-row grid in this example, but the project can easily adapt to different grid sizes by adjusting these values.
  • PADDING represents the visual spacing between elements in our UI.
  • CELL_SIZE represents the width and height of each cell in pixels. In this case, each cell will be 50x50 pixels

We'll also add GRID_HEIGHT and GRID_WIDTH variables to calculate the visual size of our grid based on these options.

Finally, we'll update the WINDOW_HEIGHT and WINDOW_WIDTH variables to be large enough to contain our grid, with additional padding around the edge.

These variables will provide a flexible foundation for our game, allowing easy adjustments to the game's appearance and structure.

// Globals.h

// ...

namespace Config{
  inline const std::string GAME_NAME{
    "Minesweeper"};
  inline constexpr int GRID_COLUMNS{8};
  inline constexpr int GRID_ROWS{4};

  // Size and Positioning
  inline constexpr int PADDING{5};
  inline constexpr int CELL_SIZE{50};

  inline constexpr int GRID_HEIGHT{
    CELL_SIZE * GRID_ROWS
    + PADDING * (GRID_ROWS - 1)
  };

  inline constexpr int GRID_WIDTH{
    CELL_SIZE * GRID_COLUMNS +
    PADDING * (GRID_COLUMNS - 1)};

  inline constexpr int WINDOW_HEIGHT{
    GRID_HEIGHT + PADDING * 2
  };
  inline constexpr int WINDOW_WIDTH{
    GRID_WIDTH + PADDING * 2
  };
  
  // ...
}

// ...

Creating Cells

Now, we'll create a class for our Minesweeper Cell. Here are some key points about the implementation:

  • We expect this class to become quite large, so we'll preemptively separate it into a header (.h) and implementation (.cpp) file.
  • The cell inherits from Engine::Button, which in turn inherits from Engine::Rectangle. As such, our constructor will accept x and y arguments to control the position of the cell, and w and h arguments to control the size.
  • We'll also accept Row and Col arguments, so the cell knows where it is within the grid.
  • We'll override the HandleEvent and Render methods. For now, they just call the base implementation on the Engine::Button class, but we'll expand them soon.

This structure allows our cells to behave like buttons while also maintaining their position within the Minesweeper grid.

// Minesweeper/Cell.h
#pragma once
#include "Engine/Button.h"

class MinesweeperCell : public Engine::Button {
public:
  MinesweeperCell(
    int X, int Y, int W, int H, int Row, int Col
  );

  void HandleEvent(const SDL_Event& E) override;
  void Render(SDL_Surface* Surface) override;

  [[nodiscard]]
  int GetRow() const{ return Row; }

  [[nodiscard]]
  int GetCol() const{ return Col; }

private:
  int Row;
  int Col;
};
// Minesweeper/Cell.cpp
#include <iostream>
#include "Minesweeper/Cell.h"
#include "Globals.h"

MinesweeperCell::MinesweeperCell(
  int x, int y, int w, int h, int Row, int Col)
  : Button{x, y, w, h}, Row{Row}, Col{Col} {};

void MinesweeperCell::HandleEvent(
  const SDL_Event& E){
  Button::HandleEvent(E);
}

void MinesweeperCell::Render(
  SDL_Surface* Surface){
  Button::Render(Surface);
}

Creating the Grid

Next, we'll implement our MinesweeperGrid class. Here are the important aspects of this class:

  • The constructor accepts x and y arguments, controlling where the grid should be drawn on the surface.
  • The constructor creates a two-dimensional grid of MinesweeperCell objects, based on the configuration options in our Globals.h file.
  • It stores all these cells in a std::vector called Children.
  • The x, y, w, h, row, and col arguments for each invocation of the MinesweeperCell constructor are being calculated in the loop body. This calculation ensures that each cell is positioned correctly within the grid, taking into account the cell size and padding.
  • It has public Render() and HandleEvent() methods. These methods iterate over all the MinesweeperCell objects in the grid, and call their Render() and HandleEvent() methods respectively.

This grid class will manage the collection of cells and handle the rendering and event distribution for the entire Minesweeper board.

// Minesweeper/Grid.h
#pragma once
#include <vector>
#include "Globals.h"
#include "Minesweeper/Cell.h"

class MinesweeperGrid {
public:
  MinesweeperGrid(int x, int y){
    using namespace Config;
    Children.reserve(GRID_COLUMNS * GRID_ROWS);
    for (int Col{1}; Col <= GRID_COLUMNS; ++Col) {
      for (int Row{1}; Row <= GRID_ROWS; ++Row) {
        constexpr int Spacing{CELL_SIZE + PADDING};
        Children.emplace_back(
          x + (Spacing) * (Col - 1),
          y + (Spacing) * (Row - 1),
          CELL_SIZE, CELL_SIZE, Row, Col
        );
      }
    }
  }

  void Render(SDL_Surface* Surface){
    for (auto& Child : Children) {
      Child.Render(Surface);
    }
  }

  void HandleEvent(const SDL_Event& E){
    for (auto& Child : Children) {
      Child.HandleEvent(E);
    }
  }

  std::vector<MinesweeperCell> Children;
};

Passing Through Events

We'll now update our MinesweeperUI class to construct the MinesweeperGrid. Here's what you need to know:

  • The MinesweeperUI has Render() and HandleEvent() methods, which are being called from the main event loop.
  • The class is now forwarding those calls to the MinesweeperGrid component.
  • As we saw above, the MinesweeperGrid component is then forwarding those HandleEvent and Render() calls to all of its Children - i.e., the MinesweeperCell objects.
  • The MinesweeperGrid is being constructed by passing the Padding value from our config, causing the Grid to be drawn slightly offset from the top left edge of the window surface.

This structure ensures that events are properly distributed throughout our game components, and that everything is rendered in the correct position.

// Minesweeper/UI.h
#pragma once
#include "Globals.h"
#include "Minesweeper/Grid.h"

class MinesweeperUI {
public:
  void Render(SDL_Surface* Surface){
    Grid.Render(Surface);
  }

  void HandleEvent(const SDL_Event& E){
    Grid.HandleEvent(E);
  }

private:
  MinesweeperGrid Grid{
    Config::PADDING, Config::PADDING
  };
};

Running our program, we should now see a grid of buttons, responding to our cursor:

Screenshot of our program

Clearing Cells

Now we'll expand our MinesweeperCell objects to let users "clear" them by left-clicking. We'll implement this using a ClearCell() method and an isCleared member variable. Our Engine::Button class has a virtual HandleLeftClick() method which we can override.

// Minesweeper/Cell.h
#pragma once
// ...

class MinesweeperCell : public Engine::Button {
public:  
  // ...

protected:
  void HandleLeftClick() override;

private:
  void ClearCell();
  bool isCleared{false};
  // ...
};

In the implementation of these functions:

  • SetIsDisabled() is inherited from Engine::Button, and will cause our object to stop reacting to mouse events.
  • SetColor is inherited from Engine::Rectangle (via Engine::Button) and will change the color of our button. We change the color to a new value that we'll add to the Config namespace in our Globals.h.

This implementation allows users to interact with our cells, changing their state when clicked.

// Minesweeper/Cell.cpp

// ...

void MinesweeperCell::ClearCell(){
  if (isCleared) return;
  isCleared = true;
  SetIsDisabled(true);
  SetColor(Config::BUTTON_CLEARED_COLOR);
}

void MinesweeperCell::HandleLeftClick(){
  ClearCell();
}

// ...
// Globals.h
// ...

namespace Config{
  // ...
  inline constexpr SDL_Color BUTTON_CLEARED_COLOR{
    240, 240, 240, 255};
  // ...
}
// ...

Reporting Cell Cleared

A cell being cleared is an important action in the context of the wider game - for example, it can trigger a game-over state if the cell contains a mine, and adjacent cells may also need to react. To implement the gameplay, we'll need to notify other components every time a cell is cleared, so we'll use the SDL event loop.

We'll add our first custom event type to our UserEvents namespace:

// Globals.h

// ...

namespace UserEvents{
  inline Uint32 CELL_CLEARED =
    SDL_RegisterEvents(1);
}

// ...

As we develop our game, cells will need to report various events. To streamline this process, let's create a ReportEvent() helper method.

This method will accept an event type parameter, which SDL represents as a 32-bit unsigned integer. Here's how we'll define it in our MinesweeperCell class:

// Minesweeper/Cell.h

// ...

#pragma once
// ...

class MinesweeperCell : public Engine::Button {
public:
  // ...

protected:
  // ...

private:
  void ReportEvent(uint32_t EventType);
  // ...
};

In the implementation, we’ll create an event of that type. We’ll also attach a pointer to ourselves using the this keyword, so consumers of the event can understand which cell was cleared. We’ll finally dispatch the event into the queue using SDL_PushEvent():

// Minesweeper/Cell.cpp

// ...

void MinesweeperCell::ReportEvent(
  uint32_t EventType){
  SDL_Event event{EventType};
  event.user.data1 = this;
  SDL_PushEvent(&event);
}

// ...

Finally, we’ll update our ClearCell() implementation to call our ReportEvent() function, passing the correct type.

We’ll also update HandleEvent() to react to a cell being cleared. For now, we’ll just log out a message, but we’ll build upon this later:

// Minesweeper/Cell.cpp

// ...

void MinesweeperCell::ClearCell(){
  if (isCleared) return;
  isCleared = true;
  SetIsDisabled(true);
  SetColor(Config::BUTTON_CLEARED_COLOR);
  ReportEvent(UserEvents::CELL_CLEARED); 
}

void MinesweeperCell::HandleEvent(
  const SDL_Event& E){
  if (E.type == UserEvents::CELL_CLEARED) {
    // TODO
    std::cout << "A Cell Was Cleared\n";
  }
  Button::HandleEvent(E);
}

// ...

Testing

At this point, you should test the application to verify it's working correctly. You should see the grid as before, but now, left-clicking on a cell will clear it, causing it to change color and stop reacting to future mouse events.

Screenshot of our program

Additionally, check the terminal to verify the event handling is working correctly. You should see "A Cell Was Cleared" logged out multiple times whenever you clear a cell.

A Cell Was Cleared
A Cell Was Cleared
A Cell Was Cleared
// ...

It's important to understand why multiple log messages appear for each cleared cell:

  1. When ClearCell() is called within any of our cells, an SDL_Event is pushed into the queue.
  2. The event loop (in main.cpp) receives this event and forwards it to the HandleEvent() method of our MinesweeperUI object.
  3. The MinesweeperUI forwards the event to the HandleEvent() method of our MinesweeperGrid object.
  4. The MinesweeperGrid forwards the event to the HandleEvent() method of every MinesweeperCell object in its Children array, allowing each cell to react to the event if necessary.

This is why "A Cell Was Cleared" is logged out multiple times. A single left click clears a single cell, but every cell is notified of the action and can react accordingly.

Complete Code

Complete versions of the files we changed in this part are available below

#pragma once

#define SHOW_DEBUG_HELPERS

#include <iostream>
#include <SDL.h>
#include <string>

namespace UserEvents{
  inline Uint32 CELL_CLEARED =
    SDL_RegisterEvents(1);
}

namespace Config{
  // Game Settings
  inline const std::string GAME_NAME{
    "Minesweeper"};
  inline constexpr int GRID_COLUMNS{8};
  inline constexpr int GRID_ROWS{4};

  // Size and Positioning
  inline constexpr int PADDING{5};
  inline constexpr int CELL_SIZE{50};

  inline constexpr int GRID_HEIGHT{
    CELL_SIZE * GRID_ROWS
    + PADDING * (GRID_ROWS - 1)
  };

  inline constexpr int GRID_WIDTH{
    CELL_SIZE * GRID_COLUMNS +
    PADDING * (GRID_COLUMNS - 1)};

  inline constexpr int WINDOW_HEIGHT{
    GRID_HEIGHT + PADDING * 2
  };

  inline constexpr int WINDOW_WIDTH{
    GRID_WIDTH + PADDING * 2};

  // Colors
  inline constexpr SDL_Color BACKGROUND_COLOR{
    170, 170, 170, 255};
  inline constexpr SDL_Color BUTTON_COLOR{
    200, 200, 200, 255};
  inline constexpr SDL_Color BUTTON_HOVER_COLOR{
    220, 220, 220, 255};
  inline constexpr SDL_Color
  BUTTON_CLEARED_COLOR{
    240, 240, 240, 255};

  // Asset Paths
  inline const std::string FONT{
    "Rubik-SemiBold.ttf"};
}

namespace Utils{
#ifdef SHOW_DEBUG_HELPERS
  inline void CheckSDLError(
    const std::string& Msg){
    const char* error = SDL_GetError();
    if (*error != '\0') {
      std::cerr << Msg << " Error: " << error <<
        '\n';
      SDL_ClearError();
    }
  }
#endif
}
#pragma once
#include "Globals.h"
#include "Minesweeper/Grid.h"

class MinesweeperUI {
public:
  void Render(SDL_Surface* Surface){
    Grid.Render(Surface);
  }

  void HandleEvent(const SDL_Event& E){
    Grid.HandleEvent(E);
  }

private:
  MinesweeperGrid Grid{
    Config::PADDING, Config::PADDING
  };
};
#pragma once
#include <vector>
#include "Globals.h"
#include "Minesweeper/Cell.h"

class MinesweeperGrid {
public:
  MinesweeperGrid(int x, int y){
    using namespace Config;
    Children.reserve(GRID_COLUMNS * GRID_ROWS);
    for (int Col{1}; Col <= GRID_COLUMNS; ++
         Col) {
      for (int Row{1}; Row <= GRID_ROWS; ++
           Row) {
        constexpr int Spacing{
          CELL_SIZE + PADDING};
        Children.emplace_back(
          x + (Spacing) * (Col - 1),
          y + (Spacing) * (Row - 1),
          CELL_SIZE, CELL_SIZE, Row, Col
        );
      }
    }
  }

  void Render(SDL_Surface* Surface){
    for (auto& Child : Children) {
      Child.Render(Surface);
    }
  }

  void HandleEvent(const SDL_Event& E){
    for (auto& Child : Children) {
      Child.HandleEvent(E);
    }
  }

  std::vector<MinesweeperCell> Children;
};
#pragma once
#include "Engine/Button.h"
#include "Engine/Image.h"

class MinesweeperCell : public Engine::Button {
public:
  MinesweeperCell(
    int X, int Y, int W, int H, int Row, int Col
  );

  void HandleEvent(const SDL_Event& E) override;
  void Render(SDL_Surface* Surface) override;

  [[nodiscard]]
  int GetRow() const{ return Row; }

  [[nodiscard]]
  int GetCol() const{ return Col; }

protected:
  void HandleLeftClick() override;

private:
  void ClearCell();
  void ReportEvent(uint32_t EventType);

  int Row;
  int Col;
  bool isCleared{false};
};
#include <iostream>
#include "Minesweeper/Cell.h"
#include "Globals.h"

MinesweeperCell::MinesweeperCell(
  int x, int y, int w, int h, int Row, int Col)
  : Button{x, y, w, h}, Row{Row}, Col{Col} {};

void MinesweeperCell::HandleEvent(
  const SDL_Event& E){
  if (E.type == UserEvents::CELL_CLEARED) {
    // TODO
    std::cout << "A Cell Was Cleared\n";
  }
  Button::HandleEvent(E);
}

void MinesweeperCell::Render(
  SDL_Surface* Surface){
  Button::Render(Surface);
}

void MinesweeperCell::ClearCell(){
  if (isCleared) return;
  isCleared = true;
  SetIsDisabled(true);
  SetColor(Config::BUTTON_CLEARED_COLOR);
  ReportEvent(UserEvents::CELL_CLEARED);
}

void MinesweeperCell::ReportEvent(
  uint32_t EventType){
  SDL_Event event{EventType};
  event.user.data1 = this;
  SDL_PushEvent(&event);
}

void MinesweeperCell::HandleLeftClick(){
  ClearCell();
}

Files not listed above have not been changed since the previous section.

Summary

In this lesson, we've laid the groundwork for our Minesweeper game by implementing the grid system and individual cells. We've set up the global configuration, created classes for cells and the grid, and implemented basic interaction functionality.

Key progress in this lesson includes:

  1. Defining the game's structure in Globals.h
  2. Creating the MinesweeperCell class with clearing functionality
  3. Implementing the MinesweeperGrid class to manage all cells
  4. Setting up event handling and propagation through the game components
  5. Implementing a custom event system for cell clearing

In the next lesson, we'll build upon these foundations and place bombs randomly within our grid. We'll implement the logic for bomb placement, update our cell-clearing functionality to handle bombs, and begin implementing the core game mechanics.

Was this lesson useful?

Next Lesson

Adding Bombs to the Minesweeper Grid

Updating the game to to place bombs randomly in the grid and render them when cells are cleared.
Abstract art representing computer programming
New: AI-Powered AssistanceAI Assistance

Questions and HelpNeed Help?

Get instant help using our free AI assistant, powered by state-of-the-art language models.

Ryan McCombe
Ryan McCombe
Posted
Lesson Contents

Creating the Grid

Building a two-dimensional grid of interactive minesweeper cells

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

Free, Unlimited Access
Project: Making Minesweeper
  • 53.GPUs and Rasterization
  • 54.SDL Renderers
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

Free, unlimited access

This course includes:

  • 55 Lessons
  • 100+ Code Samples
  • 91% Positive Reviews
  • Regularly Updated
  • Help and FAQ
Next Lesson

Adding Bombs to the Minesweeper Grid

Updating the game to to place bombs randomly in the grid and render them when cells are cleared.
Abstract art representing computer programming
Contact|Privacy Policy|Terms of Use
Copyright © 2024 - All Rights Reserved