Skip to content

Latest commit

 

History

History
1632 lines (1398 loc) · 68.5 KB

File metadata and controls

1632 lines (1398 loc) · 68.5 KB

二十、游戏对象和组件

在这一章中,我们将进行与上一章开始时讨论的实体-组件模式相关的所有编码。这意味着我们将对基础Component类进行编码,所有其他组件都将从该类中派生出来。我们也将很好地利用我们关于智能指针的新知识,这样我们就不必关心我们为这些组件分配的内存。我们也将在本章中对GameObject类进行编码。

我们将在本章中讨论以下主题:

  • 准备对组件进行编码
  • 组件基类的编码
  • 对对撞机组件进行编码
  • 编码图形组件
  • 编码更新组件
  • 编码游戏对象类

在开始编码之前,让我们进一步讨论这些组件。请注意,在本章中,我将尝试并强调实体-组件系统是如何结合在一起的,以及所有组件是如何组成一个游戏对象的。我不会解释我们已经看过很多次的每一行甚至每一块逻辑或与 SFML 相关的代码。由你来研究这些细节。

准备对组件进行编码

当你完成这一章时,会有很多错误,其中一些看起来不符合逻辑。例如,当一个类是您已经编码的类之一时,您会得到错误,说它不存在。这样做的原因是,当一个类中有错误时,其他类也不能可靠地使用它而不出错。正是由于所有类的互连性,我们直到下一章接近尾声时才会摆脱所有的错误并再次拥有可执行代码。将代码以更小的块添加到不同的类中是可能的,并且项目会更频繁地没有错误。然而,逐渐做一些事情意味着不断地进出课堂。当你在构建自己的项目时,这有时是一个很好的方法,但是我认为为这个项目做的最有启发性的事情是帮助你尽快完成它。

对组件基类进行编码

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

#pragma once
#include "GameObjectSharer.h"
#include <string>
using namespace std;
class GameObject;
class Component {
public:
    virtual string getType() = 0;
    virtual string getSpecificType() = 0;
    virtual void disableComponent() = 0;
    virtual void enableComponent() = 0;
    virtual bool enabled() = 0;
    virtual void start(GameObjectSharer* gos, GameObject* self) = 0;
};

这是每个游戏对象中每个组件的基类。纯虚函数意味着一个组件永远不能被实例化,并且必须总是从第一个继承。函数允许访问组件的类型和特定类型。组件类型包括碰撞器、图形、转换和更新,但是根据游戏的要求可以添加更多的类型。具体类型包括标准图形、入侵者更新、玩家更新等等。

有两种功能允许启用和禁用组件。这很有用,因为在使用组件之前,可以测试该组件当前是否已启用。例如,您可以调用enabled函数来测试组件的更新组件在调用其update函数之前是否已启用,或者测试图形组件在调用其draw函数之前是否已启用。

start函数可能是最有趣的函数,因为它有一个新的类类型作为参数之一。GameObjectSharer类将在所有游戏对象用其所有组件实例化后,授予对所有游戏对象的访问权限。这将使每个游戏对象中的每个组件都有机会查询细节,甚至获得指向另一个游戏对象中特定数据的指针。举个例子,所有入侵者的更新组件都需要知道玩家转换组件的位置,这样它就知道什么时候发射子弹。绝对可以在start功能中访问任何对象的任何部分。关键是每个特定的组件将决定他们需要什么,并且在关键的游戏循环期间不需要开始查询另一个游戏对象的细节。

包含该组件的GameObject也被传递给start函数,这样任何组件都可以找到更多关于自身的信息。例如,图形组件需要了解变换组件,以便它知道在哪里绘制自己。作为第二个例子,入侵者和玩家飞船的更新组件将需要一个指向他们自己的碰撞器组件的指针,这样他们就可以在移动时更新它的位置。

随着我们的进展,我们将看到更多start函数的用例。

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

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

由于Component类永远无法实例化,所以我在Component.cpp中放了前面的注释作为提醒。

对对撞机组件进行编码

太空入侵者++ 游戏将只有一个简单类型的碰撞器。它将是一个围绕物体的矩形框,就像我们在僵尸启示录和乒乓游戏中看到的那样。然而,很容易想象你可能需要其他类型的对撞机;也许是一个圆形的对撞机,或者是一个不包容的对撞机,比如我们在《托马斯迟到了》游戏中用于托马斯和鲍勃头部、脚部和侧面的对撞机。

为此,将有一个基础ColliderComponent类(继承自Component)来处理所有碰撞器的基本功能,还有RectColliderComponent,它将添加一个包罗万象的矩形碰撞器的特定功能。新的碰撞器类型可以根据游戏开发的需要添加。

接下来是具体对撞机的基类,ColliderComponent

编码碰撞组件类

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

#pragma once
#include "Component.h"
#include <iostream>
class ColliderComponent : public Component
{
private:
    string m_Type = "collider";
    bool m_Enabled = false;
public:
    /****************************************************
    *****************************************************
    From Component interface
    *****************************************************
    *****************************************************/
    string Component::getType() {
        return m_Type;
    }
    void Component::disableComponent() {
        m_Enabled = false;
    }
    void Component::enableComponent() {
        m_Enabled = true;
    }
    bool Component::enabled() {
        return m_Enabled;
    }
   void Component::start(GameObjectSharer* gos, GameObject* self)
   {

    }
};

ColliderComponent类继承自Component类。在前面的代码中,可以看到m_Type成员变量被初始化为"collider",而m_Enabled被初始化为false

public部分,代码覆盖了Component类的纯虚函数。研究它们以熟悉它们,因为它们在所有组件类中都以非常相似的方式工作。getType功能返回m_TypedisableComponent功能将m_Enabled设置为falseenableComponent功能将m_Enabled设置为trueenabled功能返回m_Enabled的值。start函数没有代码,但将被许多更具体的基于组件的类覆盖。

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

/*
All Functionality in ColliderComponent.h
*/

我在ColliderComponent.cpp中添加了前面的注释,提醒自己所有的功能都在头文件中。

编码矩形碰撞组件类

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

#pragma once
#include "ColliderComponent.h"
#include <SFML/Graphics.hpp>
using namespace sf;
class RectColliderComponent : public ColliderComponent
{
private:
    string m_SpecificType = "rect";
    FloatRect m_Collider;
    string m_Tag = "";
public:
    RectColliderComponent(string name);
    string getColliderTag();
    void setOrMoveCollider(
        float x, float y, float width, float height);

    FloatRect& getColliderRectF();
    /****************************************************
    *****************************************************
    From Component interface base class
    *****************************************************
    *****************************************************/
    string getSpecificType() {
        return m_SpecificType;
    }

    void Component::start(
        GameObjectSharer* gos, GameObject* self) {}
};

RectColliderComponent类继承自ColliderComponent类。它有一个初始化为"rect"m_SpecificType变量。现在可以查询通用Component实例向量中的任何RectColliderComponent实例,并确定其具有类型"collider"和特定类型"rect"。由于Component类的纯虚函数,所有基于组件的类都将具有该功能。

还有一个名为m_ColliderFloatRect实例将存储这个对撞机的坐标。

public部分,我们可以查看构造函数。请注意,它收到一个string。传入的值将是标识此RectColliderComponent所附加的游戏对象类型的文本,例如入侵者、子弹或玩家的船。这样就有可能确定什么类型的物体相互碰撞。

在被重写的函数之前还有三个函数;记下它们的名称和参数,然后我们将在编码它们的定义时讨论它们。

