Chapter 3. Working with Game Objects

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:

  • Using inheritance
  • Implementing polymorphism
  • Using abstract base classes
  • Effective inheritance design

Using inheritance

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:

  • Almost all of our objects will be drawn to the screen, thus requiring a draw function
  • If our objects are to be drawn, they will need a location to draw to, that is, x and y position variables
  • We don't want static objects all the time, so we will need an update function
  • Objects will be responsible for cleaning up after themselves; a function that deals with this will be important

This 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;
};

Note

The public, protected, and private keywords are very important. Public functions and data are accessible from anywhere. Protected status restricts access to only those classes derived from it. Private members are only available to that class, not even its derived classes.

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.

Note

The :: operator is called the scope resolution operator and it is used to identify the specific place that some data or function resides.

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:

Using inheritance

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.153.212