Skip to content

Latest commit

 

History

History
836 lines (690 loc) · 36.5 KB

File metadata and controls

836 lines (690 loc) · 36.5 KB

十七、声音空间化和平视显示器

在本章中,我们将添加所有的声音效果和平视显示器。我们已经在前面的两个项目中做到了这一点,但是这次我们会做一些不同的事情。我们将探索声音空间化的概念,以及 SFML 如何让这个原本复杂的概念变得美好和简单。此外,我们将构建一个 HUD 类来封装我们将信息绘制到屏幕上的代码。

我们将按以下顺序完成这些任务。

  • 什么是空间化?
  • SFML 如何处理空间化
  • 构建声音管理器类
  • 部署发射器
  • 使用声音管理器类
  • 建一个HUD
  • 使用HUD

什么是空间化?

空间化是相对于某个事物是其一部分的空间或其内部来制造该事物的行为。在我们的日常生活中,自然世界中的一切,默认都是空间化的。如果一辆摩托车从左向右呼啸而过,我们会听到声音从一边到另一边从微弱到响亮。当它经过时,它会在另一只耳朵里变得更加突出,然后再次消失在远处。如果我们有一天早上醒来,世界不再空间化,那将是异常怪异的。

如果我们能让我们的电子游戏更像真实世界一点,我们的玩家就能变得更沉浸其中。如果玩家能在远处隐约听到他们的声音,当他们从一个方向或另一个方向靠近时,他们非人的哭喊声会越来越大,我们的僵尸游戏就会有趣得多。

很明显,空间化的数学很复杂。我们如何根据从演奏者(声音的听者)到发出声音的物体(发声者)的距离和方向来计算给定声音在特定扬声器中的音量?

幸运的是,SFML 为我们完成了所有复杂的过程。我们所需要做的就是熟悉一些技术术语,然后我们可以开始使用 SFML 来使我们的声音效果空间化。

发射器、衰减和监听器

我们需要了解一些信息,以便向 SFML 提供它开展工作所需的信息。在我们的游戏世界中,我们需要知道声音来自哪里。这个声源被称为发射器。在一个游戏中,发射器可能是一个僵尸,一辆车,或者在我们当前项目的情况下,一个火砖。我们已经跟踪了游戏中物体的位置,所以给 SFML 发射器的位置会很简单。

我们需要注意的下一个因素是衰减。衰减是波恶化的速度。你可以简化这种说法,让它专门针对声音,说衰减是声音音量降低的速度。这在技术上并不准确,但对于本章和我们的游戏来说,这是一个足够好的描述。

我们需要考虑的最后一个因素是听者。当 SFML 把声音空间化时,它相对于哪里空间化;游戏的“耳朵”在哪里。?在大多数游戏中,合乎逻辑的做法是使用玩家角色。在我们的游戏中,我们将使用托马斯(我们的玩家角色)。

使用 SFML 处理空间化

SFML 有几个功能,允许我们处理发射器,衰减和听众。让我们假设性地看一下它们,然后我们将编写一些代码,将空间化的声音真实地添加到我们的项目中。

我们可以设置一个准备播放的音效,就像我们已经经常做的那样,如下所示:

// Declare SoundBuffer in the usual way
SoundBuffer zombieBuffer;
// Declare a Sound object as-per-usual
Sound zombieSound;
// Load the sound from a file like we have done so often
zombieBuffer.loadFromFile("sound/zombie_growl.wav");
// Associate the Sound object with the Buffer
zombieSound.setBuffer(zombieBuffer);

我们可以使用如下代码所示的setPosition功能设置发射器的位置:

// Set the horizontal and vertical positions of the emitter
// In this case the emitter is a zombie
// In the Zombie Arena project we could have used 
// getPosition().x and getPosition().y
// These values are arbitrary
float x = 500;
float y = 500;
zombieSound.setPosition(x, y, 0.0f);

正如前面代码的注释中所建议的,我们如何准确地获得发射器的坐标可能取决于游戏的类型。如前面的代码所示,这在僵尸竞技场项目中非常简单。当我们在这个项目中确定位置时,我们将有一些挑战需要克服。

我们可以按如下方式设置衰减级别:

zombieSound.setAttenuation(15);

实际衰减水平可能有点模糊。我们希望玩家得到的效果可能与精确的科学公式不同,该公式用于根据衰减来降低远距离音量。获得正确的衰减水平通常是通过实验来实现的。衰减级别越高,声音级别降低到静音的速度越快。