注意getSpecificType函数定义返回m_SpecificType

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

#include "RectColliderComponent.h"
RectColliderComponent::RectColliderComponent(string name) {
    m_Tag = "" + name;
}
string RectColliderComponent::getColliderTag() {
    return m_Tag;
}
void RectColliderComponent::setOrMoveCollider(
    float x, float y, float width, float height) {

    m_Collider.left = x;
    m_Collider.top = y;
    m_Collider.width = width;
    m_Collider.height = height;
}
FloatRect& RectColliderComponent::getColliderRectF() {
    return m_Collider;
}

在构造函数中,传入的string值被赋给m_Tag变量,getColliderTag函数通过类的实例使该值可用。

setOrMoveCollider函数将m_Collider定位在作为参数传入的坐标上。

getColliderRectF功能返回对m_Collider的引用。这是使用FloatRect类的intersects功能与另一台对撞机进行碰撞测试的理想选择。

我们的对撞机现在已经完成,我们可以继续处理图形了。

对图形组件进行编码

太空入侵者++ 游戏将只有一种特定类型的图形组件。叫做StandardGraphicsComponent。与碰撞器组件一样,如果我们愿意,我们将实现一个基本的GraphicsComponent类,以便于添加其他图形相关的组件。比如经典的街机版《太空入侵者》,入侵者用两帧动画上下拍打手臂。一旦你看到了StandardGraphicsComponent是如何工作的,你将能够很容易地为另一个类(也许是AnimatedGraphicsComponent)编码,这个类每半秒钟左右就用一个不同的Sprite实例来绘制自己。你也可以有一个图形组件,它有一个着色器(也许是ShaderGraphicsComponent)来实现快速和酷的效果。除了这些,还有更多的可能性。

对图形组件类进行编码

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

#pragma once
#include "Component.h"
#include "TransformComponent.h"
#include <string>
#include <SFML/Graphics.hpp>
#include "GameObjectSharer.h"
#include <iostream>
using namespace sf;
using namespace std;
class GraphicsComponent : public Component {
private:
    string m_Type = "graphics";
    bool m_Enabled = false;
public:
    virtual void draw(
        RenderWindow& window,
        shared_ptr<TransformComponent> t) = 0;
    virtual void initializeGraphics(
        string bitmapName,
        Vector2f objectSize) = 0;
    /****************************************************
    *****************************************************
    From Component interface
    *****************************************************
    *****************************************************/
    string Component::getType() {
        return m_Type;
    }
    void Component::disableComponent() {
        m_Enabled = false;
    }
    void Component::enableComponent() {
        m_Enabled = true;
    }
    bool Component::enabled() {
        return m_Enabled;
    }
    void Component::start(
        GameObjectSharer* gos, GameObject* self) {}
};

前面的大部分代码实现了Component类的纯虚函数。GraphicsComponent类的新功能是draw函数,它有两个参数。第一个参数是对RenderWindow实例的引用,以便组件可以自己绘制,而第二个参数是指向GameObjectTransformComponent实例的共享智能指针,以便在游戏的每一帧都可以访问位置和比例等重要数据。

GraphicsComponent类还有一个新功能就是initializeGraphics函数,它也有两个参数。第一个是代表要使用的图形文件的文件名的string值,而第二个是代表游戏世界中对象大小的Vector2f实例。

前面两个函数都是纯虚函数,使得GraphicsComponent类抽象。任何继承自GraphicsComponent的类都需要实现这些功能。在下一节中,我们将看到StandardGraphicsComponent是如何做到的。

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

/*
All Functionality in GraphicsComponent.h
*/

前面的注释提醒我们代码都在相关的头文件中。

对标准图形组件类进行编码

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

#pragma once
#include "Component.h"
#include "GraphicsComponent.h"
#include <string>
class Component;
class StandardGraphicsComponent : public GraphicsComponent {
private:
    sf::Sprite m_Sprite;
    string m_SpecificType = "standard";
public:
    /****************************************************
    *****************************************************
    From Component interface base class
    *****************************************************
    *****************************************************/
    string Component::getSpecificType() {
        return m_SpecificType;
    }

    void Component::start(
        GameObjectSharer* gos, GameObject* self) {
    }
    /****************************************************
    *****************************************************
    From GraphicsComponent
    *****************************************************
    *****************************************************/
    void draw(
        RenderWindow& window,
        shared_ptr<TransformComponent> t) override;
    void initializeGraphics(
        string bitmapName,
        Vector2f objectSize) override;
};

StandardGraphicsComponent类有一个Sprite成员。它不需要一个Texture实例,因为这将从BitmapStore类的每一帧中获得。该类还覆盖了来自ComponentGraphicsComponent类的所需功能。

让我们为两个纯虚函数drawinitializeGraphics的实现编码。

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

#include "StandardGraphicsComponent.h"
#include "BitmapStore.h"
#include <iostream>
void StandardGraphicsComponent::initializeGraphics(
    string bitmapName,
    Vector2f objectSize)
{
    BitmapStore::addBitmap("graphics/" + bitmapName + ".png");
    m_Sprite.setTexture(BitmapStore::getBitmap(
        "graphics/" + bitmapName + ".png"));
    auto textureSize = m_Sprite.getTexture()->getSize();
    m_Sprite.setScale(float(objectSize.x) / textureSize.x, 
        float(objectSize.y) / textureSize.y);    
    m_Sprite.setColor(sf::Color(0, 255, 0)); 
}
void StandardGraphicsComponent::draw(
    RenderWindow& window,
    shared_ptr<TransformComponent> t)
{
    m_Sprite.setPosition(t->getLocation());
    window.draw(m_Sprite);
}

initializeGraphics函数中,调用BitmapStore类的addBitmap函数,传入图像的文件路径,以及游戏世界中对象的大小。

接下来,检索刚刚添加到BitmapStore类的Texture实例,并将其设置为Sprite的图像。接下来,两个函数getTexturegetSize被链接在一起以获得纹理的大小。

下一行代码使用setScale函数使Sprite与纹理大小相同,而纹理又被设置为游戏世界中该对象的大小。

setColor功能然后将绿色应用于Sprite。这给了它更多一点复古的感觉。

draw功能中,Sprite使用setPositionTransformComponentgetLocation功能移动到位。接下来我们将对TransformComponent类进行编码。

最后一行代码将Sprite绘制到RenderWindow

对转换组件类进行编码

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

#pragma once
#include "Component.h"
#include<SFML/Graphics.hpp>
using namespace sf;
class Component;
class TransformComponent : public Component {
private:
    const string m_Type = "transform";
    Vector2f m_Location;
    float m_Height;
    float m_Width;
public:
    TransformComponent(
        float width, float height, Vector2f location);
    Vector2f& getLocation();
    Vector2f getSize();
    /****************************************************
    *****************************************************
    From Component interface
    *****************************************************
    *****************************************************/
    string Component::getType()
    {
        return m_Type;
    }
    string Component::getSpecificType()
    {
        // Only one type of Transform so just return m_Type
        return m_Type;
    }
    void Component::disableComponent(){}
    void Component::enableComponent(){}
    bool Component::enabled()
    {
        return false;
    }
    void Component::start(GameObjectSharer* gos, GameObject* self)    {}
};

这个类有一个Vector2f存储对象在游戏世界中的位置,一个float存储高度,还有一个float存储宽度。

public部分,有一个构造函数,我们将用来设置这个类的实例,还有两个函数,getLocationgetSize,我们将用来共享对象的位置和大小。我们在编写StandardGraphicsComponent类时已经使用了这些函数。

