Engine Overview

An introduction to the generic engine classes we'll use to create the game

Ryan McCombe
Updated

In this series, we'll build a fully functional Minesweeper game from scratch, combining all the concepts we've covered so far.

We'll separate our project into two main parts:

  1. An "Engine" module containing components that are generally useful across a wide range of projects, not specific to Minesweeper.
  2. A Minesweeper-specific module that builds upon our engine to create the actual game.

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:

  • We can reuse the engine code across other projects, saving time and effort in future game development.
  • It provides a logical separation of concerns, allowing us to keep our classes smaller and more focused.
  • As we add new features to our project, we can thoughtfully add generally useful methods to the base engine classes rather than the specific Minesweeper classes. Over time, this approach will make our generic engine code more powerful, which future projects can also benefit from.

In this lesson, we'll introduce all of the engine code. While it might seem like a lot of code at first, it's all very similar to what 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.

The Globals.h File

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:

  1. Custom SDL event types, which we'll store in the UserEvents namespace. This namespace is currently empty, but we'll add to it as needed in future parts of the tutorial.
  2. General configuration options that we might want to change, stored in the Config namespace. This includes settings like the game name, window dimensions, and color schemes.
  3. Generally useful free functions that can be used across various files, stored in the 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.

src/Globals.h

#pragma once

#define SHOW_DEBUG_HELPERS

#include <iostream>
#include <SDL3/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 BASE_PATH{
    SDL_GetBasePath()};
  inline const std::string BOMB_IMAGE{
    BASE_PATH + "Bomb.png"};
  inline const std::string FLAG_IMAGE{
    BASE_PATH + "Flag.png"};
  inline const std::string FONT{
    BASE_PATH + "Roboto-Medium.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
}

The CheckSDLError() function in this file is something we created in our earlier lesson on .

The Engine/Window.h File

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:

  1. A constructor that creates an SDL window using the configurations defined in our Globals.h file.
  2. A Render() method that fills the window with the background color.
  3. An Update() method that refreshes the window surface.
  4. A 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:

src/Engine/Window.h

#pragma once
#include <SDL3/SDL.h>
#include "Globals.h"

namespace Engine{
class Window {
public:
  Window(){
    SDLWindow = SDL_CreateWindow(
      Config::GAME_NAME.c_str(),
      Config::WINDOW_WIDTH,
      Config::WINDOW_HEIGHT,
      0
    );
  }

  void Render(){
    const auto* Fmt = SDL_GetPixelFormatDetails(
      GetSurface()->format
    );
    SDL_FillSurfaceRect(
      GetSurface(), nullptr,
      SDL_MapRGB(Fmt, nullptr,
        Config::BACKGROUND_COLOR.r,
        Config::BACKGROUND_COLOR.g,
        Config::BACKGROUND_COLOR.b)
    );
  }

  void Update(){
    SDL_UpdateWindowSurface(SDLWindow);
  }

  SDL_Surface* GetSurface() const {
    return SDL_GetWindowSurface(SDLWindow);
  }

  ~Window() {
    if (SDLWindow && SDL_WasInit(SDL_INIT_VIDEO)) {
      SDL_DestroyWindow(SDLWindow);
    }
  }

  Window(const Window&) = delete;
  Window& operator=(const Window&) = delete;

private:
  SDL_Window* SDLWindow{nullptr};
};
}

This Window class is similar to what we created in our earlier lesson on .

The Engine/Rectangle.h File

The Rectangle class used for drawing rectangles on the screen. It provides the following key features:

  1. A constructor that takes position (x, y), dimensions (width, height), and an optional color.
  2. A virtual Render() method for drawing the rectangle on an SDL surface.
  3. Methods for setting the color and checking if a point is within the rectangle's bounds.
  4. A virtual destructor, allowing for proper cleanup in derived classes.

This class will serve as the base for more complex visual elements in our game, such as buttons and cells in the Minesweeper grid.

src/Engine/Rectangle.h

