Chapter 21: File I/O and the Game Object Factory

This chapter handles how a GameObject gets into the m_GameObjects vector that's used in the game. We will look at how we can describe individual objects and an entire level in a text file. We will write code to interpret the text and then load up values into a class that will be a blueprint for a game object. We will also code a class called LevelManager that oversees the whole process, starting from the initial request to load a level sent from an InputHandler via the ScreenManager, right through to the factory pattern class that assembles a game object from components and delivers it to the LevelManager, neatly packed away in the m_GameObjects vector.

The following are the steps we will go through in this chapter:

  • Examine how we will describe game objects and their components in a text file
  • Code the GameObjectBlueprint class where the data from the text file will be temporarily stored
  • Code the ObjectTags class to help describe game objects consistently and without errors
  • Code BluePrintObjectParser, which will be responsible for loading the data from a game object description in the text file into a GameObjectBlueprint instance
  • Code PlayModeObjectLoader, which will open the text file and receive the GameObjectBlueprint instances one at a time from BlueprintObjectParser
  • Code the GameObjectFactoryPlayMode class, which will construct GameObject instances from GameObjectBlueprint instances
  • Code the LevelManager class, which oversees this entire process after receiving an instruction from the ScreenManager class
  • Add the code to the ScreenManager class so that we can begin using the new system that we will code in this chapter

Let's start by examining exactly how we describe a game object such as a space invader or a bullet in a text file, let alone a whole wave of them.

The structure of the file I/O and factory classes

Have a look at the following diagram, which gives an overview of the classes we will code in this chapter and how the vector of GameObject instances will be shared with the ScreenManager class that we coded in Chapter 19, Game Programming Design Patterns – Starting the Space Invaders ++ Game:

The preceding diagram shows that there is a vector of GameObject instances that's shared between four classes. This is achieved by passing the vector between the functions of the classes by reference. Each class can then carry out its role with the vector and its contents. The ScreenManager class will trigger the LevelManager class when a new level needs to be loaded into the vector. The individual Screen classes and their InputHandler-derived classes, as we saw in Chapter 19, Game Programming Design Patterns – Starting the Space Invaders ++ Game, have access to ScreenManager via ScreenManagerRemoteControl.

The LevelManager class is ultimately responsible for creating and sharing the vector. PlayModeObjectLoader will use BlueprintObjectParser to create GameObjectBlueprint instances.

The GameObjectFactoryPlayMode class will complete the GameObject creation process using these GameObjectBlueprint instances and pack GameObject instances into the vector when prompted to do so by PlayModeObjectLoader.

So, where do the different component, position, size, and appearance configurations for each GameObject instance come from?

We can also see that three classes have access to a GameObjectBlueprint instance. This instance is created by the LevelManager class and passed around by reference. BlueprintObjectParser will read the level1.txt file, which has all the details of each of the game objects. It will initialize all the variables of the GameObjectBlueprint class. PlayModeObjectLoader will then pass a reference to the vector of GameObject instances, and also pass a reference to the fully configured GameObjectBlueprint instance to the GameObjectFactoryPlayMode class. This is repeated until all the GameObject instances are packed away in the vector.

You might be wondering why I have used slightly cumbersome class names such as GameObjectFactoryPlayMode and PlayModeObjectLoader. The reason is that, once you see how convenient this system is, you might like to build tools that allow you to design your levels in a visual way by dragging and dropping them where required and then have the text file auto-generated rather than typed. This is not especially complicated, but I had to stop adding features to the game at some point. Therefore, you might well end up with a GameObjectFactoryDesignMode and a DesignModeObjectLoader.

Describing an object in the world

We have already added the level1.txt file in the world folder in Chapter 19, Game Programming Design Patterns – Starting the Space Invaders ++ Game. Let's discuss its uses, future intended uses, and its contents.

First, I would like to point out that a shooter game is not the best way to demonstrate how to describe a game world in a text file like this. The reason for this is that there are only a few types of game object and the most common one, invaders, are all lined up uniformly like soldiers on parade. They would actually be more efficiently described programmatically, perhaps in a nested for loop. However, the intention of this project was to show the ideas, rather than learn how to make a Space Invaders clone.

Take a look at the following text, which is a sample from the level1.txt file in the world folder:

[START OBJECT]

[NAME]invader[-NAME]

[COMPONENT]Standard Graphics[-COMPONENT]

[COMPONENT]Invader Update[-COMPONENT]

[COMPONENT]Transform[-COMPONENT]

[LOCATION X]0[-LOCATION X]

[LOCATION Y]0[-LOCATION Y]

[WIDTH]2[-WIDTH]

[HEIGHT]2[-HEIGHT]

[BITMAP NAME]invader1[-BITMAP NAME]

[ENCOMPASSING RECT COLLIDER]invader[-ENCOMPASSING_RECT COLLIDER]

[END OBJECT]

The preceding text describes a single object in the game; in this case, an invader. The object begins with the following text:

[START OBJECT]

That will inform our code we'll write that a new object is being described. Next in the text, we can see the following:

[NAME]invader[-NAME]

This informs the code that the type of object is an invader. This will eventually be set as the m_Tag of the ColliderComponent class. The invader will be identifiable for what it is. The text that comes next is as follows:

[COMPONENT]Standard Graphics[-COMPONENT]

[COMPONENT]Invader Update[-COMPONENT]

[COMPONENT]Transform[-COMPONENT]

This tells our system that this object will have three components added to it: a StandardGraphicsComponent instance, an InvaderUpdateComponent instance, and a TransformComponent instance. This means the object will be drawn in the standard way and will behave according to the rules we coded for an invader. It will also mean it has a location and scale in the game world. It is possible to have objects that don't have any components or fewer components. An object that takes no action and doesn't move will not need an update component, an object that is invisible will not need a graphics component (perhaps just an invisible collider which triggers some action), and an object that has no position in the world (perhaps a debugging object) will not need a transform component.

The position and scale of an object are determined by the following four lines of text:

[LOCATION X]0[-LOCATION X]

[LOCATION Y]0[-LOCATION Y]

[WIDTH]2[-WIDTH]

[HEIGHT]2[-HEIGHT]

The following line of text determines what graphics file will be used for the texture of this object:

[BITMAP NAME]invader1[-BITMAP NAME]

The following line means that the object can be collided with. A decorative object, perhaps floating clouds (or a bee), would not need a collider:

[ENCOMPASSING RECT COLLIDER]invader[-ENCOMPASSING_RECT COLLIDER]

The final line of text will inform our system that the object has finished describing itself:

[END OBJECT]

Now, let's have a look at how we describe a bullet object:

[START OBJECT]

[NAME]bullet[-NAME]

[COMPONENT]Standard Graphics[-COMPONENT]

[COMPONENT]Transform[-COMPONENT]

[COMPONENT]Bullet Update[-COMPONENT]

[LOCATION X]-1[-LOCATION X]

[LOCATION Y]-1[-LOCATION Y]

[WIDTH]0.1[-WIDTH]

[HEIGHT]2.0[-HEIGHT]

[BITMAP NAME]bullet[-BITMAP NAME]

[ENCOMPASSING RECT COLLIDER]bullet[-ENCOMPASSING_RECT COLLIDER]

[SPEED]75.0[-SPEED]

[END OBJECT]

This is very similar but not the same as an invader. A bullet object has additional data, such as a set speed. The invader's speed is set in the logic of the InvaderUpdateComponent class. We could have done this for the bullet's speed as well, but this demonstrates that you can describe the object in as much or as little detail as the specific game design requires. Also, as we would expect, a bullet has a BulletUpdateComponent and a different value for the [BITMAP NAME] element. Notice that the location of the bullet is set to -1, -1. This means the bullets are outside of the playable area at the start of the game. In the next chapter, we will see how an invader, or the player, can spawn them into action when required.

Now, study the following text, which describes the player's ship:

[START OBJECT]

[NAME]Player[-NAME]

[COMPONENT]Standard Graphics[-COMPONENT]

[COMPONENT]Transform[-COMPONENT]

[COMPONENT]Player Update[-COMPONENT]

[LOCATION X]50[-LOCATION X]

[LOCATION Y]40[-LOCATION Y]

[WIDTH]3.0[-WIDTH]

[HEIGHT]2.0[-HEIGHT]

[BITMAP NAME]playership[-BITMAP NAME]

[ENCOMPASSING RECT COLLIDER]player[-ENCOMPASSING_RECT COLLIDER]

[SPEED]10.0[-SPEED]

[END OBJECT]

The preceding text was probably quite predictable based on our discussion so far. Now that we've gone through this, we can get to work on coding the system that will interpret these object descriptions and convert them into usable GameObject instances.

Coding the GameObjectBlueprint class

Create a new header file in the Header Files/FileIO filter called GameObjectBlueprint.h and add the following code:

#pragma once

#include<vector>

#include<string>

#include<map>

using namespace std;

class GameObjectBlueprint {

private:

    string m_Name = "";

    vector<string> m_ComponentList;

    string m_BitmapName = "";

    float m_Width;

    float m_Height;

    float m_LocationX;

    float m_LocationY;

    float m_Speed;

    bool m_EncompassingRectCollider = false;

    string m_EncompassingRectColliderLabel = "";    

public:

    float getWidth();

    void setWidth(float width);

    float getHeight();

    void setHeight(float height);

    float getLocationX();

    void setLocationX(float locationX);

    float getLocationY();

    void setLocationY(float locationY);

    void setName(string name);

    string getName();

    vector<string>& getComponentList();

    void addToComponentList(string newComponent);

    string getBitmapName();

    void setBitmapName(string bitmapName);    

    string getEncompassingRectColliderLabel();

    bool getEncompassingRectCollider();

    void setEncompassingRectCollider(string label);

};