TransformComponent.h文件中剩余的代码是Component类的实现。

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

#include "TransformComponent.h"
TransformComponent::TransformComponent(
    float width, float height, Vector2f location)
{
    m_Height = height;
    m_Width = width;
    m_Location = location;
}
Vector2f& TransformComponent::getLocation() 
{
    return m_Location;
}
Vector2f TransformComponent::getSize() 
{
    return Vector2f(m_Width, m_Height);
}

实现这个类的三个功能很简单。构造函数接收大小和位置,并初始化适当的成员变量。当请求时,getLocationgetSize功能返回该数据。请注意,这些值是通过引用返回的,因此可以通过调用代码进行修改。

接下来,我们将对所有与更新相关的组件进行编码。

编码更新组件

正如您现在可能期望的那样,我们将编写一个从Component类继承的UpdateComponent类。它将拥有每个UpdateComponent需要的所有功能,然后我们将编码从UpdateComponent派生的类。这些将包含特定于游戏中各个对象的功能。对于这场比赛,我们将有BulletUpdateComponentInvaderUpdateComponentPlayerUpdateComponent。当你在自己的项目中工作,并且你想要游戏中的一个对象以一种特定的独特方式表现时,只要为它编写一个新的基于更新的组件,你就可以开始了。基于更新的组件定义行为。

对更新组件类进行编码

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

#pragma once
#include "Component.h"
class UpdateComponent : public Component
{
private:
    string m_Type = "update";
    bool m_Enabled = false;
public:
    virtual void update(float fps) = 0;

    /****************************************************
    *****************************************************
    From Component interface
    *****************************************************
    *****************************************************/
    string Component::getType() {
        return m_Type;
    }
    void Component::disableComponent() {
        m_Enabled = false;
    }
    void Component::enableComponent() {
        m_Enabled = true;
    }
    bool Component::enabled() {
        return m_Enabled;
    }
    void Component::start(
        GameObjectSharer* gos, GameObject* self) {
    }
};

UpdateComponent只带来一个功能:update功能。这个函数是纯虚拟的,所以它必须由任何渴望成为UpdateComponent的可用实例的类来实现。

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

/*
All Functionality in UpdateComponent.h
*/

这是一个有用的注释,提醒我们这个类的所有代码都在相关的头文件中。

对 BulletUpdateComponent 类进行编码

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

#pragma once
#include "UpdateComponent.h"
#include "TransformComponent.h"
#include "GameObjectSharer.h"
#include "RectColliderComponent.h"
#include "GameObject.h"
class BulletUpdateComponent : public UpdateComponent
{
private:
    string m_SpecificType = "bullet";
    shared_ptr<TransformComponent> m_TC;
    shared_ptr<RectColliderComponent> m_RCC;
    float m_Speed = 75.0f;

    int m_AlienBulletSpeedModifier;
    int m_ModifierRandomComponent = 5;
    int m_MinimumAdditionalModifier = 5;
    bool m_MovingUp = true;
public:
    bool m_BelongsToPlayer = false;
    bool m_IsSpawned = false;
    void spawnForPlayer(Vector2f spawnPosition);
    void spawnForInvader(Vector2f spawnPosition);
    void deSpawn();
    bool isMovingUp();
    /****************************************************
    *****************************************************
    From Component interface base class
    *****************************************************
    *****************************************************/
    string Component::getSpecificType() {
        return m_SpecificType;
    }

    void Component::start(
        GameObjectSharer* gos, GameObject* self) {        
        // Where is this specific invader
        m_TC = static_pointer_cast<TransformComponent>(
            self->getComponentByTypeAndSpecificType(
                "transform", "transform"));
        m_RCC = static_pointer_cast<RectColliderComponent>(
            self->getComponentByTypeAndSpecificType(
                "collider", "rect"));
    }
    /****************************************************
    *****************************************************
    From UpdateComponent
    *****************************************************
    *****************************************************/
    void update(float fps) override;
};

如果你想理解子弹的行为/逻辑,你需要花一些时间学习成员变量的名称和类型,因为我不会精确地解释子弹的行为;这些话题我们已经讨论过很多次了。然而,我要指出的是,有一些变量可以覆盖基本的东西,比如移动,有助于在一定范围内随机化每颗子弹速度的变量,以及识别子弹是属于玩家还是入侵者的布尔变量。

你还不知道但必须在这里学习的关键是,每个BulletUpdateComponent实例将持有一个指向所属游戏对象的TransformComponent实例的共享指针和一个指向所属游戏对象的RectColliderComponent实例的共享指针。

现在,仔细观察被覆盖的start函数。在start函数中,上述共享指针被初始化。代码通过使用所属游戏对象(self)的getComponentByTypeAndSpecificType功能来实现这一点,该功能是指向所属游戏对象的指针。我们将在后面的章节中对GameObject类进行编码,包括这个函数。

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

#include "BulletUpdateComponent.h"
#include "WorldState.h"
void BulletUpdateComponent::spawnForPlayer(
    Vector2f spawnPosition)
{
    m_MovingUp = true;
    m_BelongsToPlayer = true;
    m_IsSpawned = true;

    m_TC->getLocation().x = spawnPosition.x;
    // Tweak the y location based on the height of the bullet 
    // The x location is already tweaked to the center of the player
    m_TC->getLocation().y = spawnPosition.y - m_TC->getSize().y;
    // Update the collider
    m_RCC->setOrMoveCollider(m_TC->getLocation().x,
        m_TC->getLocation().y, 
        m_TC->getSize().x, m_TC->getSize().y);
}
void BulletUpdateComponent::spawnForInvader(
    Vector2f spawnPosition)
{
    m_MovingUp = false;
    m_BelongsToPlayer = false;
    m_IsSpawned = true;
    srand((int)time(0));
    m_AlienBulletSpeedModifier = (
        ((rand() % m_ModifierRandomComponent)))  
        + m_MinimumAdditionalModifier;    
    m_TC->getLocation().x = spawnPosition.x;
    // Tweak the y location based on the height of the bullet 
    // The x location already tweaked to the center of the invader
    m_TC->getLocation().y = spawnPosition.y;
    // Update the collider
    m_RCC->setOrMoveCollider(
        m_TC->getLocation().x, m_TC->
        getLocation().y, m_TC->getSize().x, m_TC->getSize().y);
}
void BulletUpdateComponent::deSpawn()
{
    m_IsSpawned = false;
}
bool BulletUpdateComponent::isMovingUp()
{
    return m_MovingUp;
}
void BulletUpdateComponent::update(float fps)
{
    if (m_IsSpawned)
    {    
        if (m_MovingUp)
        {
            m_TC->getLocation().y -= m_Speed * fps;
        }
        else
        {
            m_TC->getLocation().y += m_Speed / 
                m_AlienBulletSpeedModifier * fps;
        }
        if (m_TC->getLocation().y > WorldState::WORLD_HEIGHT 
            || m_TC->getLocation().y < -2)
        {
            deSpawn();
        }
        // Update the collider
        m_RCC->setOrMoveCollider(m_TC->getLocation().x, 
            m_TC->getLocation().y, 
            m_TC->getSize().x, m_TC->getSize().y);
    }
}