此外,我们可能希望在发射器周围设置一个区域,使体积完全不衰减。如果该功能在特定范围之外不合适,或者如果我们有许多声源并且不想“过度”使用该功能,我们可能会这样做。为此,我们可以使用如下所示的setMinimumDistance功能:

zombieSound.setMinDistance(150);

对于前一行代码,在收听者距离发射器 150 像素/单位之前,不会计算衰减。

SFML 图书馆的其他一些有用的功能包括setLoop功能。当true作为参数传入时,该函数将告诉 SFML 不断播放声音,如以下代码所示:

zombieSound.setLoop(true);

声音将继续播放,直到我们用以下代码结束:

zombieSound.stop();

有时,我们会想知道声音的状态(播放或停止)。我们可以通过getStatus函数实现这一点,如下面的代码所示:

if (zombieSound.getStatus() == Sound::Status::Stopped)
{
    // The sound is NOT playing
    // Take whatever action here
}
if (zombieSound.getStatus() == Sound::Status::Playing)
{
    // The sound IS playing
    // Take whatever action here
}

在 SFML 使用声音空间化还有一个方面我们需要讨论。倾听者。听者在哪里?我们可以用下面的代码设置监听器的位置:

// Where is the listener? 
// How we get the values of x and y varies depending upon the game
// In the Zombie Arena game or the Thomas Was Late game
// We can use getPosition()
Listener::setPosition(m_Thomas.getPosition().x, 
    m_Thomas.getPosition().y, 0.0f);

前面的代码将使所有声音相对于该位置播放。这正是我们对于远处的火瓦轰鸣声或者来袭的丧尸所需要的,但是对于像跳跃这样的常规音效来说,这是一个问题。我们可以开始处理玩家位置的发射器,但是 SFML 让事情变得简单了。每当我们想要播放一个“正常”的声音时,我们只需简单地调用setRelativeToListener,如下代码所示,然后以与目前完全相同的方式播放声音。以下是我们如何播放“正常”的非空间跳跃音效:

jumpSound.setRelativeToListener(true);
jumpSound.play();

我们所需要做的就是在播放任何空间化的声音之前再次调用Listener::setPosition

我们现在有一个广泛的 SFML 声音功能剧目,我们准备好了一些空间噪音的真实。

构建声音管理器类

您可能还记得上一个项目,所有的声音代码占用了相当多的代码行。现在,考虑一下,随着空间化,它会变得更长。为了使我们的代码易于管理,我们将编写一个类来管理所有正在播放的声音效果。此外,为了帮助我们进行空间化,我们还将向Engine类添加一个函数,但是我们将在本章稍后讨论这个问题。

编码声音管理器

让我们从编码和检查头文件开始。

右键单击解决方案资源管理器中的头文件,选择添加|新项目...。在添加新项目窗口中,突出显示(通过左键单击)头文件(。h) 然后在名称字段中,输入SoundManager.h。最后,点击添加按钮。我们现在准备为SoundManager类编码头文件。

添加并检查以下代码:

#pragma once
#include <SFML/Audio.hpp>
using namespace sf;
class SoundManager
{
    private:
        // The buffers
        SoundBuffer m_FireBuffer;
        SoundBuffer m_FallInFireBuffer;
        SoundBuffer m_FallInWaterBuffer;
        SoundBuffer m_JumpBuffer;
        SoundBuffer m_ReachGoalBuffer;
        // The Sounds
        Sound m_Fire1Sound;
        Sound m_Fire2Sound;
        Sound m_Fire3Sound;
        Sound m_FallInFireSound;
        Sound m_FallInWaterSound;
        Sound m_JumpSound;
        Sound m_ReachGoalSound;
        // Which sound should we use next, fire 1, 2 or 3
        int m_NextSound = 1;
    public:
        SoundManager();
        void playFire(Vector2f emitterLocation, 
            Vector2f listenerLocation);
        void playFallInFire();
        void playFallInWater();
        void playJump();
        void playReachGoal();
};

在我们刚刚添加的代码中没有什么棘手的地方。有五个SoundBuffer对象和八个Sound对象。三个Sound物体将会玩同样的SoundBuffer。这就解释了Sound / SoundBuffer物体数量不同的原因。我们这样做是为了让多个咆哮的声音效果同时播放,并带有不同的空间化参数。