GameObjectBlueprint has a member variable for every possible property that could go into a game object. Note that it does not compartmentalize the properties by component. For example, it just has variables for things such as width, height, and location; it doesn't go to the trouble of identifying these as part of the transform component. These details are handled in the factory. It also provides getters and setters so that the BlueprintObjectParser class can pack away all the values from the level1.txt file and the GameObjectFactoryPlayMode class can extract all the values, instantiate the appropriate components, and add them to an instance of GameObject.

Create a new source file in the Source Files/FileIO filter called GameObjectBlueprint.cpp and add the following code, which is for the definitions of the functions we have just declared:

#include "GameObjectBlueprint.h"

float GameObjectBlueprint::getWidth()

{

    return m_Width;

}

void GameObjectBlueprint::setWidth(float width)

{

    m_Width = width;

}

float GameObjectBlueprint::getHeight()

{

    return m_Height;

}

void GameObjectBlueprint::setHeight(float height)

{

    m_Height = height;

}

float GameObjectBlueprint::getLocationX()

{

    return m_LocationX;

}

void GameObjectBlueprint::setLocationX(float locationX)

{

    m_LocationX = locationX;

}

float GameObjectBlueprint::getLocationY()

{

    return m_LocationY;

}

void GameObjectBlueprint::setLocationY(float locationY)

{

    m_LocationY = locationY;

}

void GameObjectBlueprint::setName(string name)

{

    m_Name = "" + name;

}

string GameObjectBlueprint::getName()

{

    return m_Name;

}

vector<string>& GameObjectBlueprint::getComponentList()

{

    return m_ComponentList;

}

void GameObjectBlueprint::addToComponentList(string newComponent)

{

    m_ComponentList.push_back(newComponent);

}

string GameObjectBlueprint::getBitmapName()

{

    return m_BitmapName;

}

void GameObjectBlueprint::setBitmapName(string bitmapName)

{

    m_BitmapName = "" + bitmapName;

}

string GameObjectBlueprint::getEncompassingRectColliderLabel()

{

    return m_EncompassingRectColliderLabel;

}

bool GameObjectBlueprint::getEncompassingRectCollider()

{

    return m_EncompassingRectCollider;

}

void GameObjectBlueprint::setEncompassingRectCollider(

    string label)

{

    m_EncompassingRectCollider = true;

    m_EncompassingRectColliderLabel = "" + label;

}

Although this is a long class, there is nothing here we haven't seen before. The setter functions receive values which are copied into a vector or a variable, while the getters allow access to these values.

Coding the ObjectTags class

The way in which we describe the game objects in the level1.txt file needs to be precise because the BlueprintObjectParser class we will code after this class will be reading the text from the file and looking for matches. For example, the [START OBJECT] tag will trigger the start of a new object. If that tag is misspelled as, say, [START OBJECR], then the whole system falls apart and there will be all kinds of bugs, and even crashes when we run the game. To avoid this happening, we will define constant (programmatically unchangeable) string variables for all the tags we need to describe the game objects. We can use these string variables instead of typing something such as [START OBJECT] and have much less chance of making a mistake.

Create a new header file in the Header Files/FileIO filter called ObjectTags.h and add the following code:

#pragma once

#include <string>

using namespace std;

static class ObjectTags {

public:

    static const string START_OF_OBJECT;

    static const string END_OF_OBJECT;

    static const string COMPONENT;

    static const string COMPONENT_END;

    static const string NAME;

    static const string NAME_END;

    static const string WIDTH;

    static const string WIDTH_END;

    static const string HEIGHT;

    static const string HEIGHT_END;

    static const string LOCATION_X;

    static const string LOCATION_X_END;

    static const string LOCATION_Y;

    static const string LOCATION_Y_END;

    static const string BITMAP_NAME;

    static const string BITMAP_NAME_END;

    static const string ENCOMPASSING_RECT_COLLIDER;

    static const string ENCOMPASSING_RECT_COLLIDER_END;

};

We have declared a const string for every tag we will use to describe the game objects. Now, we can initialize them.

Create a new source file in the Source Files/FileIO filter called ObjectTags.cpp and add the following code:

#include "DevelopState.h"

#include "objectTags.h"

const string ObjectTags::START_OF_OBJECT = "[START OBJECT]";

const string ObjectTags::END_OF_OBJECT = "[END OBJECT]";

const string ObjectTags::COMPONENT = "[COMPONENT]";

const string ObjectTags::COMPONENT_END = "[-COMPONENT]";

const string ObjectTags::NAME = "[NAME]";

const string ObjectTags::NAME_END = "[-NAME]";

const string ObjectTags::WIDTH = "[WIDTH]";

const string ObjectTags::WIDTH_END = "[-WIDTH]";

const string ObjectTags::HEIGHT = "[HEIGHT]";

const string ObjectTags::HEIGHT_END = "[-HEIGHT]";

const string ObjectTags::LOCATION_X = "[LOCATION X]";

