Skip to content

Latest commit

 

History

History
967 lines (822 loc) · 40.2 KB

File metadata and controls

967 lines (822 loc) · 40.2 KB

二十一、文件输入输出和游戏对象工厂

本章讲述了一个GameObject如何进入游戏中使用的m_GameObjects vector。我们将研究如何在文本文件中描述单个对象和整个级别。我们将编写代码来解释文本,然后将值加载到一个类中,该类将是一个游戏对象的蓝图。我们还将编写一个名为LevelManager的类来监督整个过程,从最初请求加载从InputHandler通过ScreenManager发送的关卡,一直到工厂模式类,工厂模式类从组件组装游戏对象并将其发送到LevelManager,整齐地打包在m_GameObjects vector中。

以下是我们将在本章中经历的步骤:

  • 检查我们将如何在文本文件中描述游戏对象及其组件
  • GameObjectBlueprint类进行编码,文本文件中的数据将临时存储在该类中
  • ObjectTags类进行编码,以帮助一致且无错误地描述游戏对象
  • 代码BluePrintObjectParser,负责将文本文件中游戏对象描述的数据加载到GameObjectBlueprint实例中
  • 代码PlayModeObjectLoader,打开文本文件,从BlueprintObjectParser一次接收一个GameObjectBlueprint实例
  • GameObjectFactoryPlayMode类进行编码,该类将从GameObjectBlueprint实例构造GameObject实例
  • LevelManager类进行编码,该类在收到ScreenManager类的指令后监督整个过程
  • 将代码添加到ScreenManager类中,这样我们就可以开始使用我们将在本章中编码的新系统

让我们从检查我们如何准确地描述一个游戏对象开始,比如一个空间入侵者或文本文件中的一颗子弹,更不用说一整波了。

文件 I/O 和工厂类的结构

请看下图,该图概述了我们将在本章中编码的类,以及GameObject实例的vector将如何与我们在 第 19 章游戏编程设计模式中编码的ScreenManager类共享——启动空间入侵者++ 游戏:

上图显示了四个类之间共享的GameObject实例的vector。这是通过引用在类的函数之间传递vector来实现的。然后,每个类都可以使用vector及其内容来执行其角色。当一个新的等级需要加载到vector中时,ScreenManager等级将触发LevelManager等级。单个Screen类及其InputHandler衍生类,正如我们在 第 19 章游戏编程设计模式–启动太空入侵者++ 游戏中看到的,可以通过ScreenManagerRemoteControl访问ScreenManager

LevelManager类最终负责创建和共享向量。PlayModeObjectLoader将使用BlueprintObjectParser创建GameObjectBlueprint实例。

PlayModeObjectLoader提示时,GameObjectFactoryPlayMode类将使用这些GameObjectBlueprint实例完成GameObject创建过程并将GameObject实例打包到vector中。

那么,每个GameObject实例的不同组件、位置、尺寸和外观配置从何而来?

我们还可以看到三个类可以访问一个GameObjectBlueprint实例。这个实例由LevelManager类创建,并通过引用传递。BlueprintObjectParser将读取level1.txt文件,其中包含每个游戏对象的所有细节。它将初始化GameObjectBlueprint类的所有变量。PlayModeObjectLoader随后将传递对GameObject实例的vector的引用,并将对完全配置的GameObjectBlueprint实例的引用传递给GameObjectFactoryPlayMode类。如此重复,直到所有GameObject实例都打包到vector中。

你可能想知道为什么我使用了稍微麻烦的类名,比如GameObjectFactoryPlayModePlayModeObjectLoader。原因是,一旦您看到这个系统有多方便,您可能会喜欢构建工具,允许您通过在需要的地方拖放来以可视化的方式设计级别,然后让文本文件自动生成而不是键入。这并不特别复杂,但我不得不在某个时候停止向游戏中添加功能。因此,你很可能会得到一个GameObjectFactoryDesignMode和一个DesignModeObjectLoader

描述世界上的一个物体

我们已经在 第 19 章游戏编程设计模式-启动太空入侵者++ 游戏world文件夹中添加了level1.txt文件。让我们讨论它的用途,未来的预期用途,以及它的内容。

首先,我想指出,射手游戏并不是演示如何在这样的文本文件中描述游戏世界的最佳方式。之所以会这样,是因为游戏对象只有很少几种,最常见的一种,入侵者,都像阅兵的士兵一样整齐划一地排着队。它们实际上会被更有效地编程描述,也许是在嵌套的for循环中。然而,这个项目的目的是展示想法,而不是学习如何制作太空入侵者克隆体。