请注意m_NextSound变量,它将帮助我们记录下一步应该使用这些同步声音中的哪一个。

有一个构造器,SoundManager,我们将在这里设置我们所有的音效,有五个功能将播放音效。其中四个功能只是播放“正常”的音效,它们的代码会更简单。

其中一个功能playFire,会处理空间化的音效,会更深入一点。注意playFire功能的参数。它接收一个Vector2f,这是发射器的位置,和第二个Vector2f,这是听者的位置。

对 SoundManager.cpp 文件进行编码

现在,我们可以对函数定义进行编码。构造函数和playFire函数有大量的代码,所以我们将分别来看。其他功能都很简短,所以我们将一次性处理它们。

右键单击解决方案资源管理器中的源文件,并选择添加|新项目...。在添加新项目窗口中,突出显示(通过左键单击) C++ 文件(。cpp) 然后,在名称字段中,键入SoundManager.cpp。最后,点击添加按钮。我们现在准备为SoundManager类编码.cpp文件。

构造函数的编码

将包含指令和构造函数的以下代码添加到SoundManager.cpp:

#include "SoundManager.h"
#include <SFML/Audio.hpp>
using namespace sf;
SoundManager::SoundManager()
{
    // Load the sound in to the buffers
    m_FireBuffer.loadFromFile("sound/fire1.wav");
    m_FallInFireBuffer.loadFromFile("sound/fallinfire.wav");
    m_FallInWaterBuffer.loadFromFile("sound/fallinwater.wav");
    m_JumpBuffer.loadFromFile("sound/jump.wav");
    m_ReachGoalBuffer.loadFromFile("sound/reachgoal.wav");
    // Associate the sounds with the buffers
    m_Fire1Sound.setBuffer(m_FireBuffer);
    m_Fire2Sound.setBuffer(m_FireBuffer);
    m_Fire3Sound.setBuffer(m_FireBuffer);
    m_FallInFireSound.setBuffer(m_FallInFireBuffer);
    m_FallInWaterSound.setBuffer(m_FallInWaterBuffer);
    m_JumpSound.setBuffer(m_JumpBuffer);
    m_ReachGoalSound.setBuffer(m_ReachGoalBuffer);

    // When the player is 50 pixels away sound is full volume
    float minDistance = 150;
    // The sound reduces steadily as the player moves further away
    float attenuation = 15;
    // Set all the attenuation levels
    m_Fire1Sound.setAttenuation(attenuation);
    m_Fire2Sound.setAttenuation(attenuation);
    m_Fire3Sound.setAttenuation(attenuation);
    // Set all the minimum distance levels
    m_Fire1Sound.setMinDistance(minDistance);
    m_Fire2Sound.setMinDistance(minDistance);
    m_Fire3Sound.setMinDistance(minDistance);
    // Loop all the fire sounds
    // when they are played
    m_Fire1Sound.setLoop(true);
    m_Fire2Sound.setLoop(true);
    m_Fire3Sound.setLoop(true);
}

在前面的代码中,我们将五个声音文件加载到五个SoundBuffer对象中。接下来,我们将八个Sound对象与一个SoundBuffer对象相关联。注意m_Fire1Soundm_Fire2Soundm_Fire3Sound都是从同一个SoundBufferm_FireBuffer开始玩。

接下来,我们设置三种火音的衰减和最小距离。

小费

分别通过实验得出15015的值。一旦游戏开始运行,建议通过改变这些值并观察(或者更确切地说,听到)差异来尝试这些值。

最后,对于构造函数,我们在每个与火相关的Sound对象上使用setLoop函数。现在,当我们叫play的时候,他们会一直打下去。

对 playFire 函数进行编码

如下添加playFire功能。然后,我们可以讨论一下:

void SoundManager::playFire(
    Vector2f emitterLocation, Vector2f listenerLocation)
{
    // Where is the listener? Thomas.
    Listener::setPosition(listenerLocation.x, 
        listenerLocation.y, 0.0f);
    switch(m_NextSound)
    {
    case 1:
        // Locate/move the source of the sound
        m_Fire1Sound.setPosition(emitterLocation.x, 
            emitterLocation.y, 0.0f);
        if (m_Fire1Sound.getStatus() == Sound::Status::Stopped)
        {
            // Play the sound, if its not already
            m_Fire1Sound.play();
        }
        break;
    case 2:
        // Do the same as previous for the second sound
        m_Fire2Sound.setPosition(emitterLocation.x, 
            emitterLocation.y, 0.0f);
        if (m_Fire2Sound.getStatus() == Sound::Status::Stopped)
        {
            m_Fire2Sound.play();
        }
        break;
    case 3:
        // Do the same as previous for the third sound
        m_Fire3Sound.setPosition(emitterLocation.x, 
            emitterLocation.y, 0.0f);
        if (m_Fire3Sound.getStatus() == Sound::Status::Stopped)
        {
            m_Fire3Sound.play();
        }
        break;
    }
    // Increment to the next fire sound
    m_NextSound++ ;
    // Go back to 1 when the third sound has been started
    if (m_NextSound > 3)
    {
        m_NextSound = 1;
    }
}

我们要做的第一件事是调用Listener::setPosition并根据作为参数传入的Vector2f设置监听器的位置。

接下来,代码进入测试m_NextSound值的switch块。每个case声明都做了完全相同的事情,但是要么是m_Fire1Soundm_Fire2Sound要么是m_Fire3Sound

在每个case块中,我们通过setPosition功能使用传入的参数设置发射器的位置。每个case块中代码的下一部分检查声音当前是否停止,如果是,播放声音。很快,我们将看到如何到达传递给这个函数的发射器和接收器的位置。

playFire函数的最后一部分递增m_NextSound,并确保它只能等于 1、2 或 3,如switch块所要求的。

对 SoundManager 的其余功能进行编码

添加这四个简单的函数:

void SoundManager::playFallInFire()
{
    m_FallInFireSound.setRelativeToListener(true);
    m_FallInFireSound.play();
}
void SoundManager::playFallInWater()
{
    m_FallInWaterSound.setRelativeToListener(true);
    m_FallInWaterSound.play();
}
void SoundManager::playJump()
{
    m_JumpSound.setRelativeToListener(true);
    m_JumpSound.play();
}
void SoundManager::playReachGoal()
{
    m_ReachGoalSound.setRelativeToListener(true);
    m_ReachGoalSound.play();
}

playFallInFireplayFallInWaterplayReachGoal功能只做两件事。首先,他们各自调用setRelativeToListener使音效不空间化,使音效“正常”,不具有方向性,然后在合适的Sound对象上调用play

SoundManager课到此结束。现在,我们可以在Engine类中使用它。

在游戏引擎中添加声音管理器

打开Engine.h文件,添加新的SoundManager类的实例,如下图高亮显示的代码所示:

#pragma once
#include <SFML/Graphics.hpp>
#include "TextureHolder.h"
#include "Thomas.h"
#include "Bob.h"
#include "LevelManager.h"
#include "SoundManager.h"
using namespace sf;
class Engine
{
private:
    // The texture holder
    TextureHolder th;
    // Thomas and his friend, Bob
    Thomas m_Thomas;
    Bob m_Bob;
    // A class to manage all the levels
    LevelManager m_LM;
    // Create a SoundManager
    SoundManager m_SM;
    const int TILE_SIZE = 50;
    const int VERTS_IN_QUAD = 4;

此时,我们可以使用m_SM来调用各种play...函数。不幸的是,为了管理发射器(火砖)的位置,还有一点工作要做。

填充声音发射器

打开Engine.h文件,为populateEmitters函数添加一个新原型,并为Vector2f对象添加一个新的 STL vector:

    ...
    ...
    ...
    // Run will call all the private functions
    bool detectCollisions(PlayableCharacter& character);
    // Make a vector of the best places to emit sounds from
    void populateEmitters(vector <Vector2f>& vSoundEmitters,
        int** arrayLevel);
    // A vector of Vector2f for the fire emitter locations
    vector <Vector2f> m_FireEmitters;

public:
    ...
    ...
    ...

populateEmitters函数以一个Vector2f对象的vector为参数,还有一个指向int的指针(二维数组)。vector会将每个发射器的位置保持在一个水平。数组是保存级别布局的二维数组。

对人口发射器功能进行编码

populateEmitters功能的工作是扫描arrayLevel的所有元素,并决定发射器放在哪里。它将把结果储存在m_FireEmitters

右键单击解决方案资源管理器中的源文件,并选择添加|新项目...。在添加新项目窗口中,突出显示(通过左键单击) C++ 文件(。cpp) 然后,在名称字段中,键入PopulateEmitters.cpp。最后,点击添加按钮。现在,我们可以对新函数populateEmitters进行编码了。

完整地添加代码。请务必像您一样研究代码,以便我们可以讨论它:

#include "Engine.h"
using namespace sf;
using namespace std;
void Engine::populateEmitters(
    vector <Vector2f>& vSoundEmitters, 
   int** arrayLevel)
{
    // Make sure the vector is empty
    vSoundEmitters.empty();
    // Keep track of the previous emitter
    // so we don't make too many
    FloatRect previousEmitter;
    // Search for fire in the level
    for (int x = 0; x < (int)m_LM.getLevelSize().x; x++)
    {
        for (int y = 0; y < (int)m_LM.getLevelSize().y; y++)
        {
            if (arrayLevel[y][x] == 2)// fire is present
            {
                // Skip over any fire tiles too 
                // near a previous emitter
                if (!FloatRect(x * TILE_SIZE,
                    y * TILE_SIZE,
                    TILE_SIZE,
                    TILE_SIZE).intersects(previousEmitter))
                {
                    // Add the coordinates of this water block
                    vSoundEmitters.push_back(
                        Vector2f(x * TILE_SIZE, y * TILE_SIZE));
                    // Make a rectangle 6 blocks x 6 blocks,
                    // so we don't make any more emitters 
                    // too close to this one
                    previousEmitter.left = x * TILE_SIZE;
                    previousEmitter.top = y * TILE_SIZE;
                    previousEmitter.width = TILE_SIZE * 6;
                    previousEmitter.height = TILE_SIZE * 6;
                }
            }
        }
    }
    return;
}

乍一看,有些代码可能看起来很复杂。理解我们用来选择发射器位置的技术会使这变得更简单。在我们的关卡中,有大量的火砖。例如,在一个级别中,一个组中有 30 多个火砖。代码确保在给定的矩形内只有一个发射器。该矩形存储在previousEmitter中,为 300 像素乘 300 像素(TILE_SIZE * 6)。

代码设置了一个嵌套的for循环,循环通过arrayLevel,寻找火砖。当它找到一个时,它确保它不与previousEmitter相交。只有到那时,它才会使用pushBack功能向vSoundEmitters添加另一个发射器。这样做之后,它还会更新previousEmitter以避免获得大簇的声音发射器。

让我们制造一些噪音。

播放声音

打开LoadLevel.cpp文件,将调用添加到新的populateEmitters函数中,如下代码所示:

void Engine::loadLevel()
{
    m_Playing = false;
    // Delete the previously allocated memory
    for (int i = 0; i < m_LM.getLevelSize().y; ++ i)
    {
        delete[] m_ArrayLevel[i];
    }
    delete[] m_ArrayLevel;
    // Load the next 2d array with the map for the level
    // And repopulate the vertex array as well
    m_ArrayLevel = m_LM.nextLevel(m_VALevel);
    // Prepare the sound emitters
    populateEmitters(m_FireEmitters, m_ArrayLevel);
    // How long is this new time limit
    m_TimeRemaining = m_LM.getTimeLimit();
    // Spawn Thomas and Bob
    m_Thomas.spawn(m_LM.getStartPosition(), GRAVITY);
    m_Bob.spawn(m_LM.getStartPosition(), GRAVITY);
    // Make sure this code isn't run again
    m_NewLevelRequired = false;
}

第一个要添加的声音是跳跃声。我们记得键盘处理代码是在BobThomas类中的纯虚函数中,并且handleInput函数在成功启动跳转时返回true

打开Input.cpp文件,添加以下高亮显示的代码行,在托马斯或鲍勃成功开始跳跃时播放跳跃声音:

// Handle input specific to Thomas
if (m_Thomas.handleInput())
{
    // Play a jump sound
    m_SM.playJump();
}
// Handle input specific to Bob
if (m_Bob.handleInput())
{
    // Play a jump sound
    m_SM.playJump();
}

打开Update.cpp文件,添加以下高亮显示的代码行,当托马斯和鲍勃同时达到当前级别的目标时,播放成功声音:

// Detect collisions and see if characters have reached the goal tile
// The second part of the if condition is only executed
// when Thomas is touching the home tile
if (detectCollisions(m_Thomas) && detectCollisions(m_Bob))
{
    // New level required
    m_NewLevelRequired = true;
    // Play the reach goal sound
    m_SM.playReachGoal();
}
else
{
    // Run Bobs collision detection
    detectCollisions(m_Bob);
}

此外,在Update.cpp文件中,我们将添加代码来循环通过m_FireEmitters向量,并决定何时需要调用SoundManager类的playFire函数。

仔细观察新突出显示的代码周围的少量上下文。在正确的位置添加这段代码非常重要:

}// End if playing
// Check if a fire sound needs to be played
vector<Vector2f>::iterator it;
// Iterate through the vector of Vector2f objects
for (it = m_FireEmitters.begin(); it != m_FireEmitters.end(); it++)
{
    // Where is this emitter?
    // Store the location in pos
    float posX = (*it).x;
    float posY = (*it).y;
    // is the emitter near the player?
    // Make a 500 pixel rectangle around the emitter
    FloatRect localRect(posX - 250, posY - 250, 500, 500);
    // Is the player inside localRect?
    if (m_Thomas.getPosition().intersects(localRect))
    {
        // Play the sound and pass in the location as well
        m_SM.playFire(Vector2f(posX, posY), m_Thomas.getCenter());
    }
}