const string ObjectTags::LOCATION_X_END = "[-LOCATION X]";

const string ObjectTags::LOCATION_Y = "[LOCATION Y]";

const string ObjectTags::LOCATION_Y_END = "[-LOCATION Y]";

const string ObjectTags::BITMAP_NAME = "[BITMAP NAME]";

const string ObjectTags::BITMAP_NAME_END = "[-BITMAP NAME]";

const string ObjectTags::ENCOMPASSING_RECT_COLLIDER =

    "[ENCOMPASSING RECT COLLIDER]";

    

const string ObjectTags::ENCOMPASSING_RECT_COLLIDER_END

    = "[-ENCOMPASSING_RECT COLLIDER]";

That's all the string variables initialized. We can now use them in the next class and be sure we are describing the game objects consistently.

Coding the BlueprintObjectParser class

This class will have the code that actually reads the text from the level1.txt file we have discussed. It will parse one object at a time, as identified by the start and end tags we saw previously.

Create a new header file in the Header Files/FileIO filter called BlueprintObjectParser.h and add the following code:

#pragma once

#include "GameObjectBlueprint.h"

#include <string>

using namespace std;

class BlueprintObjectParser {

private:

    string extractStringBetweenTags(

        string stringToSearch, string startTag, string endTag);

public:

    void parseNextObjectForBlueprint(

        ifstream& reader, GameObjectBlueprint& bp);

};

The extractStringBetweenTags private function will capture the content between two tags. The parameters are three string instances. The first string is a full line of text from level1.txt, while the second and third are the start and end tags, which need to be discarded. The text between the two tags is then returned to the calling code.

The parseNextObjectForBlueprint function receives an ifstream reader, just like the one we used in the Zombie shooter and the Thomas Was Late games. It is used to read from the file. The second parameter is a reference to a GameObjectBlueprint instance. The function will populate the GameObjectBlueprint instance with the values that were read from the level1.txt file, which can then be used back in the calling code to create an actual GameObject. We will see how that happens when we code the PlayModeObjectLoader class next and the GameObjectFactoryPlayMode class after that.

Let's code the definitions we have just discussed.

Create a new source file in the Source Files/FileIO filter called BlueprintObjectParser.cpp and add the following code:

#include "BlueprintObjectParser.h"

#include "ObjectTags.h"

#include <iostream>

#include <fstream>

void BlueprintObjectParser::parseNextObjectForBlueprint(

    ifstream& reader, GameObjectBlueprint& bp)

{

    string lineFromFile;

    string value = "";

    while (getline(reader, lineFromFile))

    {

        if (lineFromFile.find(ObjectTags::COMPONENT)

            != string::npos)

          {

            value = extractStringBetweenTags(lineFromFile,

                ObjectTags::COMPONENT,

                ObjectTags::COMPONENT_END);

            bp.addToComponentList(value);

        }

        else if (lineFromFile.find(ObjectTags::NAME)

            != string::npos)

          {

            value = extractStringBetweenTags(lineFromFile,

                ObjectTags::NAME, ObjectTags::NAME_END);

            bp.setName(value);

        }

        else if (lineFromFile.find(ObjectTags::WIDTH)

            != string::npos)

          {

            value = extractStringBetweenTags(lineFromFile,

                ObjectTags::WIDTH, ObjectTags::WIDTH_END);

            bp.setWidth(stof(value));

        }

        else if (lineFromFile.find(ObjectTags::HEIGHT)

            != string::npos)

          {

            value = extractStringBetweenTags(lineFromFile,

                ObjectTags::HEIGHT, ObjectTags::HEIGHT_END);

            bp.setHeight(stof(value));

        }

        else if (lineFromFile.find(ObjectTags::LOCATION_X)

            != string::npos)

          {

            value = extractStringBetweenTags(lineFromFile,

                ObjectTags::LOCATION_X,

                ObjectTags::LOCATION_X_END);

            bp.setLocationX(stof(value));

        }

        else if (lineFromFile.find(ObjectTags::LOCATION_Y)

            != string::npos)

          {

            value = extractStringBetweenTags(

                      lineFromFile,

                      ObjectTags::LOCATION_Y,

                      ObjectTags::LOCATION_Y_END);

            bp.setLocationY(stof(value));

        }

        else if (lineFromFile.find(ObjectTags::BITMAP_NAME)

            != string::npos)

          {

            value = extractStringBetweenTags(lineFromFile,

             ObjectTags::BITMAP_NAME,

             ObjectTags::BITMAP_NAME_END);

            bp.setBitmapName(value);

        }

        

        else if (lineFromFile.find(

            ObjectTags::ENCOMPASSING_RECT_COLLIDER)

            != string::npos)

          {

            value = extractStringBetweenTags(lineFromFile,

                ObjectTags::ENCOMPASSING_RECT_COLLIDER,

                ObjectTags::ENCOMPASSING_RECT_COLLIDER_END);

            bp.setEncompassingRectCollider(value);

        }

        

        else if (lineFromFile.find(ObjectTags::END_OF_OBJECT)

            != string::npos)

        {

            return;

        }

    }

}