#pragma once
#include <SDL3/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){
      const auto* Fmt = SDL_GetPixelFormatDetails(
        Surface->format
      );
      SDL_FillSurfaceRect(
        Surface, &Rect, SDL_MapRGB(
          Fmt, nullptr, Color.r, Color.g, Color.b
        )
      );
    }

    void SetColor(SDL_Color C){ Color = C; }

    bool IsWithinBounds(int x, int y) const{
      if (x < Rect.x) return false;
      if (x > Rect.x + Rect.w) return false;
      if (y < Rect.y) return false;
      if (y > Rect.y + Rect.h) return false;
      return true;
    }

    const SDL_Rect* GetRect() const { 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 .

The Engine/Button.h File

Building upon the Rectangle class, the Button class adds interactivity to our visual elements. It includes:

  1. A constructor that sets up a button with a given position and size.
  2. A virtual HandleEvent() method for processing SDL events related to the button.
  3. Protected virtual methods for handling left clicks, right clicks, and mouse motion.
  4. A method to disable/enable the button.

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.

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.

src/Engine/Button.h

#pragma once
#include <SDL3/SDL.h>
#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_EVENT_MOUSE_MOTION) {
        HandleMouseMotion(E.motion);
      } else if (E.type ==
        SDL_EVENT_MOUSE_BUTTON_DOWN) {
        if (IsWithinBounds(
          (int)E.button.x, (int)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((int)E.x, (int)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 on .

The Engine/Image.h File

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:

  1. A constructor that loads an image from a file and applies padding.
  2. A Render() method that draws the image on the given surface.
  3. Proper resource management with a destructor that frees the image surface.

src/Engine/Image.h

#pragma once
#include <string>
#include <SDL3/SDL.h>
#include <SDL3_image/SDL_image.h>
#include "Globals.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_BlitSurfaceScaled(
        ImageSurface, nullptr,
        Surface, &Destination,
        SDL_SCALEMODE_LINEAR
      );
    }

    ~Image() {
      if (ImageSurface) {
        SDL_DestroySurface(ImageSurface);
      }
    }

    Image(const Image&) = delete;
    Image& operator=(const Image&) = delete;

  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 .

The Engine/Text.h File

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:

  1. A constructor that sets up the Text member variables, including a rectangle to determine where the text should be rendered within the destination surface.
  2. 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 rectangle.
  3. A Render() method that draws the text on the given surface.
  4. Proper resource management for the font and text surface.

src/Engine/Text.h

#pragma once
#include <string>
#include <SDL3/SDL.h>
#include <SDL3_ttf/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},
      float FontSize = 30.0f
    ) : 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_DestroySurface(TextSurface);
      }
      Color = NewColor;

      TextSurface = TTF_RenderText_Blended(
        Font, Text.c_str(), 0, 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,
        TextSurface->w, TextSurface->h
      };
    }

    void Render(SDL_Surface* Surface) {
      SDL_BlitSurface(
        TextSurface, nullptr,
        Surface, &TextPosition
      );
    }

    ~Text() {
      if (TTF_WasInit()) {
        TTF_CloseFont(Font);
      }
      if (TextSurface) {
        SDL_DestroySurface(TextSurface);
      }
    }

    Text(const Text&) = delete;
    Text& operator=(const Text&) = delete;

  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 .

The Engine/Random.h File

Minesweeper games rely on randomly place bombs on the grid to ensure each game is different. The Random namespace encapsulates our random number generation logic, providing a simple interface for generating random integers within a specified range.

  1. It sets up a random number generator using std::random_device and std::mt19937.
  2. It provides an 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.

src/Engine/Random.h

#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 .

The Minesweeper/UI.h File

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 game to life.

src/Minesweeper/UI.h

#pragma once
#include <SDL3/SDL.h>

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

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

The main.cpp File

Finally, we have our main.cpp file, which sets up the main loop of our application. It initializes SDL and its subsystems, and creates our game window and UI objects.