// Set the appropriate view around the appropriate character

前面的代码有点像声音的碰撞检测。每当托马斯在火发射器周围 500×500 像素的矩形内游荡时,就会调用playFire函数,传递发射器和托马斯的坐标。playFire功能完成剩下的工作,播放空间化的循环音效。

打开DetectCollisions.cpp文件,找到合适的地方,添加下面高亮显示的代码。当任一角色落入水中或火中时,这两行高亮显示的代码会触发声音效果:

// Has character been burnt or drowned?
// Use head as this allows him to sink a bit
if (m_ArrayLevel[y][x] == 2 || m_ArrayLevel[y][x] == 3)
{
    if (character.getHead().intersects(block))
    {
        character.spawn(m_LM.getStartPosition(), GRAVITY);
        // Which sound should be played?
        if (m_ArrayLevel[y][x] == 2)// Fire, ouch!
        {
            // Play a sound
            m_SM.playFallInFire();
        }
        else // Water
        {
            // Play a sound
            m_SM.playFallInWater();
        }
    }
}

玩这个游戏现在可以让你听到所有的声音,包括当你靠近一个火砖的时候,很酷的空间感。

实现平显类

平视显示器超级简单,与僵尸竞技场项目相比没有什么不同。不同的是,我们将把所有代码打包到一个新的HUD类中。如果我们将所有FontText和其他变量声明为这个新类的成员,那么我们可以在构造函数中初始化它们,并为它们的所有值提供 getter 函数。这将使Engine类免受大量声明和初始化的影响。

编码平视显示器

首先,我们将使用所有成员变量和函数声明对HUD.h文件进行编码。右键单击解决方案资源管理器中的头文件,选择添加|新项目...。在添加新项目窗口中,突出显示(通过左键单击)头文件(。h) 然后在名称字段中,输入HUD.h。最后,点击添加按钮。我们现在准备为HUD类编码头文件。

HUD.h中添加以下代码:

#pragma once
#include <SFML/Graphics.hpp>
using namespace sf;
class Hud
{
private:
    Font m_Font;
    Text m_StartText;
    Text m_TimeText;
    Text m_LevelText;
public:
    Hud();
    Text getMessage();
    Text getLevel();
    Text getTime();
    void setLevel(String text);
    void setTime(String text);
};

在前面的代码中,我们添加了一个Font实例和三个Text实例。Text对象将用于显示提示用户开始、剩余时间和当前级别号的信息。

公共功能更有趣。首先,有一个构造函数,大部分代码都将在这里运行。构造器将初始化FontText对象,并将它们相对于当前屏幕分辨率定位在屏幕上。

三个 getter 函数getMessagegetLevelgetTime将向调用代码返回一个Text对象,以便它可以将它们绘制到屏幕上。

setLevelsetTime功能将分别用于更新m_LevelTextm_TimeText中显示的文本。

现在,我们可以为刚刚声明的函数编写所有的定义。

对 HUD.cpp 文件进行编码

右键单击解决方案资源管理器中的源文件,并选择添加|新项目...。在添加新项目窗口中,突出显示(通过左键单击) C++ 文件(。cpp) 然后,在名称字段中,键入HUD.cpp。最后,点击添加按钮。我们现在准备为HUD类编码.cpp文件。

