All games have objects, for example, players, enemies, non-player character (NPC), traps, bullets, and doors. Keeping track of all these objects and how they interact with each other is a big task and one that we would like to make as simple as possible. Our game could become unwieldy and difficult to update if we do not have a solid implementation. So what can we do to make our task easier? We can start by really trying to leverage the power of object-oriented programming (OOP). We will cover the following in this chapter:
The first powerful feature of OOP we will look at is inheritance. This feature can help us enormously when developing our reusable framework. Through the use of inheritance, we can share common functionality between similar classes and also create subtypes from existing types. We will not go into too much detail about inheritance itself but instead we will start to think about how we will apply it to our framework.
As mentioned earlier, all games have objects of various types. In most cases, these objects will have a lot of the same data and require a lot of the same basic functions. Let's look at some examples of this common functionality:
draw
functionupdate
functionThis is a good starting point for our first game object class, so let's go ahead and create it. Add a new class to the project called GameObject
and we can begin:
class GameObject { public: void draw() { std::cout << "draw game object"; } void update() { std::cout << "update game object"; } void clean() { std::cout << "clean game object"; } protected: int m_x; int m_y; };
So, there we have our first game object class. Now let's inherit from it and create a class called Player
:
class Player : public GameObject // inherit from GameObject { public: void draw() { GameObject::draw(); std::cout << "draw player"; } void update() { std::cout << "update player"; m_x = 10; m_y = 20; } void clean() { GameObject::clean(); std::cout << "clean player"; } };
What we have achieved is the ability to reuse the code and data that we originally had in GameObject
and apply it to our new Player
class. As you can see, a derived class can override the functionality of a parent class:
void update() { std::cout << "update player"; m_x = 10; m_y = 20; }
Or it can even use the functionality of the parent class, while also having its own additional functionality on top:
void draw() { GameObject::draw(); std::cout << "draw player"; }
Here we call the draw
function from GameObject
and then define some player-specific functionality.
Okay, so far our classes do not do much, so let's add some of our SDL functionality. We will add some drawing code to the
GameObject
class and then reuse it within our Player
class. First we will update our GameObject
header file with some new values and functions to allow us to use our existing SDL code:
class GameObject { public: void load(int x, int y, int width, int height, std::string textureID); void draw(SDL_Renderer* pRenderer); void update(); void clean(); protected: std::string m_textureID; int m_currentFrame; int m_currentRow; int m_x; int m_y; int m_width; int m_height; };
We now have some new member variables that will be set in the new load
function. We are also passing in the SDL_Renderer
object we want to use in our draw
function. Let's define these functions in an implementation file and create GameObject.cpp
:
First define our new load
function:
void GameObject::load(int x, int y, int width, int height, std::string textureID) { m_x = x; m_y = y; m_width = width; m_height = height; m_textureID = textureID; m_currentRow = 1; m_currentFrame = 1; }
Here we are setting all of the values we declared in the header file. We will just use a start value of 1
for our m_currentRow
and m_currentFrame
values. Now we can create our draw
function that will make use of these values:
void GameObject::draw(SDL_Renderer* pRenderer) { TextureManager::Instance()->drawFrame(m_textureID, m_x, m_y, m_width, m_height, m_currentRow, m_currentFrame, pRenderer); }
We grab the texture we want from TextureManager
using m_textureID
and draw it according to our set values. Finally we can just put something in our update
function that we can override in the Player
class:
void GameObject::update() { m_x += 1; }
Our GameObject
class is complete for now. We can now alter the Player
header file to reflect our changes:
#include "GameObject.h" class Player : public GameObject { public: void load(int x, int y, int width, int height, std::string textureID); void draw(SDL_Renderer* pRenderer); void update(); void clean(); };
We can now move on to defining these functions in an implementation file. Create Player.cpp
and we'll walk through the functions. First we will start with the load
function:
void Player::load(int x, int y, int width, int height, string textureID) { GameObject::load(x, y, width, height, textureID); }
Here we can use our GameObject::load
function. And the same applies to our draw
function:
void Player::draw(SDL_Renderer* pRenderer) { GameObject::draw(pRenderer); }
And let's override the update
function with something different; let's animate this one and move it in the opposite direction:
void Player::update() { m_x -= 1; }
We are all set; we can create these objects in the Game
header file:
GameObject m_go; Player m_player;
Then load them in the init
function:
m_go.load(100, 100, 128, 82, "animate"); m_player.load(300, 300, 128, 82, "animate");
They will then need to be added to the render
and update
functions:
void Game::render() { SDL_RenderClear(m_pRenderer); // clear to the draw colour m_go.draw(m_pRenderer); m_player.draw(m_pRenderer); SDL_RenderPresent(m_pRenderer); // draw to the screen } void Game::update() { m_go.update(); m_player.update(); }
We have one more thing to add to make this run correctly. We need to cap our frame rate slightly; if we do not, then our objects will move far too fast. We will go into more detail about this in a later chapter, but for now we can just put a delay in our main loop. So, back in main.cpp
, we can add this line:
while(g_game->running()) { g_game->handleEvents(); g_game->update(); g_game->render(); SDL_Delay(10); // add the delay }
Now build and run to see our two separate objects:
Our Player
class was extremely easy to write, as we had already written some of the code in our GameObject
class, along with the needed variables. You may have noticed, however, that we were copying code into a lot of places in the Game
class. It requires a lot of steps to create and add a new object to the game. This is not ideal, as it would be easy to miss a step and also it will get extremely hard to manage and maintain when a game goes beyond having two or three different objects.
What we really want is for our Game
class not to need to care about different types; then we could loop through all of our game objects in one go, with separate loops for each of their functions.
3.12.153.212