前两个功能是BulletUpdateComponent类独有的;他们是spawnForPlayerspawnForInvader。这两个函数都为成员变量、转换组件和碰撞器组件准备动作。每个人的方式都略有不同。例如,对于玩家拥有的子弹,它准备从玩家飞船的顶部向上移动屏幕,而子弹准备让入侵者从入侵者的底部向下移动屏幕。需要注意的关键是,所有这些都可以通过指向转换组件和碰撞器组件的共享指针来实现。此外,请注意m_IsSpawned布尔设置为真,使该更新组件的update功能准备好调用游戏的每一帧。

update功能中,子弹以适当的速度在屏幕上上下移动。它被测试看它是否已经从屏幕的顶部或底部消失,碰撞器被更新以环绕当前位置,这样我们就可以测试碰撞。

这是我们在这本书里看到的同样的逻辑;新的是我们用来与组成这个游戏对象的其他组件进行通信的共享指针。

子弹只需要产生并测试碰撞;我们将在接下来的两章中看到如何做到这一点。现在,我们将对入侵者的行为进行编码。

对入侵日期组件类进行编码

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

#pragma once
#include "UpdateComponent.h"
#include "TransformComponent.h"
#include "GameObjectSharer.h"
#include "RectColliderComponent.h"
#include "GameObject.h"
class BulletSpawner;
class InvaderUpdateComponent : public UpdateComponent
{
private:
    string m_SpecificType = "invader";
    shared_ptr<TransformComponent> m_TC;
    shared_ptr < RectColliderComponent> m_RCC;
    shared_ptr < TransformComponent> m_PlayerTC;
    shared_ptr < RectColliderComponent> m_PlayerRCC;
    BulletSpawner* m_BulletSpawner;
    float m_Speed = 10.0f;
    bool m_MovingRight = true;
    float m_TimeSinceLastShot;
    float m_TimeBetweenShots = 5.0f;
    float m_AccuracyModifier;
    float m_SpeedModifier = 0.05;
    int m_RandSeed;
public:
    void dropDownAndReverse();
    bool isMovingRight();
    void initializeBulletSpawner(BulletSpawner* 
        bulletSpawner, int randSeed);
    /****************************************************
    *****************************************************
    From Component interface base class
    *****************************************************
    *****************************************************/
    string Component::getSpecificType() {
        return m_SpecificType;
    }
    void Component::start(GameObjectSharer* gos, 
        GameObject* self) {

        // Where is the player?
        m_PlayerTC = static_pointer_cast<TransformComponent>(
            gos->findFirstObjectWithTag("Player")
            .getComponentByTypeAndSpecificType(
                "transform", "transform"));
        m_PlayerRCC = static_pointer_cast<RectColliderComponent>(
            gos->findFirstObjectWithTag("Player")
            .getComponentByTypeAndSpecificType(
                "collider", "rect"));
        // Where is this specific invader
        m_TC = static_pointer_cast<TransformComponent>(
            self->getComponentByTypeAndSpecificType(
                "transform", "transform"));
        m_RCC = static_pointer_cast<RectColliderComponent>(
            self->getComponentByTypeAndSpecificType(
                "collider", "rect"));
    }
    /****************************************************
    *****************************************************
    From UpdateComponent
    *****************************************************
    *****************************************************/
    void update(float fps) override;    
};

在类声明中,我们可以看到编码入侵者行为所需的所有特性。有一个指向转换组件的指针,以便入侵者可以移动,还有一个指向碰撞器组件的指针,以便它可以更新其位置并被碰撞:

shared_ptr<TransformComponent> m_TC;
shared_ptr < RectColliderComponent> m_RCC;

有指向玩家变换和碰撞器的指针,这样入侵者就可以查询玩家的位置,并决定何时发射子弹:

shared_ptr < TransformComponent> m_PlayerTC;
shared_ptr < RectColliderComponent> m_PlayerRCC;

接下来,有一个BulletSpawner实例,我们将在下一章进行编码。BulletSpawner职业将允许入侵者或玩家产生子弹。

接下来是一大堆变量,我们将使用它们来控制速度、方向、射速、入侵者瞄准的精度以及发射子弹的速度。熟悉它们,因为它们将用于函数定义中相当深入的逻辑中:

float m_Speed = 10.0f;
bool m_MovingRight = true;
float m_TimeSinceLastShot;
float m_TimeBetweenShots = 5.0f;
float m_AccuracyModifier;
float m_SpeedModifier = 0.05;
int m_RandSeed;

接下来,我们可以看到三个新的公共函数,系统的不同部分可以调用它们来使入侵者向下移动一点并向另一个方向前进,测试行进方向,并分别向前面提到的BulletSpawner类传递一个指针:

void dropDownAndReverse();
bool isMovingRight();
void initializeBulletSpawner(BulletSpawner* 
        bulletSpawner, int randSeed);

一定要学习start函数,在那里初始化了入侵者和玩家的智能指针。现在,我们将对函数定义进行编码。

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

#include "InvaderUpdateComponent.h"
#include "BulletSpawner.h"
#include "WorldState.h"
#include "SoundEngine.h"
void InvaderUpdateComponent::update(float fps)
{
    if (m_MovingRight)
    {
        m_TC->getLocation().x += m_Speed * fps;
    }
    else
    {
        m_TC->getLocation().x -= m_Speed * fps;
    }
    // Update the collider
    m_RCC->setOrMoveCollider(m_TC->getLocation().x, 
        m_TC->getLocation().y, m_TC->getSize().x, m_TC-
      >getSize().y);
    m_TimeSinceLastShot += fps;

    // Is the middle of the invader above the 
   // player +- 1 world units
    if ((m_TC->getLocation().x + (m_TC->getSize().x / 2)) > 
        (m_PlayerTC->getLocation().x - m_AccuracyModifier) &&
        (m_TC->getLocation().x + (m_TC->getSize().x / 2)) < 
        (m_PlayerTC->getLocation().x + 
        (m_PlayerTC->getSize().x + m_AccuracyModifier)))
    {
        // Has the invader waited long enough since the last shot
        if (m_TimeSinceLastShot > m_TimeBetweenShots)
        {
            SoundEngine::playShoot();
            Vector2f spawnLocation;
            spawnLocation.x = m_TC->getLocation().x + 
                m_TC->getSize().x / 2;
            spawnLocation.y = m_TC->getLocation().y + 
                m_TC->getSize().y;
            m_BulletSpawner->spawnBullet(spawnLocation, false);
            srand(m_RandSeed);
            int mTimeBetweenShots = (((rand() % 10))+1) / 
                WorldState::WAVE_NUMBER;
            m_TimeSinceLastShot = 0;            
        }
    }
}
void InvaderUpdateComponent::dropDownAndReverse()
{
    m_MovingRight = !m_MovingRight;
    m_TC->getLocation().y += m_TC->getSize().y;
    m_Speed += (WorldState::WAVE_NUMBER) + 
        (WorldState::NUM_INVADERS_AT_START 
       - WorldState::NUM_INVADERS) 
        * m_SpeedModifier;
}
bool InvaderUpdateComponent::isMovingRight()
{
    return m_MovingRight;
}
void InvaderUpdateComponent::initializeBulletSpawner(
    BulletSpawner* bulletSpawner, int randSeed)
{
    m_BulletSpawner = bulletSpawner;
    m_RandSeed = randSeed;
    srand(m_RandSeed);
    m_TimeBetweenShots = (rand() % 15 + m_RandSeed);
    m_AccuracyModifier = (rand() % 2);
    m_AccuracyModifier += 0 + static_cast <float> (
        rand()) / (static_cast <float> (RAND_MAX / (10)));
}