string BlueprintObjectParser::extractStringBetweenTags(

    string stringToSearch, string startTag, string endTag)

{

    int start = startTag.length();

    int count = stringToSearch.length() - startTag.length()

        - endTag.length();

    string stringBetweenTags = stringToSearch.substr(

        start, count);

    return stringBetweenTags;

}

The code in parseNextObjectForBlueprint is lengthy but straightforward. The series of if statements identifies the starting tag at the beginning of the line of text and then passes the line of text to the extractStringBetweenTags function, which returns the value that is then loaded into the GameObjectBlueprint reference in the appropriate place. Notice that the function exits when GameObjectBlueprint has had all the data loaded into it. This point is recognized when ObjectTags::END_OF_OBJECT is found.

Coding the PlayModeObjectLoader class

This is the class that will pass GameObjectBlueprint instances to BlueprintObjectParser. When it gets the completed blueprint back, it will pass them to the GameObjectFactoryPlayMode class, which will construct the GameObject instance and pack it away in the vector instance. Once all the GameObject instances have been built and stored, responsibility will be handed to the LevelManager class, which will control access to the vector for other parts of the game engine. This is a very small class with just one function, but it links many other classes together. Refer to the diagram at the start of this chapter for clarification.

Create a new header file in the Header Files/FileIO filter called PlayModeObjectLoader.h and add the following code:

#pragma once

#include <vector>

#include <string>

#include "GameObject.h"

#include "BlueprintObjectParser.h"

#include "GameObjectFactoryPlayMode.h"

using namespace std;

class PlayModeObjectLoader {

private:

    BlueprintObjectParser m_BOP;

    GameObjectFactoryPlayMode m_GameObjectFactoryPlayMode;

public:

    void loadGameObjectsForPlayMode(

        string pathToFile, vector<GameObject>& mGameObjects);

};

The PlayModeObjectLoader class has an instance of the previous class we coded, that is, the BluePrintObjectParser class. It also has an instance of the class we will code next, that is, the GameObjectFactoryPlayMode class. It has a single public function, which receives a reference to a vector that holds GameObject instances.

Now, we will code the definition of the loadGameObjectsForPlayMode function. Create a new source file in the Source Files/FileIO filter called PlayModeObjectLoader.cpp and add the following code:

#include "PlayModeObjectLoader.h"

#include "ObjectTags.h"

#include <iostream>

#include <fstream>

void PlayModeObjectLoader::

    loadGameObjectsForPlayMode(

        string pathToFile, vector<GameObject>& gameObjects)

{

    ifstream reader(pathToFile);

    string lineFromFile;

    float x = 0, y = 0, width = 0, height = 0;

    string value = "";

    while (getline(reader, lineFromFile)) {

        if (lineFromFile.find(

            ObjectTags::START_OF_OBJECT) != string::npos) {

            GameObjectBlueprint bp;

            m_BOP.parseNextObjectForBlueprint(reader, bp);

            m_GameObjectFactoryPlayMode.buildGameObject(

                bp, gameObjects);

        }

    }       

}

The function receives a string, which is the path to the file that needs to be loaded. This game only has one such file, but you could add more files with different layouts, varying numbers of invaders, or totally different game objects if you wanted to.

An ifstream instance is used to read one line at a time from the file. In the while loop, the start tag is identified using ObjectTags::START_OF_OBJECT, and the parseNextObjectForBlueprint function of BlueprintObjectParser is called. You probably remember from the BlueprintObjectParser class that the completed blueprint is returned when ObjectTags::END_OF_OBJECT is reached.

The next line of code calls the buildGameObject of the GameObjectFactoryPlayMode class and passes in the GameObjectBlueprint instance. We will code the GameObjectFactory class now.

Coding the GameObjectFactoryPlayMode class

Now, we will code our factory, which will construct working game objects from the GameObject class and all the component related classes that we coded in the previous chapter. We will make extensive use of smart pointers, so we don't have to worry about deleting memory when we have finished with it.

Create a new header file in the Header Files/FileIO filter called GameObjectFactoryPlayMode.h and add the following code:

#pragma once

#include "GameObjectBlueprint.h"

#include "GameObject.h"

#include <vector>

class GameObjectFactoryPlayMode {

public:

    void buildGameObject(GameObjectBlueprint& bp,

        std::vector <GameObject>& gameObjects);

};

The factory class has just one function, buildGameObject. We have already seen the code that calls this function in the previous code we wrote for the PlayModeObjectLoader class. The function receives a reference to the blueprint, as well as a reference to the vector of GameObject instances.

Create a new source file in the Source Files/FileIO filter called GameObjectFactoryPlayMode.cpp and add the following code:

#include "GameObjectFactoryPlayMode.h"

#include <iostream>

#include "TransformComponent.h"

