本章讲述了一个GameObject
如何进入游戏中使用的m_GameObjects vector
。我们将研究如何在文本文件中描述单个对象和整个级别。我们将编写代码来解释文本,然后将值加载到一个类中,该类将是一个游戏对象的蓝图。我们还将编写一个名为LevelManager
的类来监督整个过程,从最初请求加载从InputHandler
通过ScreenManager
发送的关卡,一直到工厂模式类,工厂模式类从组件组装游戏对象并将其发送到LevelManager
,整齐地打包在m_GameObjects vector
中。
以下是我们将在本章中经历的步骤:
- 检查我们将如何在文本文件中描述游戏对象及其组件
- 对
GameObjectBlueprint
类进行编码,文本文件中的数据将临时存储在该类中 - 对
ObjectTags
类进行编码,以帮助一致且无错误地描述游戏对象 - 代码
BluePrintObjectParser
,负责将文本文件中游戏对象描述的数据加载到GameObjectBlueprint
实例中 - 代码
PlayModeObjectLoader
,打开文本文件,从BlueprintObjectParser
一次接收一个GameObjectBlueprint
实例 - 对
GameObjectFactoryPlayMode
类进行编码,该类将从GameObjectBlueprint
实例构造GameObject
实例 - 对
LevelManager
类进行编码,该类在收到ScreenManager
类的指令后监督整个过程 - 将代码添加到
ScreenManager
类中,这样我们就可以开始使用我们将在本章中编码的新系统
让我们从检查我们如何准确地描述一个游戏对象开始,比如一个空间入侵者或文本文件中的一颗子弹,更不用说一整波了。
请看下图,该图概述了我们将在本章中编码的类,以及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
中。
你可能想知道为什么我使用了稍微麻烦的类名,比如GameObjectFactoryPlayMode
和PlayModeObjectLoader
。原因是,一旦您看到这个系统有多方便,您可能会喜欢构建工具,允许您通过在需要的地方拖放来以可视化的方式设计级别,然后让文本文件自动生成而不是键入。这并不特别复杂,但我不得不在某个时候停止向游戏中添加功能。因此,你很可能会得到一个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.h
的Header 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.cpp
的Source 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.h
的Header 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.cpp
的Source 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
变量。我们现在可以在下一节课中使用它们,并确保我们一致地描述游戏对象。
这个类将拥有从我们已经讨论过的level1.txt
文件中实际读取文本的代码。它将一次解析一个对象,正如我们之前看到的开始和结束标签所标识的那样。
在名为BlueprintObjectParser.h
的Header 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.cpp
的Source 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
时,识别出这一点。
这是将GameObjectBlueprint
实例传递给BlueprintObjectParser
的类。当它得到完整的蓝图时,它将把它们传递给GameObjectFactoryPlayMode
类,后者将构建GameObject
实例并将其打包到vector
实例中。一旦所有的GameObject
实例被建立和存储,责任将被交给LevelManager
类,它将控制游戏引擎其他部分对向量的访问。这是一个非常小的类,只有一个函数,但它将许多其他类链接在一起。请参考本章开头的图表进行说明。
在名为PlayModeObjectLoader.h
的Header 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.cpp
的Source 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
识别起始标签,调用BlueprintObjectParser
的parseNextObjectForBlueprint
功能。您可能还记得BlueprintObjectParser
课,当到达ObjectTags::END_OF_OBJECT
时,已完成的蓝图被返回。
下一行代码调用GameObjectFactoryPlayMode
类的buildGameObject
,并传入GameObjectBlueprint
实例。我们现在将对GameObjectFactory
类进行编码。
现在,我们将对我们的工厂进行编码,该工厂将从GameObject
类和我们在上一章中编码的所有组件相关类中构造工作游戏对象。我们将广泛使用智能指针,所以当我们完成它时,我们不必担心删除内存。
在名为GameObjectFactoryPlayMode.h
的Header 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.cpp
的Source 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>());
}
以下三个代码块使用完全相同的技术来添加InvaderUpdateComponent
、BulletUpdateComponent
或StandardGraphicsComponent
实例。请注意添加调用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.h
的Header 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.cpp
的Source Files/FileIO
过滤器中创建新的源文件,并添加以下代码:
/*********************************
******THIS IS AN INTERFACE********
*********************************/
同样,这只是一个占位符文件,所有功能都在继承自GameObjectSharer
的任何类中;在这种情况下,LevelManager
类。
LevelManager
类是我们在 第 19 章游戏编程设计模式–启动太空入侵者++ 游戏中编码的内容与我们在本章中编码的所有内容之间的联系。ScreenManager
类将拥有一个LevelManager
类的实例,LevelManager
类将发起加载级别(使用我们刚刚编码的所有类)并与任何需要它们的类共享GameObject
实例。
在名为LevelManager.h
的Header 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.cpp
的Source 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
类,我们可以更新它实现的ScreenManager
和ScreenManagerRemoteControl
类。
打开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
实例以及两个函数的注释。
ScreenManager
和ScreenManagerRemoteControl
类现在功能齐全,getGameObjects
和shareGameObjectSharer
功能可以被任何引用了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
实例的系统。工厂模式用于许多类型的编程,不仅仅是游戏开发。我们使用的实现是最简单的实现,我鼓励您将工厂模式放在您的模式列表中,以便进一步研究和开发。然而,如果您希望构建一些深度且有趣的游戏,我们使用的实现应该可以很好地为您服务。
在下一章中,我们将通过添加碰撞检测、子弹产卵和游戏本身的逻辑,最终使游戏变得栩栩如生。