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.
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 pixelsWe'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
};
// ...
}
// ...
Now, we'll create a class for our Minesweeper Cell. Here are some key points about the implementation:
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.Row
and Col
arguments, so the cell knows where it is within the grid.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);
}
Next, we'll implement our MinesweeperGrid
class. Here are the important aspects of this class:
x
and y
arguments, controlling where the grid should be drawn on the surface.MinesweeperCell
objects, based on the configuration options in our Globals.h
file.std::vector
called Children
.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.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;
};
We'll now update our MinesweeperUI
class to construct the MinesweeperGrid
. Here's what you need to know:
MinesweeperUI
has Render()
and HandleEvent()
methods, which are being called from the main event loop.MinesweeperGrid
component.MinesweeperGrid
component is then forwarding those HandleEvent
and Render()
calls to all of its Children
- i.e., the MinesweeperCell
objects.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:
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};
// ...
}
// ...
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);
}
// ...
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.
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:
ClearCell()
is called within any of our cells, an SDL_Event
is pushed into the queue.main.cpp
) receives this event and forwards it to the HandleEvent()
method of our MinesweeperUI
object.MinesweeperUI
forwards the event to the HandleEvent()
method of our MinesweeperGrid
object.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 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.
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:
Globals.h
MinesweeperCell
class with clearing functionalityMinesweeperGrid
class to manage all cellsIn 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.
Learn C++ and SDL development by creating hands on, practical projects inspired by classic retro games