添加 include 指令和以下代码。然后,我们将讨论它:

#include "Hud.h"
Hud::Hud()
{
    Vector2u resolution;
    resolution.x = VideoMode::getDesktopMode().width;
    resolution.y = VideoMode::getDesktopMode().height;
    // Load the font
    m_Font.loadFromFile("fonts/Roboto-Light.ttf");
    // when Paused
    m_StartText.setFont(m_Font);
    m_StartText.setCharacterSize(100);
    m_StartText.setFillColor(Color::White);
    m_StartText.setString("Press Enter when ready!");
    // Position the text
    FloatRect textRect = m_StartText.getLocalBounds();
    m_StartText.setOrigin(textRect.left +
        textRect.width / 2.0f,
        textRect.top +
        textRect.height / 2.0f);
    m_StartText.setPosition(
        resolution.x / 2.0f, resolution.y / 2.0f);
    // Time
    m_TimeText.setFont(m_Font);
    m_TimeText.setCharacterSize(75);
    m_TimeText.setFillColor(Color::White);
    m_TimeText.setPosition(resolution.x - 150, 0);
    m_TimeText.setString("------");
    // Level
    m_LevelText.setFont(m_Font);
    m_LevelText.setCharacterSize(75);
    m_LevelText.setFillColor(Color::White);
    m_LevelText.setPosition(25, 0);
    m_LevelText.setString("1");
}

首先,我们将水平和垂直分辨率存储在名为resolutionVector2u中。接下来,我们从fonts目录加载字体,我们在 第 14 章抽象和代码管理–更好地利用 OOP

接下来的四行代码设置m_StartText的字体、颜色、大小和文本。在这之后的代码块捕获包裹m_StartText的矩形的大小,并执行计算,以计算出如何将其定位在屏幕的中心。如果你想更彻底的解释这部分代码,那么参考 第三章c++ 字符串和 SFML 时间–玩家输入和 HUD

在构造函数的最后两个代码块中,设置了m_TimeTextm_LevelText的字体、文本大小、颜色、位置和实际文本。稍后,我们将看到这两个Text对象将通过两个 setter 函数进行更新,无论何时需要。

在我们刚刚添加的代码下面添加以下 getter 和 setter 函数:

Text Hud::getMessage()
{
    return m_StartText;
}
Text Hud::getLevel()
{
    return m_LevelText;
}
Text Hud::getTime()
{
    return m_TimeText;
}
void Hud::setLevel(String text)
{
    m_LevelText.setString(text);
}
void Hud::setTime(String text)
{
    m_TimeText.setString(text);
}

前面代码中的前三个函数只是返回适当的Text对象,即m_StartTextm_LevelTextm_TimeText。当我们将平视显示器绘制到屏幕上时,我们将很快使用这些功能。最后两个函数setLevelsetTime使用setString函数更新相应的Text对象,其值将从Engine类的update函数传入,每 500 帧更新一次。

所有这些完成后,我们可以让平视显示器类在我们的游戏引擎中工作。

使用抬头显示器类

打开Engine.h,为我们的新类添加一个 include,声明一个新的HUD类的实例,并声明和初始化两个新的成员变量,它们将跟踪我们更新 HUD 的频率。正如我们在前面的项目中学到的,我们不需要每一帧都更新 HUD。

Engine.h中添加以下高亮显示的代码:

#pragma once
#include <SFML/Graphics.hpp>
#include "TextureHolder.h"
#include "Thomas.h"
#include "Bob.h"
#include "LevelManager.h"
#include "SoundManager.h"
#include "HUD.h"
using namespace sf;
class Engine
{
private:
    // The texture holder
    TextureHolder th;
    // Thomas and his friend, Bob
    Thomas m_Thomas;
    Bob m_Bob;
    // A class to manage all the levels
    LevelManager m_LM;
    // Create a SoundManager
    SoundManager m_SM;
    // The Hud
    Hud m_Hud;
    int m_FramesSinceLastHUDUpdate = 0;
    int m_TargetFramesPerHUDUpdate = 500;
    const int TILE_SIZE = 50;

接下来,我们需要给Engine类的update函数添加一些代码。打开Update.cpp并添加以下高亮显示的代码,每 500 帧更新一次抬头显示器:

    // Set the appropriate view around the appropriate character
    if (m_SplitScreen)
    {
        m_LeftView.setCenter(m_Thomas.getCenter());
        m_RightView.setCenter(m_Bob.getCenter());
    }
    else
    {
        // Centre full screen around appropriate character
        if (m_Character1)
        {
            m_MainView.setCenter(m_Thomas.getCenter());
        }
        else
        {
            m_MainView.setCenter(m_Bob.getCenter());
        }
    }
    // Time to update the HUD?
// Increment the number of frames since 
   // the last HUD calculation
    m_FramesSinceLastHUDUpdate++ ;
    // Update the HUD every m_TargetFramesPerHUDUpdate frames
    if (m_FramesSinceLastHUDUpdate > m_TargetFramesPerHUDUpdate)
    {
        // Update game HUD text
        stringstream ssTime;
        stringstream ssLevel;
        // Update the time text
        ssTime << (int)m_TimeRemaining;
        m_Hud.setTime(ssTime.str());
        // Update the level text
        ssLevel << "Level:" << m_LM.getCurrentLevel();
        m_Hud.setLevel(ssLevel.str());
        m_FramesSinceLastHUDUpdate = 0;
    }
}// End of update function

在前面的代码中,m_FramesSinceLastUpdate每帧递增一次。当m_FramesSinceLastUpdate超过m_TargetFramesPerHUDUpdate时,执行进入if块。在if块中,我们使用stringstream对象来更新我们的Text,就像我们在之前的项目中所做的那样。在这个项目中,我们使用的是HUD类,所以我们通过传入Text对象需要设置的当前值来调用setTimesetLevel函数。

if块的最后一步是将m_FramesSinceLastUpdate设置回零,以便它可以开始向下一次更新计数。

最后,打开Draw.cpp文件,添加以下高亮显示的代码,绘制每一帧的 HUD:

    else
    {
        // Split-screen view is active
        // First draw Thomas' side of the screen
        // Switch to background view
        m_Window.setView(m_BGLeftView);
        // Draw the background
        m_Window.draw(m_BackgroundSprite);
        // Switch to m_LeftView
        m_Window.setView(m_LeftView);
        // Draw the Level
        m_Window.draw(m_VALevel, &m_TextureTiles);

        // Draw thomas
        m_Window.draw(m_Bob.getSprite());
        // Draw thomas
        m_Window.draw(m_Thomas.getSprite());

        // Now draw Bob's side of the screen
        // Switch to background view
        m_Window.setView(m_BGRightView);
        // Draw the background
        m_Window.draw(m_BackgroundSprite);
        // Switch to m_RightView
        m_Window.setView(m_RightView);
        // Draw the Level
        m_Window.draw(m_VALevel, &m_TextureTiles);
        // Draw thomas
        m_Window.draw(m_Thomas.getSprite());
        // Draw bob
        m_Window.draw(m_Bob.getSprite());

    }
    // Draw the HUD
    // Switch to m_HudView
    m_Window.setView(m_HudView);
    m_Window.draw(m_Hud.getLevel());
    m_Window.draw(m_Hud.getTime());
    if (!m_Playing)
    {
        m_Window.draw(m_Hud.getMessage());
    }
    // Show everything we have just drawn
    m_Window.display();
}// End of draw

上面的代码通过使用抬头显示器类中的 getter 函数来绘制抬头显示器。请注意,绘制提示玩家开始的消息的调用仅在游戏当前未玩(!m_Playing)时使用。

运行游戏,玩几个关卡,看时间滴答下降,关卡滴答上升。当你再次回到 1 级时,注意你的时间比以前少了 10%。

总结

在这一章中,我们探讨了声音的空间化。我们的“托马斯迟到了”游戏现在不仅完全可以玩了,而且我们还增加了定向音效和一个简单但信息丰富的平视显示器。我们还可以轻松地添加新的级别。至此,我们可以收工了。

多加一点火花就好了。在下一章中,我们将研究两个游戏概念。首先,我们将看看粒子系统,这是我们处理爆炸或其他特殊效果的方式。为了实现这一点,我们需要多学一点 C++。正因如此,才会引入多重继承的话题。

之后,当我们了解 OpenGL 和可编程图形管道时,我们将为游戏添加最后的繁荣。然后,我们将能够接触到 GLSL 语言,它允许我们编写直接在 GPU 上执行的代码,这样我们就可以创建一些特殊效果。