代码太多了。实际上,里面没有我们以前没有见过的 C++ 代码。控制入侵者的行为完全是逻辑。让我们概述一下它的全部功能,为了方便起见,部分代码被重新打印。

解释更新功能

第一个ifelse块根据情况将入侵者向右或向左移动每一帧:

void InvaderUpdateComponent::update(float fps)
{
    if (m_MovingRight)
    {
        m_TC->getLocation().x += m_Speed * fps;
    }
    else
    {
        m_TC->getLocation().x -= m_Speed * fps;
    }

接下来,碰撞器被更新到新位置:

    // Update the collider
    m_RCC->setOrMoveCollider(m_TC->getLocation().x, 
        m_TC->getLocation().y, m_TC->getSize().x, m_TC 
      ->getSize().y);

这段代码跟踪这个入侵者最后一次开枪已经有多久了,然后测试玩家是在入侵者的左边还是右边一个世界单位(+或者–对于随机精度修改器,所以每个入侵者都有点不同):

   m_TimeSinceLastShot += fps;

    // Is the middle of the invader above the 
   // player +- 1 world units
    if ((m_TC->getLocation().x + (m_TC->getSize().x / 2)) > 
        (m_PlayerTC->getLocation().x - m_AccuracyModifier) &&
        (m_TC->getLocation().x + (m_TC->getSize().x / 2)) < 
        (m_PlayerTC->getLocation().x + 
        (m_PlayerTC->getSize().x + m_AccuracyModifier)))
    {

在前面的if测试中,另一个测试确保入侵者从最后一枪开始已经等了足够长的时间。如果有,那就拍一张。播放声音,计算子弹的产卵位置,调用BulletSpawner实例的spawnBullet函数,并计算新的随机等待时间,然后可以拍摄另一个镜头:

        // Has the invader waited long enough since the last shot
        if (m_TimeSinceLastShot > m_TimeBetweenShots)
        {
            SoundEngine::playShoot();
            Vector2f spawnLocation;
            spawnLocation.x = m_TC->getLocation().x + 
                m_TC->getSize().x / 2;
            spawnLocation.y = m_TC->getLocation().y + 
                m_TC->getSize().y;
            m_BulletSpawner->spawnBullet(spawnLocation, false);
            srand(m_RandSeed);
            int mTimeBetweenShots = (((rand() % 10))+1) / 
                WorldState::WAVE_NUMBER;
            m_TimeSinceLastShot = 0;            
        }
    }
}

BulletSpawner类的细节将在下一章透露,但作为对未来的一瞥,它将是一个抽象类,有一个名为spawnBullet的函数,并将被GameScreen类继承。

解释 dropDownAndReverse 函数

dropDownAndReverse功能中,方向反转,垂直位置增加入侵者的高度。此外,入侵者的速度相对于玩家清除了多少波以及还有多少入侵者有待消灭而言会有所增加。清除的波浪越多,剩下的入侵者越少,入侵者移动的速度就越快:

void InvaderUpdateComponent::dropDownAndReverse()
{
    m_MovingRight = !m_MovingRight;
    m_TC->getLocation().y += m_TC->getSize().y;
    m_Speed += (WorldState::WAVE_NUMBER) + 
        (WorldState::NUM_INVADERS_AT_START 
      - WorldState::NUM_INVADERS) 
        * m_SpeedModifier;
}

下一个函数很简单,但为了完整起见,包含了它。

解释 isMovingRight 函数

该代码只是提供了对当前行驶方向的访问:

bool InvaderUpdateComponent::isMovingRight()
{
    return m_MovingRight;
}

它将用于测试是在屏幕左侧(向左移动时)还是在屏幕右侧(向右移动时)寻找碰撞,并允许碰撞触发对dropDownAndReverse功能的调用。

解释 initializeBulletSpawner 函数

我已经提到了BulletSpawner类是抽象的,将由GameScreen类实现。当GameScreen类的initialize函数被调用时,这个initializeBulletSpawner函数将被每个入侵者调用。如您所见,第一个参数是指向BulletSpawner实例的指针。这赋予了每个InvaderUpdateComponent调用spawnBullet函数的能力:

void InvaderUpdateComponent::initializeBulletSpawner(
    BulletSpawner* bulletSpawner, int randSeed)
{
    m_BulletSpawner = bulletSpawner;
    m_RandSeed = randSeed;
    srand(m_RandSeed);
    m_TimeBetweenShots = (rand() % 15 + m_RandSeed);
    m_AccuracyModifier = (rand() % 2);
    m_AccuracyModifier += 0 + static_cast <float> (
        rand()) / (static_cast <float> (RAND_MAX / (10)));
}

initializeBulletSpawner函数中的其余代码设置随机值,使每个入侵者的行为与其他入侵者略有不同。

对 PlayerUpdateComponent 类进行编码

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

#pragma once
#include "UpdateComponent.h"
#include "TransformComponent.h"
#include "GameObjectSharer.h"
#include "RectColliderComponent.h"
#include "GameObject.h"
class PlayerUpdateComponent : public UpdateComponent
{
private:
    string m_SpecificType = "player";
    shared_ptr<TransformComponent> m_TC;
    shared_ptr<RectColliderComponent> m_RCC;
    float m_Speed = 50.0f;
    float m_XExtent = 0;
    float m_YExtent = 0;
    bool m_IsHoldingLeft = false;
    bool m_IsHoldingRight = false;
    bool m_IsHoldingUp = false;
    bool m_IsHoldingDown = false;
public:
    void updateShipTravelWithController(float x, float y);
    void moveLeft();
    void moveRight();
    void moveUp();
    void moveDown();
    void stopLeft();
    void stopRight();
    void stopUp();
    void stopDown();
    /****************************************************
    *****************************************************
    From Component interface base class
    *****************************************************
    *****************************************************/
    string Component::getSpecificType() {
        return m_SpecificType;
    }
    void Component::start(GameObjectSharer* gos, GameObject* self) {        
        m_TC = static_pointer_cast<TransformComponent>(self->
            getComponentByTypeAndSpecificType(
                "transform", "transform"));
        m_RCC = static_pointer_cast<RectColliderComponent>(self->
            getComponentByTypeAndSpecificType(
                "collider", "rect"));        
    }
    /****************************************************
    *****************************************************
    From UpdateComponent
    *****************************************************
    *****************************************************/
    void update(float fps) override;
};

PlayerUpdateComponent类中,我们有跟踪玩家是否按下键盘键所需的所有布尔变量,以及可以切换这些布尔值的函数。我们以前没有见过像m_XExtentM_YExtent float类型变量这样的东西,当我们在函数定义中看到它们的用法时,我们会解释它们。

注意,就像BulletUpdateComponentInvaderUpdateComponent类一样,我们共享了指向这个游戏对象的转换和碰撞器组件的指针。正如我们所料,这些共享指针是在start函数中初始化的。

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