请看下面的文字,这是来自world文件夹中level1.txt文件的样本:

[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]

前面的文字描述了游戏中的单个对象;在这种情况下,入侵者。该对象以下列文本开头:

[START OBJECT]

这将通知我们将要编写的代码,一个新的对象正在被描述。在文本中,我们可以看到以下内容:

[NAME]invader[-NAME]

这通知代码对象的类型是入侵者。这最终将被设置为ColliderComponent类的m_Tag。入侵者会被识别出来。接下来的文字如下:

[COMPONENT]Standard Graphics[-COMPONENT]
[COMPONENT]Invader Update[-COMPONENT]
[COMPONENT]Transform[-COMPONENT]

这告诉我们的系统,这个对象将添加三个组件:一个StandardGraphicsComponent实例、一个InvaderUpdateComponent实例和一个TransformComponent实例。这意味着物体将以标准的方式绘制,并按照我们为入侵者编写的规则运行。这也将意味着它在游戏世界中有位置和规模。有可能对象没有任何组件或组件较少。一个不采取动作也不移动的对象将不需要更新组件,一个不可见的对象将不需要图形组件(也许只是一个触发某些动作的不可见碰撞器),一个在世界上没有位置的对象(也许是一个调试对象)将不需要变换组件。

对象的位置和比例由以下四行文本决定:

[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]

现在,让我们来看看如何描述子弹物体:

[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]

这与入侵者非常相似,但又不相同。项目符号对象有附加数据,如设定速度。入侵者的速度在InvaderUpdateComponent类的逻辑中设定。我们也可以为了子弹的速度而这样做,但这表明你可以根据具体游戏设计的要求来描述物体的细节。此外,正如我们所料,子弹有一个BulletUpdateComponent和一个不同的[BITMAP NAME]元素值。请注意,项目符号的位置设置为-1,-1。这意味着游戏开始时子弹在可玩区域之外。在下一章中,我们将会看到一个入侵者,或者玩家,如何在需要的时候将他们变成行动。

现在,研究以下描述玩家船的文本:

[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]

根据我们迄今为止的讨论,前面的案文可能是完全可以预见的。现在我们已经完成了这一步,我们可以开始对解释这些对象描述的系统进行编码,并将它们转换成可用的GameObject实例。

编写游戏对象蓝图类

在名为GameObjectBlueprint.hHeader Files/FileIO过滤器中创建新的头文件,并添加以下代码:

#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对于每个可能进入游戏对象的属性都有一个成员变量。请注意,它没有按组件划分属性。例如,它只有宽度、高度和位置等变量;它不会麻烦地将这些识别为转换组件的一部分。这些细节在工厂里处理。它还提供了获取器和设置器,以便BlueprintObjectParser类可以打包掉level1.txt文件中的所有值,GameObjectFactoryPlayMode类可以提取所有值,实例化适当的组件,并将它们添加到GameObject的实例中。

在名为GameObjectBlueprint.cppSource Files/FileIO过滤器中创建一个新的源文件,并添加以下代码,用于我们刚刚声明的函数的定义:

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

虽然这是一堂很长的课,但这里没有我们以前没有见过的东西。setter 函数接收复制到向量或变量中的值,而 getter 函数允许访问这些值。

对对象标签类进行编码

我们在level1.txt文件中描述游戏对象的方式需要精确,因为我们将在这个类之后编码的BlueprintObjectParser类将从文件中读取文本并寻找匹配。例如,[START OBJECT]标签将触发新对象的开始。如果那个标签拼错了,比如说[START OBJECR],那么整个系统就会分崩离析,会出现各种各样的 bug,甚至在我们运行游戏的时候崩溃。为了避免这种情况发生,我们将为描述游戏对象所需的所有标签定义常量(以编程方式不可更改)string变量。我们可以使用这些string变量,而不是输入像[START OBJECT]这样的东西,出错的机会就少得多。

在名为ObjectTags.hHeader Files/FileIO过滤器中创建新的头文件,并添加以下代码:

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

我们已经为每个用来描述游戏对象的标签声明了一个const string。现在,我们可以初始化它们。

在名为ObjectTags.cppSource Files/FileIO过滤器中创建新的源文件,并添加以下代码:

#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]";

以上就是初始化的所有string变量。我们现在可以在下一节课中使用它们,并确保我们一致地描述游戏对象。

对 BlueprintObjectParser 类进行编码