It also contains the main game loop, which forms the core of our game's execution as we've been doing in the past:

  1. It continuously processes SDL events, allowing the game to respond to user inputs and other events we'll add in future lessons.
  2. It calls the Render() methods to draw the current game state.
  3. It updates the display to show the newly rendered frame.

This loop runs repeatedly until we receive an SDL_EVENT_QUIT event:

src/main.cpp

#include <SDL3/SDL.h>
#include <SDL3/SDL_main.h>
#include <SDL3_image/SDL_image.h>
#include <SDL3_ttf/SDL_ttf.h>

#include "Globals.h"
#include "Engine/Window.h"
#include "Minesweeper/UI.h"

int main(int, char**){
  SDL_Init(SDL_INIT_VIDEO);
#ifdef SHOW_DEBUG_HELPERS
  Utils::CheckSDLError("SDL_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_EVENT_QUIT) {
        shouldQuit = true;
      } else { UI.HandleEvent(Event); }
    }
    GameWindow.Render();
    UI.Render(GameWindow.GetSurface());
    GameWindow.Update();
  }

  TTF_Quit();
  SDL_Quit();
  return 0;
}

If the code in our main.cpp is unclear, reviewing the lesson on is recommended.

Assets

Our game will require some assets to function properly:

  1. SDL libraries, which we covered in our earlier installation guides.
  2. A font file for rendering text.
  3. Images for our bomb and flag icons.

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.

Include Directories

In the rest of this chapter, the #include directives in our code examples assume that the /src directory is in your project's include directories or, alternatively, you adapt the directives to match your project configuration.

If you're using CMake to manage your project, the CMakeLists.txt file provided in the next section includes the required configuration.

If you're not sure what include directories are or how to set them, we cover it in more detail in our .

The CMakeLists.txt File

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 include directories, and links against the required SDL libraries.

CMakeLists.txt

cmake_minimum_required(VERSION 3.23)

project(
  Minesweeper
  VERSION 1.0
  DESCRIPTION "Minesweeper"
  LANGUAGES CXX
)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

set(
  CMAKE_RUNTIME_OUTPUT_DIRECTORY
  "${CMAKE_BINARY_DIR}/$<CONFIGURATION>"
)

set(
  CMAKE_LIBRARY_OUTPUT_DIRECTORY
  "${CMAKE_BINARY_DIR}/$<CONFIGURATION>"
)

add_executable(Minesweeper
  src/main.cpp
  
  # This file will be added later in the chapter
  # src/Minesweeper/Cell.cpp
)

target_include_directories(Minesweeper
  PRIVATE ${PROJECT_SOURCE_DIR}/src
)

if(APPLE)
  set_target_properties(Minesweeper PROPERTIES
    INSTALL_RPATH "@executable_path;@loader_path"
    BUILD_WITH_INSTALL_RPATH TRUE
    MACOSX_RPATH TRUE
  )
elseif(UNIX)
  set_target_properties(Minesweeper PROPERTIES
    INSTALL_RPATH "$ORIGIN"
    BUILD_WITH_INSTALL_RPATH TRUE
  )
endif()

set(SDLTTF_VENDORED ON)
set(VENDOR_DIR "${PROJECT_SOURCE_DIR}/vendor")
add_subdirectory(${VENDOR_DIR}/SDL)
add_subdirectory(${VENDOR_DIR}/SDL_image)
add_subdirectory(${VENDOR_DIR}/SDL_ttf)

target_link_libraries(Minesweeper
  SDL3::SDL3
  SDL3_image::SDL3_image
  SDL3_ttf::SDL3_ttf
)

You can find more information on using CMake with SDL3 in our earlier .

Running the Application

At this point, you should be able to compile and run the application. When you do, you should see an empty window appear.

In the next part, we'll start adding functionality to our program, bringing our game to life step by step.

Next Lesson
Lesson 33 of 37

Creating the Grid

Building a two-dimensional grid of interactive minesweeper cells

Have a question about this lesson?
Answers are generated by AI models and may not be accurate