#include "PlayerUpdateComponent.h"
#include "WorldState.h"
void PlayerUpdateComponent::update(float fps)
{
    if (sf::Joystick::isConnected(0))
    {
        m_TC->getLocation().x += ((m_Speed / 100) 
            * m_XExtent) * fps;
        m_TC->getLocation().y += ((m_Speed / 100) 
            * m_YExtent) * fps;        
    }
    // Left and right
    if (m_IsHoldingLeft)
    {
        m_TC->getLocation().x -= m_Speed * fps;
    }
    else if (m_IsHoldingRight)
    {
        m_TC->getLocation().x += m_Speed * fps;
    }
    // Up and down
    if (m_IsHoldingUp)
    {
        m_TC->getLocation().y -= m_Speed * fps;
    }
    else if (m_IsHoldingDown)
    {
        m_TC->getLocation().y += m_Speed * fps;
    }

    // Update the collider
    m_RCC->setOrMoveCollider(m_TC->getLocation().x, 
        m_TC->getLocation().y, m_TC->getSize().x, 
        m_TC->getSize().y);

    // Make sure the ship doesn't go outside the allowed area
    if (m_TC->getLocation().x >
        WorldState::WORLD_WIDTH - m_TC->getSize().x) 
    {
        m_TC->getLocation().x = 
            WorldState::WORLD_WIDTH - m_TC->getSize().x;
    }
    else if (m_TC->getLocation().x < 0)
    {
        m_TC->getLocation().x = 0;
    }
    if (m_TC->getLocation().y > 
        WorldState::WORLD_HEIGHT - m_TC->getSize().y)
    {
        m_TC->getLocation().y = 
            WorldState::WORLD_HEIGHT - m_TC->getSize().y;
    }
    else if (m_TC->getLocation().y < 
        WorldState::WORLD_HEIGHT / 2)
    {
        m_TC->getLocation().y = 
            WorldState::WORLD_HEIGHT / 2;
    }
}    
void PlayerUpdateComponent::
    updateShipTravelWithController(float x, float y)
{
    m_XExtent = x;
    m_YExtent = y;
}
void PlayerUpdateComponent::moveLeft()
{
    m_IsHoldingLeft = true;
    stopRight();
}
void PlayerUpdateComponent::moveRight()
{
    m_IsHoldingRight = true;
    stopLeft();
}
void PlayerUpdateComponent::moveUp()
{
    m_IsHoldingUp = true;
    stopDown();
}
void PlayerUpdateComponent::moveDown()
{
    m_IsHoldingDown = true;
    stopUp();
}
void PlayerUpdateComponent::stopLeft()
{
    m_IsHoldingLeft = false;
}
void PlayerUpdateComponent::stopRight()
{
    m_IsHoldingRight = false;
}
void PlayerUpdateComponent::stopUp()
{
    m_IsHoldingUp = false;
}
void PlayerUpdateComponent::stopDown()
{
    m_IsHoldingDown = false;
}

在更新功能的第一个if块中,条件是sf::Joystick::isConnected(0)。当玩家将游戏手柄插入 USB 端口时,这种情况会返回真。在if块内,变换组件的水平和垂直位置都被改变:

…((m_Speed / 100) * m_YExtent) * fps;

前面的代码将目标速度除以 100,然后乘以m_YExtentThe m_XExtentm_YExtent变量将在每一帧更新,以保存代表玩家在水平和垂直方向上移动游戏手柄拇指棒的程度的值。值的范围是从-100 到 100,因此前面的代码具有这样的效果:当指杆位于其全部范围中的任何一个时,以全速向任何方向移动变换组件,或者当指杆部分位于中心(完全不移动)和其全部范围之间时,以该速度的一部分移动变换组件。这意味着如果玩家选择使用游戏手柄而不是键盘,他们将更好地控制飞船的速度。

我们将在 第 22 章使用游戏对象和构建游戏中看到更多关于游戏手柄操作的细节。

update功能的其余部分响应布尔变量,代表玩家正在按下或已经释放的键盘按键。

在游戏手柄和键盘操作后,碰撞器组件被移动到新的位置,一系列if块确保玩家船不能移动到屏幕之外或屏幕上的中途点上方。

下一个功能是updateShipTravelWithController功能;当控制器被插入时,它将为每一帧更新拇指操纵杆移动或静止的程度。

其余功能更新布尔值,指示是否使用键盘键来移动船只。请注意,更新组件不处理发射子弹。我们本可以从这里处理它,一些游戏可能有一个很好的理由这样做。在这个游戏中,处理从GameInputHandler类射出子弹稍微直接一点。我们将在 第 22 章中看到的GameInputHandler类使用游戏对象并构建游戏将调用所有让PlayerUpdateComponent类知道游戏手柄和键盘发生了什么的功能。我们在前一章的GameInputHandler课程中对键盘响应的基础进行了编码。

现在,让我们编写GameObject类的代码,它将保存所有不同的组件实例。

编码游戏对象类

我将非常详细地介绍这个类中的代码,因为它是所有其他类如何工作的关键。但是,我认为您将受益于完整地查看代码并首先研究它。考虑到这一点,在名为GameObject.hHeader Files/GameObjects过滤器中创建新的头文件,并添加以下代码:

#pragma once
#include <SFML/Graphics.hpp>
#include <vector>
#include <string>
#include "Component.h"
#include "GraphicsComponent.h"
#include "GameObjectSharer.h"
#include "UpdateComponent.h"
class GameObject {
private:
    vector<shared_ptr<Component>> m_Components;
    string m_Tag;
    bool m_Active = false;
    int m_NumberUpdateComponents = 0;
    bool m_HasUpdateComponent = false;
    int m_FirstUpdateComponentLocation = -1;
    int m_GraphicsComponentLocation = -1;
    bool m_HasGraphicsComponent = false;
    int m_TransformComponentLocation = -1;
    int m_NumberRectColliderComponents = 0;
    int m_FirstRectColliderComponentLocation = -1;
    bool m_HasCollider = false;
public:
    void update(float fps);
    void draw(RenderWindow& window);
    void addComponent(shared_ptr<Component> component);
    void setActive();
    void setInactive();
    bool isActive();
    void setTag(String tag);
    string getTag();
    void start(GameObjectSharer* gos);
    // Slow only use in init and start
    shared_ptr<Component> getComponentByTypeAndSpecificType(
        string type, string specificType);
    FloatRect& getEncompassingRectCollider();
    bool hasCollider();
    bool hasUpdateComponent();
    string getEncompassingRectColliderTag();
    shared_ptr<GraphicsComponent> getGraphicsComponent();
    shared_ptr<TransformComponent> getTransformComponent();
    shared_ptr<UpdateComponent> getFirstUpdateComponent();
};

在前面的代码中,一定要仔细检查变量、类型、函数名及其参数。

在名为GameObject.cppSource Files/GameObjects过滤器中创建新的源文件,然后研究并添加以下代码:

#include "DevelopState.h"
#include "GameObject.h"
#include <iostream> 
#include "UpdateComponent.h"
#include "RectColliderComponent.h"
void GameObject::update(float fps)
{
    if (m_Active && m_HasUpdateComponent)
    {
        for (int i = m_FirstUpdateComponentLocation; i < 
            m_FirstUpdateComponentLocation + 
            m_NumberUpdateComponents; i++) 
        {
            shared_ptr<UpdateComponent> tempUpdate =
                static_pointer_cast<UpdateComponent>(
             m_Components[i]);
            if (tempUpdate->enabled()) 
            {
                tempUpdate->update(fps);
            }
        }
    }
}
void GameObject::draw(RenderWindow& window)
{
    if (m_Active && m_HasGraphicsComponent)
    {
        if (m_Components[m_GraphicsComponentLocation]->enabled())
        {
            getGraphicsComponent()->draw(window, 
                getTransformComponent());
        }
    }
}
shared_ptr<GraphicsComponent> GameObject::getGraphicsComponent() 
{
    return static_pointer_cast<GraphicsComponent>(
        m_Components[m_GraphicsComponentLocation]);
}
shared_ptr<TransformComponent> GameObject::getTransformComponent() 
{
    return static_pointer_cast<TransformComponent>(
        m_Components[m_TransformComponentLocation]);
}
void GameObject::addComponent(shared_ptr<Component> component)
{
    m_Components.push_back(component);
    component->enableComponent();

   if (component->getType() == "update") 
    {
        m_HasUpdateComponent = true;
        m_NumberUpdateComponents++ ;
        if (m_NumberUpdateComponents == 1) 
        {
            m_FirstUpdateComponentLocation = 
                m_Components.size() - 1;
        }
    }
    else if (component->getType() == "graphics") 
    {
        // No iteration in the draw method required
        m_HasGraphicsComponent = true;
        m_GraphicsComponentLocation = m_Components.size() - 1;
    }
    else if (component->getType() == "transform") 
    {
        // Remember where the Transform component is
        m_TransformComponentLocation = m_Components.size() - 1;
    }
    else if (component->getType() == "collider" && 
        component->getSpecificType() == "rect") 
    {
        // Remember where the collider component(s) is
        m_HasCollider = true;
        m_NumberRectColliderComponents++ ;
        if (m_NumberRectColliderComponents == 1) 
        {
            m_FirstRectColliderComponentLocation = 
                m_Components.size() - 1;
        }
    }    
}
void GameObject::setActive()
{
    m_Active = true;
}
void GameObject::setInactive()
{
    m_Active = false;
}
bool GameObject::isActive()
{
    return m_Active;
}
void GameObject::setTag(String tag)
{
    m_Tag = "" + tag;
}
std::string GameObject::getTag()
{
    return m_Tag;
}
void GameObject::start(GameObjectSharer* gos) 
{
    auto it = m_Components.begin();
    auto end = m_Components.end();
    for (it;
        it != end;
        ++ it)
    {
        (*it)->start(gos, this);
    }
}
// Slow - only use in start function
shared_ptr<Component> GameObject::
   getComponentByTypeAndSpecificType(
    string type, string specificType) {
    auto it = m_Components.begin();
    auto end = m_Components.end();
    for (it;
        it != end;
        ++ it)
    {
        if ((*it)->getType() == type)
        {
            if ((*it)->getSpecificType() == specificType)
            {
                return  (*it);
            }
        }
    }
    #ifdef debuggingErrors        
        cout << 
            "GameObject.cpp::getComponentByTypeAndSpecificType-" 
            << "COMPONENT NOT FOUND ERROR!" 
            << endl;
    #endif
        return m_Components[0];
}
FloatRect& GameObject::getEncompassingRectCollider() 
{
    if (m_HasCollider) 
    {
        return (static_pointer_cast<RectColliderComponent>(
            m_Components[m_FirstRectColliderComponentLocation]))
            ->getColliderRectF();
    }
}
string GameObject::getEncompassingRectColliderTag() 
{
    return static_pointer_cast<RectColliderComponent>(
        m_Components[m_FirstRectColliderComponentLocation])->
        getColliderTag();
}
shared_ptr<UpdateComponent> GameObject::getFirstUpdateComponent()
{
    return static_pointer_cast<UpdateComponent>(
        m_Components[m_FirstUpdateComponentLocation]);
}
bool GameObject::hasCollider() 
{
    return m_HasCollider;
}
bool GameObject::hasUpdateComponent()
{
    return m_HasUpdateComponent;
}

小费

在继续之前,一定要学习前面的代码。下面的解释假设您对变量名和类型以及函数名、参数和返回类型有基本的了解。

解释游戏对象类

让我们一次浏览GameObject类的一个函数,并重新打印代码,以便于讨论。

解释更新功能

对于每个游戏对象,游戏循环的每一帧都调用一次update功能。像我们大多数其他项目一样,当前的帧速率是必需的。在update功能中,进行一个测试来查看这个GameObject实例是否是活动的并且有一个更新组件。一个游戏对象不一定要有更新组件,尽管这个项目中的所有游戏对象都有。

接下来,update功能循环遍历它拥有的所有组件,从m_FirstUpdateComponent开始到m_FirstUpdateComponent + m_NumberUpdateComponents。这段代码意味着一个游戏对象可以有多个更新组件。这是为了让你可以设计具有行为层次的游戏对象。这种行为分层在 第 22 章使用游戏对象和构建游戏中有进一步的讨论。这个项目中所有的游戏对象都只有一个更新组件,所以你可以在update功能中简化(并加快)逻辑,但我建议你先看完 第 22 章使用游戏对象并构建游戏后再这样做。

因为一个组件可能是许多类型中的一种,所以我们创建一个临时的更新相关组件(tempUpdate),将组件从组件向量转换为UpdateComponent,并调用update函数。UpdateComponent类的具体推导没关系;它将实现update功能,因此UpdateComponent类型足够具体:

void GameObject::update(float fps)
{
    if (m_Active && m_HasUpdateComponent)
    {
        for (int i = m_FirstUpdateComponentLocation; i < 
            m_FirstUpdateComponentLocation + 
            m_NumberUpdateComponents; i++) 
        {
            shared_ptr<UpdateComponent> tempUpdate =
                static_pointer_cast<UpdateComponent>(
             m_Components[i]);
            if (tempUpdate->enabled()) 
            {
                tempUpdate->update(fps);
            }
        }
    }
}

当我们进入后面的addComponent函数时,我们将看到如何初始化各种控制变量,例如m_FirstUpdateComponentLocationm_NumberOfUpdateComponents

解释绘图功能

draw功能检查游戏对象是否活动,是否有图形组件。如果是,则检查图形组件是否已启用。如果所有这些测试成功,则调用draw功能:

void GameObject::draw(RenderWindow& window)
{
    if (m_Active && m_HasGraphicsComponent)
    {
        if (m_Components[m_GraphicsComponentLocation]->enabled())
        {
            getGraphicsComponent()->draw(window, 
                getTransformComponent());
        }
    }
}

draw功能的结构意味着不是每个游戏对象都要自己画。我在 第 19 章游戏编程设计模式–启动太空入侵者++ 游戏中提到,您可能希望永远看不到的游戏对象充当不可见的触发区域(没有图形组件),当玩家经过它们或暂时保持不可见的游戏对象(暂时禁用,但有图形组件)时,它们会做出响应。在这个项目中,所有游戏对象都有一个永久启用的图形组件。

解释 getGraphicsComponent 函数

此函数返回指向图形组件的共享指针:

shared_ptr<GraphicsComponent> GameObject::getGraphicsComponent() 
{
    return static_pointer_cast<GraphicsComponent>(
        m_Components[m_GraphicsComponentLocation]);
}

getGraphicsComponent函数让任何拥有包含的游戏对象实例的代码都可以访问图形组件。

解释 getTransformComponent 函数

此函数返回一个指向转换组件的共享指针:

shared_ptr<TransformComponent> GameObject::getTransformComponent() 
{
    return static_pointer_cast<TransformComponent>(
        m_Components[m_TransformComponentLocation]);
}

getTransformComponent函数让任何拥有包含的游戏对象实例的代码都可以访问转换组件。

解释添加组件功能

我们将在下一章中编码的工厂模式类将使用addComponent函数。该函数接收指向Component实例的共享指针。函数内部发生的第一件事是将Component实例添加到m_Components向量中。接下来,使用enabled功能启用组件。

接下来是一系列的ifelse if语句,处理每种可能的组件类型。当组件的类型被识别时,各种控制变量被初始化,以使类的其余部分中的逻辑能够正确工作。

