In this series, we'll build a fully functional Minesweeper game from scratch, giving you hands-on experience with game development concepts and C++ programming.
We'll separate our project into two main parts:
For example, we'll create a general Button
class in our engine that can be used across various projects. The cells of our Minesweeper grid will then inherit from this Button
class, expanding it with Minesweeper-specific logic such as whether the cell contains a bomb and counting the number of bombs in adjacent cells.
This separation offers several benefits:
Over time, this approach will make our generic engine code more powerful, which future projects can also benefit from. It's a great way to build a personal library of reusable game components.
In this lesson, we'll introduce all of the engine code. While it might seem like a lot of code at first, you'll find that it's all built on concepts we've covered in previous sections of the course. Don't worry if you feel overwhelmed - future lessons will slow down and take things step by step as we build the new, Minesweeper-specific functionality.
Let's dive in and explore our starting point
Globals.h
We'll start by creating a Globals.h
header file to store variables and functionality that are useful across a wide variety of files in our project. This includes:
UserEvents
namespace. This namespace is currently empty, but we'll add to it as needed in future parts of the tutorial.Config
namespace. This includes settings like the game name, window dimensions, and color schemes.Utils
(utilities) namespace.An important feature to note is the SHOW_DEBUG_HELPERS
definition. This definition enables extra functionality that will be useful when we're developing and debugging the project. For example, it allows us to use the Utils::CheckSDLError
function to print detailed error messages during development.
By organizing our global definitions and utilities in this way, we create a central place for important constants and helper functions, making our code more organized and easier to maintain.
#pragma once
#define SHOW_DEBUG_HELPERS
#include <iostream>
#include <SDL.h>
#include <string>
namespace UserEvents{}
namespace Config{
// Game Settings
inline const std::string GAME_NAME{
"Minesweeper"};
// Size and Positioning
inline constexpr int WINDOW_HEIGHT{200};
inline constexpr int WINDOW_WIDTH{400};
// 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};
// 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
}
Note that the CheckSDLError()
function in this file is something we created in our earlier lesson on error handling:
Engine/Window.h
The first component of our engine is the Window
class, which is responsible for creating and managing SDL_Window
objects. This class, like all our engine components, is stored within the Engine
namespace and located in the Engine/
directory of our project.
The Window
class provides the following functionality:
Globals.h
file.Render
method that fills the window with the background color.Update
method that refreshes the window surface.GetSurface
method that returns the SDL surface associated with the window.This class encapsulates all the basic window management functionality we'll need for our game, providing a clean interface for creating and manipulating our game window:
#pragma once
#include <SDL.h>
#include "Globals.h"
namespace Engine{
class Window {
public:
Window(){
SDLWindow = SDL_CreateWindow(
Config::GAME_NAME.c_str(),
SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
Config::WINDOW_WIDTH,
Config::WINDOW_HEIGHT, 0
);
}
void Render(){
SDL_FillRect(
GetSurface(), nullptr,
SDL_MapRGB(GetSurface()->format,
Config::BACKGROUND_COLOR.r,
Config::BACKGROUND_COLOR.g,
Config::BACKGROUND_COLOR.b));
}
void Update(){
SDL_UpdateWindowSurface(SDLWindow);
}
SDL_Surface* GetSurface(){
return SDL_GetWindowSurface(SDLWindow);
}
~Window() {
SDL_DestroyWindow(SDLWindow);
}
Window(const Window&) = delete;
Window& operator=(const Window&) = delete;
private:
SDL_Window* SDLWindow;
};
}
This Window
class is similar to what we created in our earlier lesson on SDL windows:
Engine/Rectangle.h
The Rectangle
class is a fundamental building block in our engine, used for drawing rectangles on the screen. It provides the following key features:
Render
method for drawing the rectangle on an SDL surface.This class will serve as the base for more complex visual elements in our game, such as buttons and cells in the Minesweeper grid.
#pragma once
#include <SDL.h>
namespace Engine{
class Rectangle {
public:
Rectangle(
int x, int y, int w, int h,
SDL_Color Color = {0, 0, 0, 255})
: Rect{x, y, w, h}, Color{Color}{}
virtual void Render(SDL_Surface* Surface){
SDL_FillRect(
Surface, &Rect, SDL_MapRGB(
Surface->format, Color.r, Color.g,
Color.b
)
);
}
void SetColor(SDL_Color C){ Color = C; }
bool IsWithinBounds(int x, int y) const{
// Too far left
if (x < Rect.x) return false;
// Too far right
if (x > Rect.x + Rect.w) return false;
// Too high
if (y < Rect.y) return false;
// Too low
if (y > Rect.y + Rect.h) return false;
// Within bounds
return true;
}
SDL_Rect* GetRect(){ return &Rect; }
virtual ~Rectangle() = default;
private:
SDL_Rect Rect{0, 0, 0, 0};
SDL_Color Color{0, 0, 0, 0};
};
}
This Rectangle
class is similar to what we created in our earlier lesson when we started creating UI elements for SDL:
Engine/Button.h
Building upon the Rectangle
class, the Button
class adds interactivity to our visual elements. It includes:
HandleEvent
method for processing SDL events related to the button.The use of virtual methods here is crucial for extensibility. By declaring these methods as virtual, we enable derived classes to override them, allowing us to implement Minesweeper-specific behavior without modifying the base Button
class. This adheres to the Open-Closed Principle of object-oriented design.
For example, we'll be able to create a custom cell class for our Minesweeper grid that inherits from Button
and implements its own behavior for clicks and mouse movement.
#pragma once
#include "Globals.h"
#include "Engine/Rectangle.h"
namespace Engine{
class Button : public Rectangle {
public:
Button(int x, int y, int w, int h)
: Rectangle{x, y, w, h}{
SetColor(Config::BUTTON_COLOR);
}
virtual void
HandleEvent(const SDL_Event& E){
if (isDisabled) return;
if (E.type == SDL_MOUSEMOTION) {
HandleMouseMotion(E.motion);
} else if (E.type ==
SDL_MOUSEBUTTONDOWN) {
if (IsWithinBounds(E.button.x,
E.button.y)) {
E.button.button == SDL_BUTTON_LEFT
? HandleLeftClick()
: HandleRightClick();
}
}
}
void SetIsDisabled(bool NewValue){
isDisabled = NewValue;
}
protected:
virtual void HandleLeftClick(){}
virtual void HandleRightClick(){}
virtual void HandleMouseMotion(
const SDL_MouseMotionEvent& E){
if (IsWithinBounds(E.x, E.y)) {
SetColor(Config::BUTTON_HOVER_COLOR);
} else { SetColor(Config::BUTTON_COLOR); }
}
private:
bool isDisabled{false};
};
}
This Button
class is similar to what we created in our earlier lesson:
Engine/Image.h
The Image
class allows us to display images in our game. This will be useful for showing icons like bombs and flags in our Minesweeper grid. Key features include:
Render
method that draws the image on the given surface.This class demonstrates how we can extend our basic Rectangle
to create more specialized visual elements.
#pragma once
#include <SDL_image.h>
namespace Engine{
class Image {
public:
Image(
int x, int y, int w, int h,
const std::string& Filename,
int Padding = 12
): Destination{
x + Padding/2, y + Padding/2,
w-Padding, h-Padding
}{
ImageSurface = IMG_Load(Filename.c_str());
#ifdef SHOW_DEBUG_HELPERS
Utils::CheckSDLError("IMG_Load");
#endif
}
void Render(SDL_Surface* Surface) {
SDL_BlitScaled(
ImageSurface,nullptr,
Surface, &Destination
);
}
~Image() {
if (ImageSurface) {
SDL_FreeSurface(ImageSurface);
}
}
Image(const Image&){}
private:
SDL_Surface* ImageSurface{nullptr};
SDL_Rect Destination{0, 0, 0, 0};
};
}
This Image
class uses the techniques we covered in our introduction to images, surface blitting, and SDL_Image
:
Engine/Text.h
The Text
class enables us to render text in our game. We'll use this for displaying numbers and other information in our Minesweeper grid. It provides:
Text
member variables, including a rectangle to determine where the text should be rendered within the destination surface..SetText()
methods to change the text content and color. These methods also update a TextPosition
rectangle to ensure the text is rendered in the middle of the destination rectangleRender
method that draws the text on the given surface.#pragma once
#include <SDL_ttf.h>
#include "Globals.h"
namespace Engine{
class Text {
public:
Text(
int x, int y, int w, int h,
const std::string& Content,
SDL_Color Color = {0, 0, 0, 255},
int FontSize = 30
) : DestinationRect{x, y, w, h},
Color{Color}
{
Font = TTF_OpenFont(
Config::FONT.c_str(), FontSize);
#ifdef SHOW_DEBUG_HELPERS
Utils::CheckSDLError("TTF_OpenFont");
#endif
SetText(Content);
}
void SetText(const std::string& Text){
SetText(Text, Color);
}
void SetText(const std::string& Text,
SDL_Color NewColor){
if (TextSurface) {
SDL_FreeSurface(TextSurface);
}
Color = NewColor;
TextSurface = TTF_RenderUTF8_Blended(
Font, Text.c_str(), Color
);
auto [x, y, w, h] = DestinationRect;
// Horizontally centering
const int WidthDifference{
w - TextSurface->w};
const int LeftOffset{WidthDifference / 2};
// Vertically centering
const int HeightDifference{
h - TextSurface->h};
const int TopOffset{HeightDifference / 2};
TextPosition = {
x + LeftOffset, y + TopOffset, w, h
};
}
void Render(SDL_Surface* Surface) {
SDL_BlitSurface(
TextSurface, nullptr,
Surface, &TextPosition
);
}
~Text() {
if (Font) { TTF_CloseFont(Font); }
if (TextSurface) {
SDL_FreeSurface(TextSurface);
}
}
private:
SDL_Surface* TextSurface{nullptr};
TTF_Font* Font{nullptr};
SDL_Rect DestinationRect{0, 0, 0, 0};
SDL_Rect TextPosition{0, 0, 0, 0};
SDL_Color Color{0, 0, 0, 255};
};
}
This Text
class uses the techniques we covered in our introduction to SDL_ttf:
Engine/Random.h
Randomness is a crucial element in Minesweeper for generating unique game boards. We use it primarily to randomly place bombs on the grid, ensuring each game is different and challenging. The Random
namespace encapsulates our random number generation logic, providing a simple interface for generating random integers within a specified range.
std::random_device
and std::mt19937
.Int
function to generate random integers within a specified range.This abstraction will make it easy to add randomness to our game in a controlled and reusable manner.
#pragma once
#include <random>
namespace Engine::Random{
inline std::random_device SEEDER;
inline std::mt19937 ENGINE{SEEDER()};
inline size_t Int(size_t Min, size_t Max){
std::uniform_int_distribution Get{Min, Max};
return Get(ENGINE);
}
}
This Engine::Random
namespace uses the techniques we covered in our introduction to the randomness utilities in C++:
Minesweeper/UI.h
While most of the code we've looked at is part of our general-purpose engine, the MinesweeperUI
class is our first Minesweeper-specific component. We're storing it in the Minesweeper/
directory to keep it separate from our engine code.
The MinesweeperUI
class serves as the central point for managing all user interface elements specific to our Minesweeper game. It will be responsible for rendering the game board, handling user interactions, and updating the game state. For now, it contains placeholder Render
and HandleEvent
methods, which we'll implement in subsequent lessons to bring our Minesweeper game to life.
#pragma once
class MinesweeperUI {
public:
void Render(SDL_Surface* Surface){
// ...
}
void HandleEvent(const SDL_Event& E){
// ...
};
};
main.cpp
Finally, we have our main.cpp
file, which sets up the main loop of our application. It initializes SDL and its subsystems (SDL_image and SDL_ttf), and creates our game window and UI objects.
It also contains the main game loop, which forms the core of our game's execution:
Render
methods to draw the current game state.This loop runs repeatedly until we receive an SDL_QUIT
event:
#include <SDL_image.h>
#include <SDL_ttf.h>
#include "Globals.h"
#include "Engine/Window.h"
#include "Minesweeper/UI.h"
int main(int argc, char** argv){
SDL_Init(SDL_INIT_VIDEO);
#ifdef SHOW_DEBUG_HELPERS
Utils::CheckSDLError("SDL_Init");
#endif
IMG_Init(IMG_INIT_PNG);
#ifdef SHOW_DEBUG_HELPERS
Utils::CheckSDLError("IMG_Init");
#endif
TTF_Init();
#ifdef SHOW_DEBUG_HELPERS
Utils::CheckSDLError("TTF_Init");
#endif
Engine::Window GameWindow;
MinesweeperUI UI;
SDL_Event Event;
bool shouldQuit{false};
while (!shouldQuit) {
while (SDL_PollEvent(&Event)) {
if (Event.type == SDL_QUIT) {
shouldQuit = true;
} else { UI.HandleEvent(Event); }
}
GameWindow.Render();
UI.Render(GameWindow.GetSurface());
GameWindow.Update();
}
SDL_Quit();
return 0;
}
If the code in our main.cpp
is unclear, reviewing the lesson on application loops is recommended:
Our game will require some assets to function properly:
Feel free to use any assets you prefer. The assets used in our screenshots are:
Remember, these assets should be located in the same directory as your compiled executable for the game to find them.
CMakeLists.txt
For those using CMake to manage their project, the following CMakeLists.txt
file may be helpful.
This file sets up the project, adds the necessary source files, and links against the required SDL libraries. It also includes a command to copy the SDL DLLs and asset files to the output directory, ensuring that everything the game needs is in the right place when you run it.
cmake_minimum_required(VERSION 3.16)
set(CMAKE_CXX_STANDARD 20)
project(Minesweeper VERSION 1.0.0)
add_executable(Minesweeper
"main.cpp"
"Globals.h"
"Engine/Button.h"
"Engine/Rectangle.h"
"Engine/Image.h"
"Engine/Text.h"
"Engine/Window.h"
"Engine/Random.h"
"Minesweeper/UI.h"
# Files that will be added later:
# "Minesweeper/Grid.h"
# "Minesweeper/UI.h"
# "Minesweeper/Cell.h"
# "Minesweeper/Cell.cpp"
# "Minesweeper/NewGameButton.h"
)
target_include_directories(
Minesweeper PUBLIC ${PROJECT_SOURCE_DIR}
)
add_subdirectory(external/SDL)
add_subdirectory(external/SDL_image)
add_subdirectory(external/SDL_ttf)
target_link_libraries(Minesweeper PRIVATE
SDL2
SDL2_image
SDL2_ttf
)
if (WIN32)
target_link_libraries(
Minesweeper PRIVATE SDL2main
)
endif()
# Copy DLLs and Assets to output directory
set(AssetDirectory "${PROJECT_SOURCE_DIR}/Assets")
add_custom_command(
TARGET Minesweeper POST_BUILD COMMAND
${CMAKE_COMMAND} -E copy_if_different
"$<TARGET_FILE:SDL2>"
"$<TARGET_FILE:SDL2_image>"
"$<TARGET_FILE:SDL2_ttf>"
"${AssetDirectory}/bomb.png"
"${AssetDirectory}/Rubik-SemiBold.ttf"
"${AssetDirectory}/flag.png"
"$<TARGET_FILE_DIR:Minesweeper>"
VERBATIM
)
At this point, you should be able to compile and run the application. When you do, you should see an empty window appear. This might not look like much, but it's the foundation upon which we'll build our game!
In the next part of this tutorial, we'll start adding Minesweeper-specific functionality to our program, bringing our game to life step by step.
An introduction to the generic engine classes we'll use to create the game
Apply what we learned to build an interactive, portfolio-ready capstone project using C++ and the SDL2 library
Apply what we learned to build an interactive, portfolio-ready capstone project using C++ and the SDL2 library
Free, unlimited access