#include "StandardGraphicsComponent.h"

#include "PlayerUpdateComponent.h"

#include "RectColliderComponent.h"

#include "InvaderUpdateComponent.h"

#include "BulletUpdateComponent.h"

void GameObjectFactoryPlayMode::buildGameObject(

    GameObjectBlueprint& bp,

    std::vector<GameObject>& gameObjects)

{

    GameObject gameObject;

    gameObject.setTag(bp.getName());

    auto it = bp.getComponentList().begin();

    auto end = bp.getComponentList().end();

    for (it;

        it != end;

        ++it)

    {

        if (*it == "Transform")

        {

            gameObject.addComponent(

                make_shared<TransformComponent>(

                bp.getWidth(),

                bp.getHeight(),

                Vector2f(bp.getLocationX(),

                 bp.getLocationY())));

        }

        else if (*it == "Player Update")

        {

            gameObject.addComponent(make_shared

                <PlayerUpdateComponent>());

        }

        else if (*it == "Invader Update")

        {

            gameObject.addComponent(make_shared

                <InvaderUpdateComponent>());

        }

        else if (*it == "Bullet Update")

        {

            gameObject.addComponent(make_shared

                <BulletUpdateComponent>());

        }

        else if (*it == "Standard Graphics")

        {

            shared_ptr<StandardGraphicsComponent> sgp =

                make_shared<StandardGraphicsComponent>();

            gameObject.addComponent(sgp);

            sgp->initializeGraphics(

                bp.getBitmapName(),

                Vector2f(bp.getWidth(),

                    bp.getHeight()));

        }        

    }

    if (bp.getEncompassingRectCollider()) {

        shared_ptr<RectColliderComponent> rcc =

            make_shared<RectColliderComponent>(

            bp.getEncompassingRectColliderLabel());

        gameObject.addComponent(rcc);

        rcc->setOrMoveCollider(bp.getLocationX(),

            bp.getLocationY(),

            bp.getWidth(),

            bp.getHeight());

    }   

    

    gameObjects.push_back(gameObject);

}

The first thing that happens in the buildGameObject function is that a new GameObject instance is created and the setTag function of the GameObject class is used to pass in the name of the current object being built:

GameObject gameObject;

gameObject.setTag(bp.getName());

Next, a for loop loops through all the components in the m_Components vector. For each component that is found, a different if statement creates a component of the appropriate type. The way that each component is created varies, as you would expect since the way they are coded varies.

The following code creates a shared pointer to a TransformComponent instance. You can see the necessary arguments being passed to the constructor, that is, width, height, and location. The result of creating the new shared pointer to a TransformComponent instance is passed to the addComponent function of the GameObject class. The GameObject instance now has its size and place in the world:

if (*it == "Transform")

{

    gameObject.addComponent(make_shared<TransformComponent>(

        bp.getWidth(),

        bp.getHeight(),

        Vector2f(bp.getLocationX(), bp.getLocationY())));

}

The following code executes when a PlayerUpdateComponent is required. Again, the code creates a new shared pointer to the appropriate class and passes it in to the addComponent function of the GameObject instance:

else if (*it == "Player Update")

{

    gameObject.addComponent(make_shared

        <PlayerUpdateComponent>());

}

The following three blocks of code use exactly the same technique to add either an InvaderUpdateComponent, BulletUpdateComponent, or StandardGraphicsComponent instance. Notice the extra line of code after adding a StandardGraphicsComponent instance that calls the initialize function, which adds a Texture instance (if required) to the BitmapStore singleton and prepares the component to be drawn:

else if (*it == "Invader Update")

{

    gameObject.addComponent(make_shared

        <InvaderUpdateComponent>());

}

else if (*it == "Bullet Update")

{

    gameObject.addComponent(make_shared

        <BulletUpdateComponent>());

}

else if (*it == "Standard Graphics")

{

    shared_ptr<StandardGraphicsComponent> sgp =

        make_shared<StandardGraphicsComponent>();

    gameObject.addComponent(sgp);

    sgp->initializeGraphics(

        bp.getBitmapName(),

        Vector2f(bp.getWidth(),

            bp.getHeight()));

}

The final if block, as shown in the following code, handles adding a RectColliderComponent instance. The first line of code creates the shared pointer, while the second line of code calls the addComponent function to add the instance to the GameObject instance. The third line of code calls the setOrMoveCollider and passes in the location and size of the object. At this stage, the object is ready to be collided with. Obviously, we still need to write the code that tests for collisions. We will do so in the next chapter:

if (bp.getEncompassingRectCollider()) {

        shared_ptr<RectColliderComponent> rcc =

            make_shared<RectColliderComponent>(

            bp.getEncompassingRectColliderLabel());

        gameObject.addComponent(rcc);

        rcc->setOrMoveCollider(bp.getLocationX(),

            bp.getLocationY(),

            bp.getWidth(),

            bp.getHeight());

}

The following line of code in the class adds the just-constructed GameObject instance to the vector that will be shared with the GameScreen class and used to make the game come to life:

gameObjects.push_back(gameObject);

The next class we will write makes it easy to share the vector we have just filled with GameObject instances around the various classes of the project.

Coding the GameObjectSharer class

This class will have two pure virtual functions that share GameObject instances with other classes.

Create a new header file in the Header Files/FileIO filter called GameObjectSharer.h and add the following code:

#pragma once

#include<vector>

#include<string>

class GameObject;

class GameObjectSharer {

public:

    virtual std::vector<GameObject>& getGameObjectsWithGOS() = 0;

    virtual GameObject& findFirstObjectWithTag(

             std::string tag) = 0;

};

The getGameObjectsWithGOS function returns a reference to the entire vector of GameObject instances. The findFirstObjectWithTag function returns just a single GameObject reference. We will see how we implement these functions when we inherit from GameObjectSharer when we code the LevelManager class next.

Briefly, before the LevelManager class, create a new source file in the Source Files/FileIO filter called GameObjectSharer.cpp and add the following code:

/*********************************

******THIS IS AN INTERFACE********

*********************************/

Again, this is just a placeholder file and the full functionality goes in any of the classes that inherit from GameObjectSharer; in this case, the LevelManager class.

Coding the LevelManager class

The LevelManager class is the connection between what we coded in Chapter 19, Game Programming Design Patterns – Starting the Space Invaders ++ Game, and everything we coded in this chapter. The ScreenManager class will have an instance of the LevelManager class, and the LevelManager class will instigate loading levels (using all the classes we have just coded) and share GameObject instances with any classes that need them.

Create a new header file in the Header Files/Engine filter called LevelManager.h and add the following code:

#pragma once

#include "GameObject.h"

#include <vector>

#include <string>

#include "GameObjectSharer.h"

using namespace std;

class LevelManager : public GameObjectSharer {

private:

    vector<GameObject> m_GameObjects;

    const std::string WORLD_FOLDER = "world";

    const std::string SLASH = "/";

    void runStartPhase();

    void activateAllGameObjects();

public:

    vector<GameObject>& getGameObjects();

    void loadGameObjectsForPlayMode(string screenToLoad);

    /****************************************************

    *****************************************************

    From GameObjectSharer interface

    *****************************************************

    *****************************************************/

    vector<GameObject>& GameObjectSharer::getGameObjectsWithGOS()

    {

        return m_GameObjects;

    }

    GameObject& GameObjectSharer::findFirstObjectWithTag(

         string tag)

    {

        auto it = m_GameObjects.begin();

        auto end = m_GameObjects.end();

        for (it;

            it != end;

            ++it)

        {

            if ((*it).getTag() == tag)

            {

                return (*it);

            }

        }

        

#ifdef debuggingErrors        

    cout <<

        "LevelManager.h findFirstGameObjectWithTag() "

        << "- TAG NOT FOUND ERROR!"

        << endl;

#endif    

        return m_GameObjects[0];

    }

};

This class provides two different ways to get the vector full of the game objects. One way is via a simple call to getGameObjects, but another is via the getGameObjectsWithGOS function. The latter is the implementation of a pure virtual function from the GameObjectSharer class and will be a way to pass access to each and every game object so that is has access to all the other game objects. You may recall from Chapter 20, Game Objects and Components, that a GameObjectSharer instance is passed in during the start function call of the GameObject class. It was in this function that, among other things, the invaders could get access to the location of the player.

There are also two private functions: runStartPhase, which loops through all the GameObject instances calling start, and activateAllGameObjects, which loops through and sets all the GameObject instances to the active status.

Also, part of the LevelManager class is the loadGameObjectsForPlayMode function, which will trigger the entire game object creation process that the rest of this chapter has described.

The final function in the LevelManger.h file is the implementation of the other GameObjectSharer pure virtual function, findFirstObjectWithTag. This allows any class with a GameObjectSharer instance to track down a specific game object using its tag. The code loops through all the GameObject instances in the vector and returns the first match. Note, that if no match is found, a null pointer will be returned and crash the game. We use an #ifdef statement to output some text to the console to tell us what caused the crash so that we won't be scratching our heads for hours should we accidentally search for a tag that doesn't exist.

We can now code the implementations of the functions.

Create a new source file in the Source Files/Engine filter called LevelManager.cpp and add the following code:

#include "LevelManager.h"

#include "PlayModeObjectLoader.h"

#include <iostream>

void LevelManager::

    loadGameObjectsForPlayMode(string screenToLoad)

{

    m_GameObjects.clear();

    string levelToLoad = ""

        + WORLD_FOLDER + SLASH + screenToLoad;

    PlayModeObjectLoader pmol;

    pmol.loadGameObjectsForPlayMode(

        levelToLoad, m_GameObjects);

    runStartPhase();

}

vector<GameObject>& LevelManager::getGameObjects()

{

    return m_GameObjects;

}

void LevelManager::runStartPhase()