例如,如果检测到更新组件,则初始化m_HasUpdateComponentm_NumberUpdateComponentsm_FirstUpdateComponentLocation变量。

作为另一个例子,如果检测到碰撞器组件以及rect特定类型,则m_HasColliderm_NumberRectColliderComponentsm_FirstRectColliderComponent变量被初始化:

void GameObject::addComponent(shared_ptr<Component> component)
{
    m_Components.push_back(component);
    component->enableComponent();

   if (component->getType() == "update") 
    {
        m_HasUpdateComponent = true;
        m_NumberUpdateComponents++ ;
        if (m_NumberUpdateComponents == 1) 
        {
            m_FirstUpdateComponentLocation = 
                m_Components.size() - 1;
        }
    }
    else if (component->getType() == "graphics") 
    {
        // No iteration in the draw method required
        m_HasGraphicsComponent = true;
        m_GraphicsComponentLocation = m_Components.size() - 1;
    }
    else if (component->getType() == "transform") 
    {
        // Remember where the Transform component is
        m_TransformComponentLocation = m_Components.size() - 1;
    }
    else if (component->getType() == "collider" && 
        component->getSpecificType() == "rect") 
    {
        // Remember where the collider component(s) is
        m_HasCollider = true;
        m_NumberRectColliderComponents++ ;
        if (m_NumberRectColliderComponents == 1) 
        {
            m_FirstRectColliderComponentLocation = 
                m_Components.size() - 1;
        }
    }    
}

请注意,GameObject类不参与实际组件本身的配置或设置。这些都是在工厂模式类中处理的,我们将在下一章进行编码。

解释 getter 和 setter 函数

下面的代码是一系列非常简单的获取器和设置器:

void GameObject::setActive()
{
    m_Active = true;
}
void GameObject::setInactive()
{
    m_Active = false;
}
bool GameObject::isActive()
{
    return m_Active;
}
void GameObject::setTag(String tag)
{
    m_Tag = "" + tag;
}
std::string GameObject::getTag()
{
    return m_Tag;
}

前面的获取器和设置器提供了关于游戏对象的信息,比如它是否活动以及它的标签是什么。它们还允许您设置标签,并告诉我们游戏对象是否处于活动状态。

解释启动功能

start功能是一个重要的功能。正如我们在对所有组件进行编码时所看到的那样,start函数允许访问任何游戏对象中的任何组件以及任何其他游戏对象的组件。一旦所有的GameObject实例由它们的所有组件组成,就调用start函数。在下一章中,我们将看到这是如何发生的,以及何时在每个GameObject实例上调用start函数。如我们所见,在start函数中,它循环遍历每个组件并共享一个新的类实例,即GameObjectSharer实例。这个GameObjectSharer类将在下一章进行编码,并允许访问任何类中的任何组件。我们看到了入侵者如何需要知道玩家在哪里,以及当我们对各种组件进行编码时如何使用GameObjectSharer参数。当在每个组件上调用start时,this指针也被传入,以使每个组件易于访问其包含的GameObject实例:

void GameObject::start(GameObjectSharer* gos) 
{
    auto it = m_Components.begin();
    auto end = m_Components.end();
    for (it;
        it != end;
        ++ it)
    {
        (*it)->start(gos, this);
    }
}

让我们进入getComponentByTypeAndSpecificType功能。

解释 getcomponentbyteyandspecifictype 函数

getComponentByTypeAndSpecificType函数有一个嵌套的for循环,它寻找组件类型与第一个string参数的匹配,然后在第二个string参数中寻找特定组件类型的匹配。它返回一个指向基类Component实例的共享指针。这意味着调用代码需要确切地知道正在返回什么派生的Component类型,以便能够将其转换为所需的类型。这应该不是问题,因为,当然,他们同时请求类型和特定类型:

// Slow only use in start
shared_ptr<Component> GameObject::getComponentByTypeAndSpecificType(
    string type, string specificType) {
    auto it = m_Components.begin();
    auto end = m_Components.end();
    for (it;
        it != end;
        ++ it)
    {
        if ((*it)->getType() == type)
        {
            if ((*it)->getSpecificType() == specificType)
            {
                return  (*it);
            }
        }
    }
    #ifdef debuggingErrors        
        cout << 
            "GameObject.cpp::getComponentByTypeAndSpecificType-" 
            << "COMPONENT NOT FOUND ERROR!" 
            << endl;
    #endif
        return m_Components[0];
}

这个函数中的代码非常慢,因此打算在主游戏循环之外使用。在该功能结束时,如果已经定义了debuggingErrors,代码将向控制台写入一条错误消息。这样做的原因是因为,如果执行达到这一点,就意味着没有找到匹配的组件,游戏就会崩溃。控制台的输出应该使错误易于发现。崩溃的原因可能是为无效类型或特定类型调用了该函数。

解释 getEncompassingRectCollider 函数

getEncompassingRectCollider函数检查游戏对象是否有碰撞器,如果有,则返回调用代码:

FloatRect& GameObject::getEncompassingRectCollider() 
{
    if (m_HasCollider) 
    {
        return (static_pointer_cast<RectColliderComponent>(
            m_Components[m_FirstRectColliderComponentLocation]))
            ->getColliderRectF();
    }
}

值得注意的是,如果你扩展这个项目来处理多种类型的碰撞器,那么这段代码也需要修改。

解释 getEncompassingRectColliderTag 函数

这个简单的函数返回碰撞器的标签。这将有助于确定测试碰撞的对象类型:

string GameObject::getEncompassingRectColliderTag() 
{
    return static_pointer_cast<RectColliderComponent>(
        m_Components[m_FirstRectColliderComponentLocation])->
        getColliderTag();
}

我们还有几个函数要讨论。

解释 getFirstUpdateComponent 函数

getFirstUpdateComponent使用m_FirstUpdateComponent变量定位更新组件,然后将其返回给调用代码:

shared_ptr<UpdateComponent> GameObject::getFirstUpdateComponent()
{
    return static_pointer_cast<UpdateComponent>(
        m_Components[m_FirstUpdateComponentLocation]);
}

现在我们只需要看几个吸气剂,然后我们就完成了。

解释最终的 getter 函数

这两个剩余的函数返回一个布尔值(每个)来告诉调用代码游戏对象是否有碰撞器和/或更新组件:

bool GameObject::hasCollider() 
{
    return m_HasCollider;
}
bool GameObject::hasUpdateComponent()
{
    return m_HasUpdateComponent;
}

我们已经对GameObject类进行了完整的编码。我们现在可以考虑让它(以及它将包含的所有组件)工作。

总结

在本章中,我们已经完成了将我们的游戏对象绘制到屏幕上、控制它们的行为并让它们通过碰撞与其他类交互的所有代码。本章要讲的最重要的事情不是任何特定的基于组件的类如何工作,而是实体-组件系统有多灵活。如果你想要一个有特定行为方式的游戏对象,创建一个新的更新组件。如果它需要了解游戏中的其他对象,可以在start功能中获取一个指向相应组件的指针。如果它需要以一种奇特的方式绘制,也许用一个着色器或者一个动画,编码一个图形组件来执行draw函数中的动作。如果你需要多个对撞机,就像我们在托马斯迟到项目中为托马斯和鲍勃做的那样,这是没有问题的:编写一个新的基于对撞机的组件。

在下一章中,我们将对文件输入和输出系统以及类进行编码,该类将是构建所有游戏对象并用组件组成它们的工厂。