这个类将拥有从我们已经讨论过的level1.txt文件中实际读取文本的代码。它将一次解析一个对象,正如我们之前看到的开始和结束标签所标识的那样。

在名为BlueprintObjectParser.hHeader Files/FileIO过滤器中创建新的头文件,并添加以下代码:

#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);
};

extractStringBetweenTags私有函数将捕获两个标签之间的内容。参数是三个string实例。第一个string是从level1.txt开始的一整行文字,第二个和第三个是开始和结束标签,需要丢弃。然后,两个标记之间的文本返回给调用代码。

parseNextObjectForBlueprint功能接收一个ifstream阅读器,就像我们在僵尸射手和托马斯迟到游戏中使用的那个一样。它用于读取文件。第二个参数是对GameObjectBlueprint实例的引用。该函数将使用从level1.txt文件中读取的值填充GameObjectBlueprint实例,然后可以在调用代码中使用这些值来创建实际的GameObject。当我们接下来对PlayModeObjectLoader类和之后的GameObjectFactoryPlayMode类进行编码时,我们将看到这是如何发生的。

让我们对刚才讨论的定义进行编码。

在名为BlueprintObjectParser.cppSource Files/FileIO过滤器中创建新的源文件,并添加以下代码:

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

parseNextObjectForBlueprint中的代码很长,但很简单。一系列if语句识别文本行开头的起始标记,然后将文本行传递给extractStringBetweenTags函数,该函数返回值,然后将该值加载到适当位置的GameObjectBlueprint引用中。请注意,当GameObjectBlueprint已经将所有数据加载到函数中时,函数退出。发现ObjectTags::END_OF_OBJECT时,识别出这一点。

对 PlayModeObjectLoader 类进行编码

这是将GameObjectBlueprint实例传递给BlueprintObjectParser的类。当它得到完整的蓝图时,它将把它们传递给GameObjectFactoryPlayMode类,后者将构建GameObject实例并将其打包到vector实例中。一旦所有的GameObject实例被建立和存储,责任将被交给LevelManager类,它将控制游戏引擎其他部分对向量的访问。这是一个非常小的类,只有一个函数,但它将许多其他类链接在一起。请参考本章开头的图表进行说明。

在名为PlayModeObjectLoader.hHeader Files/FileIO过滤器中创建新的头文件,并添加以下代码:

#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);
};

PlayModeObjectLoader类有一个我们编码的前一个类的实例,也就是BluePrintObjectParser类。它还有一个我们接下来要编码的类的实例,即GameObjectFactoryPlayMode类。它有一个单一的公共功能,接收对保存GameObject实例的vector的引用。

现在,我们将对loadGameObjectsForPlayMode函数的定义进行编码。在名为PlayModeObjectLoader.cppSource Files/FileIO过滤器中创建新的源文件,并添加以下代码:

#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);
        }
    }       
}

该函数接收一个string,它是需要加载的文件的路径。这个游戏只有一个这样的文件,但是你可以添加更多不同布局的文件,不同数量的入侵者,或者完全不同的游戏对象,如果你想的话。

一个ifstream实例用于从文件中一次读取一行。在while循环中,使用ObjectTags::START_OF_OBJECT识别起始标签,调用BlueprintObjectParserparseNextObjectForBlueprint功能。您可能还记得BlueprintObjectParser课,当到达ObjectTags::END_OF_OBJECT时,已完成的蓝图被返回。

下一行代码调用GameObjectFactoryPlayMode类的buildGameObject,并传入GameObjectBlueprint实例。我们现在将对GameObjectFactory类进行编码。

编码游戏对象要素类型 layMode 类

现在,我们将对我们的工厂进行编码,该工厂将从GameObject类和我们在上一章中编码的所有组件相关类中构造工作游戏对象。我们将广泛使用智能指针,所以当我们完成它时,我们不必担心删除内存。

在名为GameObjectFactoryPlayMode.hHeader Files/FileIO过滤器中创建新的头文件,并添加以下代码:

#pragma once
#include "GameObjectBlueprint.h"
#include "GameObject.h"
#include <vector>
class GameObjectFactoryPlayMode {
public:
    void buildGameObject(GameObjectBlueprint& bp, 
        std::vector <GameObject>& gameObjects);
};

工厂类只有一个功能,buildGameObject。我们已经在之前为PlayModeObjectLoader类编写的代码中看到了调用该函数的代码。该函数接收对蓝图的引用,以及对GameObject实例的vector的引用。

在名为GameObjectFactoryPlayMode.cppSource Files/FileIO过滤器中创建新的源文件,并添加以下代码:

#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);
}

buildGameObject函数中发生的第一件事是创建一个新的GameObject实例,并使用GameObject类的setTag函数传入正在构建的当前对象的名称:

GameObject gameObject;
gameObject.setTag(bp.getName());

接下来,for循环通过m_Components vector中的所有组件。对于找到的每个组件,一个不同的if 语句创建一个适当类型的组件。正如您所料,每个组件的创建方式各不相同,因为它们的编码方式也各不相同。

下面的代码创建了一个指向TransformComponent实例的共享指针。您可以看到传递给构造函数的必要参数,即宽度、高度和位置。创建指向TransformComponent实例的新共享指针的结果被传递给GameObject类的addComponent函数。GameObject实例现在在世界上有其规模和地位:

if (*it == "Transform")
{
    gameObject.addComponent(make_shared<TransformComponent>(
        bp.getWidth(),
        bp.getHeight(),
        Vector2f(bp.getLocationX(), bp.getLocationY())));
}

当需要PlayerUpdateComponent时,执行以下代码。同样,代码创建一个指向适当类的新共享指针,并将其传递给GameObject实例的addComponent函数:

else if (*it == "Player Update")
{
    gameObject.addComponent(make_shared
        <PlayerUpdateComponent>());
}

以下三个代码块使用完全相同的技术来添加InvaderUpdateComponentBulletUpdateComponentStandardGraphicsComponent实例。请注意添加调用initialize函数的StandardGraphicsComponent实例后的额外代码行,该实例将Texture实例(如果需要)添加到BitmapStore单例中,并准备要绘制的组件:

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 块,如下面的代码所示,处理添加RectColliderComponent实例。第一行代码创建共享指针,而第二行代码调用addComponent函数将实例添加到GameObject实例。第三行代码调用setOrMoveCollider并传递对象的位置和大小。在这个阶段,物体已经准备好被碰撞。显然,我们仍然需要编写测试冲突的代码。我们将在下一章中这样做:

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());
}

该类中的下面一行代码将刚刚构建的GameObject实例添加到vector中,该实例将与GameScreen类共享,并用于使游戏变得生动起来:

gameObjects.push_back(gameObject);

我们将编写的下一个类使得共享我们刚刚填充的围绕项目各个类的vector实例变得容易。

编写游戏对象共享类的代码

这个类将有两个与其他类共享GameObject实例的纯虚函数。

在名为GameObjectSharer.hHeader Files/FileIO过滤器中创建新的头文件,并添加以下代码:

#pragma once
#include<vector>
#include<string>
class GameObject;
class GameObjectSharer {
public:
    virtual std::vector<GameObject>& getGameObjectsWithGOS() = 0;
    virtual GameObject& findFirstObjectWithTag(
             std::string tag) = 0;
};

getGameObjectsWithGOS函数返回对整个GameObject实例向量的引用。findFirstObjectWithTag函数只返回一个GameObject引用。当我们接下来对LevelManager类进行编码时,我们将看到如何从GameObjectSharer继承这些函数。

简而言之,在LevelManager类之前,在名为GameObjectSharer.cppSource Files/FileIO过滤器中创建新的源文件,并添加以下代码:

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

同样,这只是一个占位符文件,所有功能都在继承自GameObjectSharer的任何类中;在这种情况下,LevelManager类。

对级别管理器类进行编码

LevelManager类是我们在 第 19 章游戏编程设计模式–启动太空入侵者++ 游戏中编码的内容与我们在本章中编码的所有内容之间的联系。ScreenManager类将拥有一个LevelManager类的实例,LevelManager类将发起加载级别(使用我们刚刚编码的所有类)并与任何需要它们的类共享GameObject实例。

在名为LevelManager.hHeader Files/Engine过滤器中创建新的头文件,并添加以下代码:

#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];
    }
};

这个类提供了两种不同的方法来使vector充满游戏对象。一种方法是通过对getGameObjects的简单调用,但另一种方法是通过getGameObjectsWithGOS功能。后者是来自GameObjectSharer类的纯虚拟函数的实现,并且将是一种传递对每个游戏对象的访问的方式,从而可以访问所有其他游戏对象。您可能还记得 第 20 章游戏对象和组件中,在GameObject类的start函数调用期间传入了一个GameObjectSharer实例。在这个功能中,入侵者可以访问玩家的位置。

还有两个私有函数:runStartPhase,它循环遍历所有调用 start 的GameObject实例,activateAllGameObjects,它循环遍历所有GameObject实例并将其设置为活动状态。