{

    auto it = m_GameObjects.begin();

    auto end = m_GameObjects.end();

    for (it;

        it != end;

        ++it)

    {

        (*it).start(this);

    }

    activateAllGameObjects();

}

void LevelManager::activateAllGameObjects()

{

    auto it = m_GameObjects.begin();

    auto end = m_GameObjects.end();

    for (it;

        it != end;

        ++it)

    {

        (*it).setActive();

    }

}

The loadLevelForPlayMode function clears the vector, instantiates a PlayModeObjectLoader instance that does all the file reading, and packs the GameObject instances in the vector. Finally, the runStartPhase function is called. In the runStartPhase function, all the GameObject instances are passed a GameObjectSharer (this) and given the opportunity to set themselves up, ready to be played. Remember that, inside the GameObject class in the start function, each of the derived Component instances is given access to GameObjectSharer. Refer to Chapter 20, Game Objects and Components, to see what we did with this when we coded the Component classes.

The runStartPhase function concludes by calling activateAllGameObjects, which loops through the vector, calling setActive on every GameObject instance.

The getGameObjects function passes a reference to the vector of GameObject instances.

Now that we have coded the LevelManager class, we can update the ScreenManager and the ScreenManagerRemoteControl classes that it implements.

Updating the ScreenManager and ScreenManagerRemoteControl classes

Open the ScreenManagerRemoteControl.h file and uncomment everything so that the code is the same as the following. I have highlighted the lines that have been uncommented:

#pragma once

#include <string>

#include <vector>

#include "GameObject.h"

#include "GameObjectSharer.h"

using namespace std;

class ScreenManagerRemoteControl

{

public:

    virtual void SwitchScreens(string screenToSwitchTo) = 0;

    virtual void loadLevelInPlayMode(string screenToLoad) = 0;

    virtual vector<GameObject>& getGameObjects() = 0;

    virtual GameObjectSharer& shareGameObjectSharer() = 0;

};

Next, open ScreenManager.h, which implements this interface and uncomments all the commented-out code. The code in question is abbreviated and highlighted as follows:

...

#include "SelectScreen.h"

//#include "LevelManager.h"

#include "BitmapStore.h"

...

...

private:

    map <string, unique_ptr<Screen>> m_Screens;

    //LevelManager m_LevelManager;

protected:

    ...

    ...

/****************************************************

*****************************************************

From ScreenManagerRemoteControl interface

*****************************************************

*****************************************************/

    ...

    ...

    //vector<GameObject>&

        //ScreenManagerRemoteControl::getGameObjects()

    //{

        //return m_LevelManager.getGameObjects();

    //}

    //GameObjectSharer& shareGameObjectSharer()

    //{

        //return m_LevelManager;

    //}

    ...

    ...

Be sure to uncomment the include directive, the m_LevelManager instance, as well as the two functions.

The ScreenManager and ScreenManagerRemoteControl classes are now fully functional and the getGameObjects and shareGameObjectSharer functions are usable by any class with a reference to the ScreenManager class.

Where are we now?

At this point, all the errors in our GameObject class, as well as all component-related classes, are gone. We are making good progress.

Furthermore, we can revisit the ScreenManager.h file and uncomment all the commented-out code.

Open ScreenManager.h and uncomment the #include directive, as follows:

//#include "LevelManager.h"

Change it to this:

#include "LevelManager.h"

Do the same for the functions from the ScreenManagerRemoteControl interface that are implemented in ScreenManager.h. They look like the following:

void ScreenManagerRemoteControl::

        loadLevelInPlayMode(string screenToLoad)

    {

        //m_LevelManager.getGameObjects().clear();

        //m_LevelManager.

            //loadGameObjectsForPlayMode(screenToLoad);

        SwitchScreens("Game");

    }

//vector<GameObject>&

    //ScreenManagerRemoteControl::getGameObjects()

//{

    //return m_LevelManager.getGameObjects();

//}

Change them as follows:

void ScreenManagerRemoteControl::

    loadLevelInPlayMode(string screenToLoad)

{

    m_LevelManager.getGameObjects().clear();

    m_LevelManager.

        loadGameObjectsForPlayMode(screenToLoad);

    SwitchScreens("Game");

}

vector<GameObject>&

    ScreenManagerRemoteControl::getGameObjects()

{

    return m_LevelManager.getGameObjects();

}

We aren't quite ready to run the game, however, because there are still some missing classes that are used in the code, such as BulletSpawner in the InvaderUpdateComponent class.

Summary

In this chapter, we have put in place a way to describe a level in a game and a system to interpret the description and build usable GameObject instances. The Factory pattern is used in many types of programming, not just game development. The implementation we have used is the simplest possible implementation and I encourage you to put the Factory pattern on your list of patterns to research and develop further. The implementation we have used should serve you well if you wish to build some deep and interesting games, however.

In the next chapter, we will finally make the game come to life by adding collision detection, bullet spawning, and the logic of the game itself.

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

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