Building a Strong Foundation

In this article, by Mickey Macdonald, author of the book Mastering C++ Game Development, we will cover how these libraries can work together and build some of the libraries needed to round out the structure.

(For more resources related to this topic, see here.)

To get started, we will focus on, arguably one of the most important aspects of any game project, the rendering system. Proper, performant implementations not only takes a significant amount of time, but it also takes specialized knowledge of video driver implementations and mathematics for computer graphics. Having said that, it is not, in fact, impossible to create a custom low-level graphics library yourself, it's just not overly recommended if your end goal is just to make video games. So instead of creating a low-level implementation themselves, most developers turn to a few different libraries to provide them abstracted access to the bare metal of the graphics device.

We will be using a few different graphic APIs to help speed up the process and help provide coherence across platforms. These APIs include the following:

  • OpenGL (https://www.opengl.org/): The Open Graphics Library (OpenGL) is an open cross-language, cross-platform application programming interface, or API, used for rendering 2D and 3D graphics. The API provides low-level access to the graphics processing unit (GPU).
  • SDL (https://www.libsdl.org/): Simple DirectMedia Layer (SDL) is a cross-platform software development library designed to deliver a low-level hardware abstraction layer to multimedia hardware components. While it does provide its own mechanism for rendering, SDL can use OpenGL to provide full 3D rendering support.

While these APIs save us time and effort by providing us some abstraction when working with the graphics hardware, it will quickly become apparent that the level of abstraction will not be high enough.

You will need another layer of abstraction to create an efficient way of reusing these APIs in multiple projects. This is where the helper and manager classes come in. These classes will provide the needed structure and abstraction for us and other coders. They will wrap all the common code needed to set up and initialize the libraries and hardware. The code that is required by any project regardless of gameplay or genre can be encapsulated in these classes and will become part of the "engine."

In this article, we will cover the following topics:

  • Building helper classes
  • Encapsulation with managers
  • Creating interfaces

Building helper classes

In object-oriented programming, a helper class is used to assist in providing some functionality, which is not, directly the main goal of the application in which it is used. Helper classes come in many forms and are often a catch-all term for classes that provide functionality outside of the current scope of a method or class. Many different programming patterns make use of helper classes. In our examples, we too will make heavy use of helper classes. Here is just one example.

Let's take a look at the very common set of steps used to create a Window. It's safe to say that most of the games you will create will have some sort of display and will generally be typical across different targets, in our case Windows and the macOS. Having to retype the same instructions constantly over and over for each new project seems like kind of a waste. That sort of situation is perfect for abstracting away in a helper class that will eventually become part of the engine itself. The code below is the header for the Window class included in the demo code examples.

To start, we have a few necessary includes, SDL, glew which is a Window creation helper library, and lastly, the standard string class is included:

#pragma once
#include <SDL/SDL.h>
#include <GL/glew.h>
#include <string>

Next, we have an enum WindowFlags. We use this for setting some bitwise operations to change the way the window will be displayed; invisible, full screen, or borderless. You will notice that I have wrapped the code in the namespace BookEngine, this is essential for keeping naming conflicts from happening and will be very helpful once we start importing our engine into projects:

namespace BookEngine
{
  enum WindowFlags //Used for bitwise passing 
  {
    INVISIBLE = 0x1,
    FULLSCREEN = 0x2,
    BORDERLESS = 0x4
  };

Now we have the Window class itself. We have a few public methods in this class. First the default constructor and destructor. It is a good idea to include a default constructor and destructor even if they are empty, as shown here, despite the compiler, including its own, these specified ones are needed if you plan on creating intelligent or managed pointers, such as unique_ptr, of the class:

class Window
  {
  public:
    Window();
    ~Window();

Next we have the Create function, this function will be the one that builds or creates the window. It takes a few arguments for the creation of the window such as the name of the window, screen width and height, and any flags we want to set, see the previously mentioned enum.

void Create(std::string windowName, int screenWidth, int 
    screenHeight, unsigned int currentFlags);

Then we have two getter functions. These functions will just return the width and height respectively:

int GetScreenWidth() { return m_screenWidth; }
    int GetScreenHeight() { return m_screenHeight; }

The last public function is the SwapBuffer function; this is an important function that we will take a look at in more depth shortly.

void SwapBuffer();

To close out the class definition, we have a few private variables. The first is a pointer to a SDL_Window* type, named appropriate enough m_SDL_Window. Then we have two holder variables to store the width and height of our screen. This takes care of the definition of the new Window class, and as you can see it is pretty simple on face value. It provides easy access to the creation of the Window without the developer calling it having to know the exact details of the implementation, which is one aspect that makes Object Orientated Programming and this method is so powerful:

private:
    SDL_Window* m_SDL_Window;
    int m_screenWidth;
    int m_screenHeight;
  };
}

To get a real sense of the abstraction, let's walk through the implementation of the Window class and really see all the pieces it takes to create the window itself.

#include "Window.h"
#include "Exception.h"
#include "Logger.h"
namespace BookEngine
{
  Window::Window()
  {
  }
  Window::~Window()
  {
  }

The Window.cpp files starts out with the need includes, of course, we need to include Window.h, but you will also note we need to include the Exception.h and Logger.h header files also. These are two other helper files created to abstract their own processes. The Exception.h file is a helper class that provides an easy-to-use exception handling system. The Logger.h file is a helper class that as its name says, provides an easy-to-use logging system.

After the includes, we again wrap the code in the BookEngine namespace and provide the empty constructor and destructor for the class.

The Create function is the first to be implemented. In this function are the steps needed to create the actual window. It starts out setting the window display flags using a series of if statements to create a bitwise representation of the options for the window. We use the enum we created before to make this easier to read for us humans.

void Window::Create(std::string windowName, int screenWidth, int 
  screenHeight, unsigned int currentFlags)
  {
    Uint32 flags = SDL_WINDOW_OPENGL;
    if (currentFlags & INVISIBLE)
    {
      flags |= SDL_WINDOW_HIDDEN;
    }
    if (currentFlags & FULLSCREEN)
    {
      flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
    }
    if (currentFlags & BORDERLESS)
    {
      flags |= SDL_WINDOW_BORDERLESS;
    }

After we set the window's display options, we move on to using the SDL library to create the window. As I mentioned before, we use libraries such as SDL to help us ease the creation of such structures. We start out wrapping these function calls in a Try statement; this will allow us to catch any issues and pass it along to our Exception class as we will see soon:

try {
      //Open an SDL window
      m_SDL_Window = SDL_CreateWindow(windowName.c_str(),
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        screenWidth,
        screenHeight,
        flags);

The first line sets the private member variable m_SDL_Window to a newly created window using the passed in variables, for the name, width, height, and any flags. We also set the default window's spawn point to the screen center by passing the SDL_WINDOWPOS_CENTERED define to the function.

if (m_SDL_Window == nullptr)
        throw Exception("SDL Window could not be created!");

After we have attempted to create the window, it is a good idea to check and see if the process did succeed. We do this with a simple if statement and check to see if the variable m_SDL_Window is set to a nullptr; if it is, we throw an Exception. We pass the Exception the string "SDL Window could not be created!". This is the error message that we can then print out in a catch statement. Later on, we will see an example of this. Using this method, we provide ourselves some simple error checking.

Once we have created our window and have done some error checking, we can move on to setting up a few other components. One of these components is the OpenGL library which requires what is referred to as a context to be set. An OpenGL context can be thought of as a set of states that describes all the details related to the rendering of the application. The OpenGLcontext must be set before any drawing can be done.

One problem is that creating a window and an OpenGL context is not part of the OpenGL specification itself. What this means is that every platform can handle this differently. Luckily for us, the SDL API again abstracts the heavy lifting for us and allows us to do this all in one line of code. We create a SDL_GLContext variable named glContext. We then assign glContext to the return value of the SDL_GL_CreateContext function that takes one argument, the SDL_Window we created earlier. After this we, of course, do a simple check to make sure everything worked as intended, just like we did earlier with the window creation:

//Set up our OpenGL context
      SDL_GLContext glContext = 
      SDL_GL_CreateContext(m_SDL_Window);
      if (glContext == nullptr)
        throw Exception("SDL_GL context could not be created!");

The next component we need to initialize is GLEW. Again this is abstracted for us to one simple command, glewInit(). This function takes no arguments but does return an error status code. We can use this status code to perform a similar error check like we did with the window and OpenGL. This time instead checking it against the defined GLEW_OK. If it evaluates to anything other than GLEW_OK, we throw an Exception to be caught later on.

//Set up GLEW (optional)
      GLenum error = glewInit();
      if (error != GLEW_OK)
        throw Exception("Could not initialize glew!");

Now that the needed components are initialized, now is a good time to log some information about the device running the application. You can log all kinds of data about the device which can provide valuable insights when trying to track down obscure issues. In this case, I am polling the system for the version of OpenGL that is running the application and then using the Logger helper class printing this out to a "Runtime" text file:

//print some log info
      std::string versionNumber = (const 
      char*)glGetString(GL_VERSION);
      WriteLog(LogType::RUN, "*** OpenGL Version: " + 
      versionNumber + "***");

Now we set the clear color or the color that will be used to refresh the graphics card. In this case, it will be the background color of our application. The glClearColor function takes four float values that represent the red, green, blue, and alpha values in a range of 0.0 to 1.0. Alpha is the transparency value where 1.0f is opaque, and 0.0f is completely transparent.

//Set the background color to blue
      glClearColor(0.0f, 0.0f, 1.0f, 1.0f);

The next line sets the VSYNC value, which is a mechanism that will attempt to match the application's framerate to that of the physical display. The SDL_GL_SetSwapInterval function takes one argument, an integer that can be 1 for on or 0 for off.

//Enable VSYNC
      SDL_GL_SetSwapInterval(1);

The last two lines that make up the try statement block, enable blending and set the method used when performing alpha blending. For more information on these specific functions, check out the OpenGL development documents:

//Enable alpha blend
      glEnable(GL_BLEND);
      glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
    }

After our try block, we now have to include the catch block or blocks. This is where we will capture any of the thrown errors that have occurred. In our case, we are just going to grab all the Exceptions. We use the WriteLog function from the Logger helper class to add the exception message, e.reason to the error log text file. This is a very basic case, but of course, we could do more here, possibly even recover from an error if possible.

catch (Exception e)
    {
      //Write Log
      WriteLog(LogType::ERROR, e.reason);
    }
  }

Finally, the last function in the Window.cpp file is the SwapBuffer function. Without going too deep on the implementation, what swapping buffers does is exchange the front and back buffers of the GPU. This in a nutshell allows smoother drawing to the screen. It is a complicated process that again has been abstracted by the SDL library. Our SwapBuffer function, abstracts this process again so that when we want to swap the buffers we simply call SwapBuffer instead of having to call the SDL function and specify the window, which is what is exactly done in the function.

void Window::SwapBuffer()
  {
    SDL_GL_SwapWindow(m_SDL_Window);
  }
}

So as you can see, building up these helper functions can go a long way in making the process of development and iteration much quicker and simpler. Next, we will look at another programming method that again abstracts the heavy lifting from the developer's hands and provides a form of control over the process, a management system.

Encapsulation with managers

When working with complex systems such as input and audio systems, it can easily become tedious and unwieldy to control and check each state and other internals of the system directly. This is where the idea of the "Manager" programming pattern comes in. Using abstraction and polymorphism we can create classes that allow us to modularize and simplify the interaction with these systems. Manager classes can be found in many different use cases. Essentially if you see a need to have structured control over a certain system, this could be a candidate for a Manger class.

Stepping away from the rendering system for a second, let's take a look at a very common task that any game will need to perform, handling input. Since every game needs some form of input, it only makes sense to move the code that handles this to a class that we can use over and over again. Let's take a look at the InputManager class, starting with the header file:

#pragma once
#include <unordered_map>
#include <glm/glm.hpp>
namespace BookEngine {
  class InputManager
  {
  public:
    InputManager();
    ~InputManager();

The InputManager class starts just like the others, we have the includes needed and again we wrap the class in the BookEngine namespace for convince and safety. The standard constructor and destructor are also defined.

Next, we have a few more public functions. First the Update function, which will not surprisingly update the input system. Then we have the KeyPressed and KeyReleased functions, these functions both take an integer value corresponding to a keyboard key. The following functions fire off when the key is pressed or released respectively.

void Update();
    void KeyPress(unsigned int keyID);
    void KeyRelease(unsigned int keyID);

After the KeyPress and KeyRelease functions, we have two more key related functions the isKeyDown and isKeyPressed . Like the KeyPress and KeyRelease functions the isKeyDown and isKeyPressed functions take integer values that correspond to keyboard keys. The noticeable difference is that these functions return a Boolean value based on the status of the key. We will see more about this in the implementation file coming up.

bool isKeyDown(unsigned int keyID); //Returns true if key is 
    held
    bool isKeyPressed(unsigned int keyID); //Returns true if key 
    was pressed this update

The last two public functions in the InputManager class are SetMouseCoords and GetMouseCoords which do exactly as the names suggest and set or get the mouse coordinates respectively.

void SetMouseCoords(float x, float y);
    glm::vec2 GetMouseCoords() const { return m_mouseCoords; };

Moving on to the private members and functions, we have a few variables declared to store some information about the keys and mouse. First, we have a Boolean value that stores the state of the key being pressed down or not. Next, we have two unordered maps that will store the current keymap and previous keymaps. The last value we store is the mouse coordinates. We us a vec2 construct from another helper library the Graphic Library Math library or GLM. We use this vec2, which is just a two-dimensional vector, to store the x and y coordinate values of the mouse cursor since it is on a 2D plane, the screen. If you are looking for a refresher on vectors and the Cartesian coordinate system, I highly recommend the Beginning Math Concepts for Game Developers book by Dr. John P Flynt:

private:
    bool WasKeyDown(unsigned int keyID);
    std::unordered_map<unsigned int, bool> m_keyMap;
    std::unordered_map<unsigned int, bool> m_previousKeyMap;
    glm::vec2 m_mouseCoords;
};
}

Now let's look at the implementation, the InputManager.cpp file.

Again we start out with the includes and the namespace wrapper. Then we have the constructor and destructor. The highlight to note here is the setting of the m_mouseCoords to 0.0f in the constructor:

namespace BookEngine
{
  InputManager::InputManager() : m_mouseCoords(0.0f)
  {
  }
  InputManager::~InputManager()
  {
  }

Next is the Update function. This is a simple update where we are stepping through each key in the key map and copying it over to the previous key map holder:

m_previousKeyMap.
  void InputManager::Update()
  {
    for (auto& iter : m_keyMap)
    {
      m_previousKeyMap[iter.first] = iter.second;
    }
  }

The next function is the KeyPress function. In this function, we use the trick of an associative array to test and insert the key pressed whichmatches the ID passed in. The trick is that if the item located at the index of the keyID index does not exist, it will automatically be created:

void InputManager::KeyPress(unsigned int keyID)
  {
    m_keyMap[keyID] = true;
  }
. We do the same for the KeyRelease function below.
  void InputManager::KeyRelease(unsigned int keyID)
  {
    m_keyMap[keyID] = false;
  }

The KeyRelease function is the same setup as the KeyPressed function, except that we are setting the keyMap item at the keyID index to false.

bool InputManager::isKeyDown(unsigned int keyID)
  {
    auto key = m_keyMap.find(keyID);
    if (key != m_keyMap.end())
      return key->second;   // Found the key
    return false;
  }

After the KeyPress and KeyRelease functions, we implement the isKeyDown and isKeyPressed functions. First the isKeydown function; here we want to test if a key is already pressed down. In this case, we take a different approach to testing the key than in the KeyPress and KeyRelease functions and avoid the associative array trick. This is because we don't want to create a key if it does not already exist, so instead, we do it manually:

bool InputManager::isKeyPressed(unsigned int keyID)
  {
    if(isKeyDown(keyID) && !m_wasKeyDown(keyID))
    {
      return true;
    }
    return false;
  }

The isKeyPressed function is quite simple. Here we test to see if the key that matches the passed in ID is pressed down, by using the isKeyDown function, and that it was not already pressed down by also passing the ID to m_wasKeyDown. If both of these conditions are met, we return true, or else we return false. Next, we have the WasKeyDown function, much like the isKeyDown function, we do a manual lookup to avoid accidentally creating the object using the associative array trick:

bool InputManager::WasKeyDown(unsigned int keyID)
  {
    auto key = m_previousKeyMap.find(keyID);
    if (key != m_previousKeyMap.end())
      return key->second;   // Found the key
    return false;
}

The final function in the InputManger is SetMouseCoords. This is a very simple "setter" function that takes the passed in floats and assigns them to the x and y members of the two-dimensional vector, m_mouseCoords.

void InputManager::SetMouseCoords(float x, float y)
  {
    m_mouseCoords.x = x;
    m_mouseCoords.y = y;
  }
}

Creating interfaces

Sometimes you are faced with a situation where you need to describe capabilities and provide access to general behaviors of a class without committing to a particular implementation. This is where the idea of interfaces or abstract classes comes into play. Using interfaces provides a simple base class that other classes can then inherit from without having to worry about the intrinsic details. Building strong interfaces can enable rapid development by providing a standard class to interact with. While interfaces could, in theory, be created of any class, it is more common to see them used in situations where the code is commonly being reused.

Let's take a look at an interface from the example code in the repository. This interface will provide access to the core components of the Game. I have named this class IGame, using the prefix I to identify this class as an interface. The following is the implementation beginning with the definition file IGame.h.

To begin with, we have the needed includes and the namespace wrapper. You will notice that the files we are including are some of the ones we just created. This is a prime example of the continuation of the abstraction. We use these building blocks to continue to build the structure that will allow this seamless abstraction:

#pragma once
#include <memory>
#include "BookEngine.h"
#include "Window.h"
#include "InputManager.h"
#include "ScreenList.h"
namespace BookEngine
{

Next, we have a forward declaration. This declaration is for another interface that has been created for screens. The full source code to this interface and its supporting helper classes are available in the code repository.

class IScreen;using forward declarations like this is a common practice in C++.

If the definition file only requires the simple definition of a class, not adding the header for that class will speed up compile times.

Moving onto the public members and functions, we start off the constructor and destructor. You will notice that this destructor in this case is virtual. We are setting the destructor as virtual to allow us to call delete on the instance of the derived class through a pointer. This is handy when we want our interface to handle some of the cleanup directly as well.

class IGame
  {
  public:
    IGame();
    virtual ~IGame();

Next we have declarations for the Run function and the ExitGame function.

void Run();
    void ExitGame();

We then have some pure virtual functions, OnInit, OnExit, and AddScreens. Pure virtual functions are functions that must be overridden by the inheriting class. By adding the =0; to the end of the definition, we are telling the compiler that these functions are purely virtual.

When designing your interfaces, it is important to be cautious when defining what functions must be overridden. It's also very important to note that having pure virtual function implicitly makes the class it is defined for abstract. Abstract classes cannot be instantiated directly because of this and any derived classes need to implement all inherited pure virtual functions. If they do not, they too will become abstract.

virtual void OnInit() = 0;
    virtual void OnExit() = 0;
    virtual void AddScreens() = 0;

After our pure virtual function declarations, we have a function OnSDLEvent which we use to hook into the SDL event system. This provides us support for our input and other event-driven systems:

void OnSDLEvent(SDL_Event& event);

The public function in the IGame interface class is a simple helper function GetFPS that returns the current FPS. Notice the const modifiers, they identify quickly that this function will not modify the variable's value in any way:

declarations

const float GetFPS() const { return m_fps; }

In our protected space, we start with a few function declarations. First is the Init or initialization function. This will be the function that handles a good portion of the setup. Then we have two virtual functions Update and Draw.

Like pure virtual functions, a virtual function is a function that can be overridden by a derived class's implementation. Unlike a pure virtual function, the virtual function does not make the class abstract by default and does not have to be overridden. Virtual and pure virtual functions are keystones of polymorphic design. You will quickly see their benefits as you continue your development journey:

protected:
    bool Init();
    virtual void Update();
    virtual void Draw();

To close out the IGame definition file, we have a few members to house different objects and values. I am not going to go through these line by line since I feel they are pretty self-explanatory:

declarations

std::unique_ptr<ScreenList> m_screenList = nullptr;
    IGameScreen* m_currentScreen = nullptr;
    Window m_window;
    InputManager m_inputManager;
    bool m_isRunning = false;
    float m_fps = 0.0f;
  };
}

Now that we have taken a look at the definition of our interface class, let's quickly walk through the implementation. The following is the IGame.cpp file. To save time and space, I am going to highlight the key points. For the most part, the code is self-explanatory, and the source located in the repository is well commented for more clarity:

#include "IGame.h"
#include "IScreen.h"
#include "ScreenList.h"
#include "Timing.h"
namespace BookEngine
{
  IGame::IGame()
  {
    m_screenList = std::make_unique<ScreenList>(this);
  }

  IGame::~IGame()
  {
  }

Our implementation starts out with the constructor and destructor. The constructor is simple, its only job is to add a unique pointer of a new screen using this IGame object as the argument to pass in. See the IScreen class for more information on screen creation. Next, we have the implementation of the Run function. This function, when called will set the engine in motion. Inside the function, we do a quick check to make sure we have already initialized our object. We then use yet another helper class, FPSlimiter, to set the max fps that our game can run. After that, we set the isRunning boolean value to true, which we then use to control the game loop:

void IGame::Run()
  {
    if (!Init())
      return;
    FPSLimiter fpsLimiter;
    fpsLimiter.SetMaxFPS(60.0f);
    m_isRunning = true;

Next is the game loop. In the game loop, we do a few simple calls. First, we start the fps limiter. We then call the update function on our input manager.

It is a good idea always to check input before doing other updates or drawing since their calculations are sure to use the new input values.

After we update the input manager, we recursively call our Update and Draw class, which we will see shortly. We close out the loop by ending the fpsLimiter function and calling SwapBuffer on the Window object.

///Game Loop
    while (m_isRunning)
    {
      fpsLimiter.Begin();
      m_inputManager.Update();
      Update();
      Draw();
      m_fps = fpsLimiter.End();
      m_window.SwapBuffer();
    }
  }

The next function we implement is the ExitGame function. Ultimately, this will be the function that will be called on the final exit of the game. We close out, destroy, and free up any memory that the screen list has created and set the isRunning Boolean to false, which will put an end to the loop:

void IGame::ExitGame()
  {
    m_currentScreen->OnExit();
    if (m_screenList)
    {
      m_screenList->Destroy();
      m_screenList.reset(); //Free memory
    }
    m_isRunning = false;
  }

Next up is the Init function. This function will initialize all the internal object settings and call the initialization on the connected systems. Again, this is an excellent example of OOP or object orientated programming and polymorphism. Handling initialization in this manner allows the cascading effect, keeping the code modular and easier to modify:

bool IMainGame::Init()
  {
    BookEngine::Init();
    SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1);
    m_window.Create("BookEngine", 1024, 780, 0);
    OnInit();
    AddScreens();
    m_currentScreen = m_screenList->GetCurrentScreen();
    m_currentScreen->OnEntry();
    m_currentScreen->Run();
return true;
}

Next, we have the update function. In this Update function, we create a structure to allow us to execute certain code based on a state that the current screen is in. We accomplish this using a simple Switch case method with the enumerated elements of the ScreenState type as the cases. This setup is considered a simple finite state machine and is a very powerful design method used throughout game development:

void IMainGame::Update()
  {
    if (m_currentScreen)
    {
      switch (m_currentScreen->GetScreenState())
      {
      case ScreenState::RUNNING:
        m_currentScreen->Update();
        break;
      case ScreenState::CHANGE_NEXT:
        m_currentScreen->OnExit();
        m_currentScreen = m_screenList->MoveToNextScreen();
        if (m_currentScreen)
        {
          m_currentScreen->Run();
          m_currentScreen->OnEntry();
        }
        break;
      case ScreenState::CHANGE_PREVIOUS:
        m_currentScreen->OnExit();
        m_currentScreen = m_screenList->MoveToPreviousScreen();
        if (m_currentScreen)
        {
          m_currentScreen->Run();
          m_currentScreen->OnEntry();
        }
        break;
      case ScreenState::EXIT_APP:
        ExitGame();
        break;
      default:
        break;
      }
    }
    else
    {
      //we have no screen so exit
      ExitGame();
    }
  }

After our Update, we implement the Draw function. In our function, we only do a couple of things. First, we reset the Viewport as a simple safety check, then if the current screen's state matches the enumerated value RUNNING, we again use polymorphism to pass the Draw call down the object line:

void IGame::Draw()
  {
    //For safety
    glViewport(0, 0, m_window.GetScreenWidth(), 
    m_window.GetScreenHeight());

    //Check if we have a screen and that the screen is running
    if (m_currentScreen &&
      m_currentScreen->GetScreenState() == ScreenState::RUNNING)
    {
      m_currentScreen->Draw();
    }
  }

The last function we need to implement is the OnSDLEvent function. Like I mention in the definition section of this class, we will use this function to connect our input manger system to the SDL built in event system.

Every key press or mouse movement is handled as an event. Based on the type of event that has occurred, we again use a Switch case statement to create a simple finite state machine.

void IGame::OnSDLEvent(SDL_Event & event)
  {
    switch (event.type) {
    case SDL_QUIT:
      m_isRunning = false;
      break;
    case SDL_MOUSEMOTION:
      m_inputManager.SetMouseCoords((float)event.motion.x, 
      (float)event.motion.y);
      break;
    case SDL_KEYDOWN:
      m_inputManager.KeyPress(event.key.keysym.sym);
      break;
    case SDL_KEYUP:
      m_inputManager.KeyRelease(event.key.keysym.sym);
      break;
    case SDL_MOUSEBUTTONDOWN:
      m_inputManager.KeyPress(event.button.button);
      break;
    case SDL_MOUSEBUTTONUP:
      m_inputManager.KeyRelease(event.button.button);
      break;
    }
  }
}

Well, that takes care of the IGame interface. With this created, we can now create a new project that can utilize this and other interfaces in the example engine to create a game and initialize it all with just a few lines of code:

#pragma once
#include <BookEngine/IMainGame.h>
#include "GamePlayScreen.h"
class App : public BookEngine::IGame
{
public:
  App();
  ~App();
  virtual void OnInit() override;
  virtual void OnExit() override;
  virtual void AddScreens() override;
private:
  std::unique_ptr<GameplayScreen> m_gameplayScreen = nullptr;
};

The highlights to note here are that, one, the App class inherits from the BookEngine::IGame interface and two, we have all the necessary overrides that the inherited class requires. Next, if we take a look at the main.cpp file, the entry point for our application, you will see the simple commands to set up and kick off all the amazing things our interfaces, managers, and helpers abstract for us:

#include <BookEngine/IMainGame.h>
#include "App.h"
int main(int argc, char** argv)
{
  App app;
  app.Run();
  return 0;
}

As you can see, this is far simpler to type out every time we want to create a new project than having to recreate the framework constantly from scratch.

To see the output of the framework, build the BookEngine project, then build and run the example project.

On Windows, the example project when run will look like the following:

Mastering C++ Game Development

On macOS, the example project when run will look like the following:

Mastering C++ Game Development

Summary

In this article, we covered quite a bit. We took a look at the different methods of using object oriented programming and polymorphism to create a reusable structure for all your game projects. We walked through the differences in Helper, Managers, and Interfaces classes with examples from real code.

Resources for Article:


Further resources on this subject:


You've been reading an excerpt of:

Mastering C++ Game Development

Explore Title