此外,LevelManager类的一部分是loadGameObjectsForPlayMode函数,它将触发本章其余部分描述的整个游戏对象创建过程。

LevelManger.h文件中的最后一个函数是另一个GameObjectSharer纯虚函数findFirstObjectWithTag的实现。这允许任何具有GameObjectSharer实例的类使用其标签来追踪特定的游戏对象。该代码遍历vector中的所有GameObject实例,并返回第一个匹配项。请注意,如果没有找到匹配,将返回一个空指针并使游戏崩溃。我们使用#ifdef语句向控制台输出一些文本,告诉我们是什么导致了崩溃,这样,如果我们不小心搜索到一个不存在的标签,我们就不会在几个小时内摸不着头脑。

我们现在可以对函数的实现进行编码。

在名为LevelManager.cppSource Files/Engine过滤器中创建新的源文件,并添加以下代码:

#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();
    }
}

loadLevelForPlayMode函数清除vector,实例化一个完成所有文件读取的PlayModeObjectLoader实例,并将GameObject实例打包到vector中。最后调用runStartPhase函数。在runStartPhase功能中,所有的GameObject实例都被传递一个GameObjectSharer ( this)并有机会进行自我设置,准备播放。请记住,在start函数的GameObject类中,每个派生的Component实例都可以访问GameObjectSharer。参考 第二十章游戏对象和组件,看看我们在对Component类进行编码时是怎么处理的。

runStartPhase函数以调用activateAllGameObjects结束,它循环通过vector,在每个GameObject实例上调用setActive

getGameObjects函数传递对GameObject实例的vector的引用。

现在我们已经编码了LevelManager类,我们可以更新它实现的ScreenManagerScreenManagerRemoteControl类。

更新屏幕管理器和屏幕管理器远程控制类

打开ScreenManagerRemoteControl.h文件,取消注释所有内容,使代码如下所示。我强调了未注释的行:

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

接下来打开ScreenManager.h,实现这个接口,取消注释掉所有注释掉的代码。所述代码被缩写并突出显示如下:

...
#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;
 //}
    ...
    ...

请务必取消对 include 指令、m_LevelManager实例以及两个函数的注释。

ScreenManagerScreenManagerRemoteControl类现在功能齐全,getGameObjectsshareGameObjectSharer功能可以被任何引用了ScreenManager类的类使用。

我们现在在哪里?

此时,我们的GameObject类中的所有错误,以及所有组件相关的类都消失了。我们正在取得良好的进展。

此外,我们可以重新访问ScreenManager.h文件并取消注释所有注释掉的代码。

打开ScreenManager.h并取消注释#include指令,如下所示:

//#include "LevelManager.h"

将其更改为:

#include "LevelManager.h"

对于在ScreenManager.h中实现的ScreenManagerRemoteControl界面的功能也是如此。它们看起来如下:

void ScreenManagerRemoteControl::
        loadLevelInPlayMode(string screenToLoad)
    {
        //m_LevelManager.getGameObjects().clear();
        //m_LevelManager.
            //loadGameObjectsForPlayMode(screenToLoad);
        SwitchScreens("Game");
    }
//vector<GameObject>& 
    //ScreenManagerRemoteControl::getGameObjects()
//{
    //return m_LevelManager.getGameObjects();
//}

按如下方式进行更改:

void ScreenManagerRemoteControl::
    loadLevelInPlayMode(string screenToLoad)
{
    m_LevelManager.getGameObjects().clear();
    m_LevelManager.
        loadGameObjectsForPlayMode(screenToLoad);
    SwitchScreens("Game");
}
vector<GameObject>& 
    ScreenManagerRemoteControl::getGameObjects()
{
    return m_LevelManager.getGameObjects();
}

然而,我们还没有完全准备好运行游戏,因为代码中仍然有一些缺失的类被使用,比如InvaderUpdateComponent类中的BulletSpawner

总结

在这一章中,我们已经建立了一种描述游戏中某个关卡的方法,以及一个解释描述并构建可用GameObject实例的系统。工厂模式用于许多类型的编程,不仅仅是游戏开发。我们使用的实现是最简单的实现,我鼓励您将工厂模式放在您的模式列表中,以便进一步研究和开发。然而,如果您希望构建一些深度且有趣的游戏,我们使用的实现应该可以很好地为您服务。

在下一章中,我们将通过添加碰撞检测、子弹产卵和游戏本身的逻辑,最终使游戏变得栩栩如生。