Skip to content

Latest commit

 

History

History
476 lines (401 loc) · 15.7 KB

File metadata and controls

476 lines (401 loc) · 15.7 KB

十三、音效,文件 I/O,完成游戏

我们快到了。 这个简短的章节将演示如何使用 c++ 标准库轻松地操作存储在硬盘上的文件,我们还将添加声音效果。 当然,我们知道如何添加声音效果,但我们将讨论对play函数的调用在代码中的确切位置。 我们也将解决一些松散的结束,使游戏完成。

在本章中,我们将涵盖以下主题:

  • 使用文件输入和文件输出保存和加载高分
  • 添加声音效果
  • 允许玩家升级
  • 创造多个永不结束的波浪

保存并加载高分

文件i/o输入/输出是一个相当技术性的主题。 幸运的是,由于这是编程中常见的需求,所以有一个库可以为我们处理所有这些复杂性。 与连接 HUD 中的字符串一样,c++ 标准库通过fstream提供了必要的功能。

首先,我们以包含sstream的方式包含fstream:

#include <sstream>
#include <fstream>
#include <SFML/Graphics.hpp>
#include "ZombieArena.h"
#include "Player.h"
#include "TextureHolder.h"
#include "Bullet.h"
#include "Pickup.h"
using namespace sf;

现在,在ZombieArena文件夹中添加一个名为gamedata的新文件夹。 接下来,右键单击该文件夹并创建一个名为scores.txt的新文件。 在这个文件中,我们将保存玩家的高分。 您可以轻松地打开该文件并添加一个分数。 如果是,请确保它是一个相当低的分数,以便我们可以很容易地测试是否超过该分数会导致添加新的分数。 一定要关闭文件,一旦你完成它,否则游戏将无法访问它。

在下面的代码中,我们将创建一个名为inputFileifstream对象,并将刚才创建的文件夹和文件作为参数发送给它的构造函数。

if(inputFile.is_open())检查文件是否存在并准备好进行读取。 然后将该文件的内容放入hiScore中并关闭该文件。 添加以下突出显示的代码:

// Score
Text scoreText;
scoreText.setFont(font);
scoreText.setCharacterSize(55);
scoreText.setColor(Color::White);
scoreText.setPosition(20, 0);
// Load the high score from a text file
std::ifstream inputFile("gamedata/scores.txt");
if (inputFile.is_open())
{
 // >> Reads the data
 inputFile >> hiScore;
 inputFile.close();
}
// Hi Score
Text hiScoreText;
hiScoreText.setFont(font);
hiScoreText.setCharacterSize(55);
hiScoreText.setColor(Color::White);
hiScoreText.setPosition(1400, 0);
std::stringstream s;
s << "Hi Score:" << hiScore;
hiScoreText.setString(s.str());

现在,我们可以保存一个可能的新分数了。 在处理玩家健康值小于或等于 0 的块中,我们需要创建一个名为outputFileofstream对象,将hiScore的值写入文本文件,然后关闭该文件,如下所示:

// Have any zombies touched the player            
for (int i = 0; i < numZombies; i++)
{
    if (player.getPosition().intersects
        (zombies[i].getPosition()) && zombies[i].isAlive())
    {
        if (player.hit(gameTimeTotal))
        {
            // More here later
        }
        if (player.getHealth() <= 0)
        {
            state = State::GAME_OVER;
 std::ofstream outputFile("gamedata/scores.txt");
 // << writes the data
 outputFile << hiScore;
 outputFile.close();

        }
    }
}// End player touched

你可以玩游戏,你的高分将被保存。 退出游戏,并注意到如果你再次玩游戏,你的高分仍然存在。

让我们制造一些噪音。

准备音效

在本节中,我们将创建所有的SoundBufferSound对象,我们需要为游戏添加一系列的声音效果。

首先添加所需的 SFML#include语句:

#include <sstream>
#include <fstream>
#include <SFML/Graphics.hpp>
#include <SFML/Audio.hpp>
#include "ZombieArena.h"
#include "Player.h"
#include "TextureHolder.h"
#include "Bullet.h"
#include "Pickup.h"

现在,继续加入七SoundBufferSound对象加载和准备七个声音文件,我们准备在第八章【5】,SFML 观点——僵尸射击游戏开始:

// When did we last update the HUD?
int framesSinceLastHUDUpdate = 0;
// What time was the last update
Time timeSinceLastUpdate;
// How often (in frames) should we update the HUD
int fpsMeasurementFrameInterval = 1000;
// Prepare the hit sound
SoundBuffer hitBuffer;
hitBuffer.loadFromFile("sound/hit.wav");
Sound hit;
hit.setBuffer(hitBuffer);
// Prepare the splat sound
SoundBuffer splatBuffer;
splatBuffer.loadFromFile("sound/splat.wav");
Sound splat;
splat.setBuffer(splatBuffer);
// Prepare the shoot sound
SoundBuffer shootBuffer;
shootBuffer.loadFromFile("sound/shoot.wav");
Sound shoot;
shoot.setBuffer(shootBuffer);
// Prepare the reload sound
SoundBuffer reloadBuffer;
reloadBuffer.loadFromFile("sound/reload.wav");
Sound reload;
reload.setBuffer(reloadBuffer);
// Prepare the failed sound
SoundBuffer reloadFailedBuffer;
reloadFailedBuffer.loadFromFile("sound/reload_failed.wav");
Sound reloadFailed;
reloadFailed.setBuffer(reloadFailedBuffer);
// Prepare the powerup sound
SoundBuffer powerupBuffer;
powerupBuffer.loadFromFile("sound/powerup.wav");
Sound powerup;
powerup.setBuffer(powerupBuffer);
// Prepare the pickup sound
SoundBuffer pickupBuffer;
pickupBuffer.loadFromFile("sound/pickup.wav");
Sound pickup;
pickup.setBuffer(pickupBuffer);
// The main game loop
while (window.isOpen())

现在,七个音效已经准备好了。 我们只需要找出对play函数的每个调用在代码中的位置。

升级

下面我们将添加的代码允许玩家在两次波之间升级。 正因为我们已经做了很多工作,所以这是很容易实现的。

添加以下高亮代码到我们处理玩家输入的LEVELING_UP状态:

// Handle the LEVELING up state
if (state == State::LEVELING_UP)
{
    // Handle the player LEVELING up
    if (event.key.code == Keyboard::Num1)
    {
 // Increase fire rate
 fireRate++ ;
        state = State::PLAYING;
    }
    if (event.key.code == Keyboard::Num2)
    {
 // Increase clip size
 clipSize += clipSize;
        state = State::PLAYING;
    }
    if (event.key.code == Keyboard::Num3)
    {
 // Increase health
 player.upgradeHealth();
        state = State::PLAYING;
    }
    if (event.key.code == Keyboard::Num4)
    {
 // Increase speed
 player.upgradeSpeed();
        state = State::PLAYING;
    }
    if (event.key.code == Keyboard::Num5)
    {
 // Upgrade pickup
 healthPickup.upgrade();
        state = State::PLAYING;
    }
    if (event.key.code == Keyboard::Num6)
    {
 // Upgrade pickup
 ammoPickup.upgrade();
        state = State::PLAYING;
    }
    if (state == State::PLAYING)
    {

玩家现在可以在每次清除一波僵尸时升级。 然而,我们还不能增加僵尸的数量或关卡的大小。

LEVELING_UP状态的下一部分中,就在我们刚刚添加的代码之后,修改当状态从LEVELING_UP更改为PLAYING时运行的代码。

下面是完整的代码。 我突出显示了新的或稍微修改过的行。

添加或修改下列突出显示的代码:

    if (event.key.code == Keyboard::Num6)
    {
        ammoPickup.upgrade();
        state = State::PLAYING;
    }
    if (state == State::PLAYING)
    {
 // Increase the wave number
 wave++ ;
        // Prepare the level
        // We will modify the next two lines later
 arena.width = 500 * wave;
 arena.height = 500 * wave;
        arena.left = 0;
        arena.top = 0;
        // Pass the vertex array by reference 
        // to the createBackground function
        int tileSize = createBackground(background, arena);
        // Spawn the player in the middle of the arena
        player.spawn(arena, resolution, tileSize);
        // Configure the pick-ups
        healthPickup.setArena(arena);
        ammoPickup.setArena(arena);
        // Create a horde of zombies
 numZombies = 5 * wave;
        // Delete the previously allocated memory (if it exists)
        delete[] zombies;
        zombies = createHorde(numZombies, arena);
        numZombiesAlive = numZombies;
 // Play the powerup sound
 powerup.play();
        // Reset the clock so there isn't a frame jump
        clock.restart();
    }
}// End LEVELING up

前面的代码从增加wave变量开始。 然后,修改代码,使僵尸的数量和竞技场的大小相对于新值wave。 最后,我们添加了powerup.play()的调用来播放升级音效。

重启游戏

我们已经通过变量wave的值确定了竞技场的大小和僵尸的数量。 我们还必须重置弹药和枪支相关变量,并在每款新游戏开始时将wavescore设为零。 在游戏循环的事件处理部分找到以下代码,并添加以下突出显示的代码:

// Start a new game while in GAME_OVER state
else if (event.key.code == Keyboard::Return &&
    state == State::GAME_OVER)
{
    state = State::LEVELING_UP;
 wave = 0;
 score = 0;
 // Prepare the gun and ammo for next game
 currentBullet = 0;
 bulletsSpare = 24;
 bulletsInClip = 6;
 clipSize = 6;
 fireRate = 1;
 // Reset the player's stats
 player.resetPlayerStats();
}

现在,我们可以玩这个游戏了,玩家可以变得更加强大,而僵尸会在一个越来越大的竞技场中变得越来越多——直到他们死去。 然后,游戏又重新开始。

播放其余的声音

现在,我们将添加对play函数的其余调用。 我们将单独处理每一个球员,因为准确地定位他们的去向是在正确的时间和他们比赛的关键。

在玩家重新加载时添加音效

在三个地方添加以下高亮代码,以便当玩家按下R键试图重新装填枪支时,播放适当的reloadreloadFailed声音:

if (state == State::PLAYING)
{
    // Reloading
    if (event.key.code == Keyboard::R)
    {
        if (bulletsSpare >= clipSize)
        {
            // Plenty of bullets. Reload.
            bulletsInClip = clipSize;
            bulletsSpare -= clipSize;        
 reload.play();
        }
        else if (bulletsSpare > 0)
        {
            // Only few bullets left
            bulletsInClip = bulletsSpare;
            bulletsSpare = 0;                
 reload.play();
        }
        else
        {
            // More here soon?!
 reloadFailed.play();
        }
    }
}

当玩家重新加载或尝试重新加载时,现在将得到一个声音响应。 让我们继续播放射击声音。

发出射击的声音

在处理玩家点击鼠标左键的代码末尾添加以下高亮显示的shoot.play()调用:

// Fire a bullet
if (sf::Mouse::isButtonPressed(sf::Mouse::Left))
{
    if (gameTimeTotal.asMilliseconds()
        - lastPressed.asMilliseconds()
        > 1000 / fireRate && bulletsInClip > 0)
    {
        // Pass the centre of the player and crosshair
        // to the shoot function
        bullets[currentBullet].shoot(
            player.getCenter().x, player.getCenter().y,
            mouseWorldPosition.x, mouseWorldPosition.y);
        currentBullet++ ;
        if (currentBullet > 99)
        {
            currentBullet = 0;
        }
        lastPressed = gameTimeTotal;
 shoot.play();
        bulletsInClip--;
    }
}// End fire a bullet

游戏现在将播放一个令人满意的射击声音。 接下来,我们将播放玩家被僵尸击中时的声音。

当玩家被击中时播放声音

在下面的代码中,我们将对hit.play的调用封装在一个测试中,以查看player.hit函数是否返回 true。 记住,player.hit 函数测试是否在之前的 100 毫秒内记录了一次命中。 这将产生一个快速重复的重击声音的效果,但不会太快,以至于声音模糊成一个噪音。

将调用添加到hit.play,如下代码中高亮显示:

// Have any zombies touched the player            
for (int i = 0; i < numZombies; i++)
{
    if (player.getPosition().intersects
        (zombies[i].getPosition()) && zombies[i].isAlive())
    {
        if (player.hit(gameTimeTotal))
        {
            // More here later
 hit.play();
        }
        if (player.getHealth() <= 0)
        {
            state = State::GAME_OVER;
            std::ofstream OutputFile("gamedata/scores.txt");
            OutputFile << hiScore;
            OutputFile.close();

        }
    }
}// End player touched

当僵尸触碰他们时,玩家将听到一种不祥的砰砰声,如果僵尸继续触碰他们,这种声音将以每秒 5 次的速度重复出现。 其逻辑包含在Player类的hit函数中。

拾取时播放声音

当玩家拾取生命值时,我们会播放常规拾取的声音。 然而,当玩家拿到弹药时,我们会播放装弹音效。

在适当的碰撞检测代码中添加两个调用来播放声音:

// Has the player touched health pickup
if (player.getPosition().intersects
    (healthPickup.getPosition()) && healthPickup.isSpawned())
{
    player.increaseHealthLevel(healthPickup.gotIt());
 // Play a sound
 pickup.play();

}
// Has the player touched ammo pickup
if (player.getPosition().intersects
    (ammoPickup.getPosition()) && ammoPickup.isSpawned())
{
    bulletsSpare += ammoPickup.gotIt();
 // Play a sound
 reload.play();

}

当僵尸被射杀时发出啪啪声

在检测子弹与僵尸碰撞的代码部分末尾添加一个调用splat.play:

// Have any zombies been shot?
for (int i = 0; i < 100; i++)
{
    for (int j = 0; j < numZombies; j++)
    {
        if (bullets[i].isInFlight() && 
            zombies[j].isAlive())
        {
            if (bullets[i].getPosition().intersects
                (zombies[j].getPosition()))
            {
                // Stop the bullet
                bullets[i].stop();
                // Register the hit and see if it was a kill
                if (zombies[j].hit()) {
                    // Not just a hit but a kill too
                    score += 10;
                    if (score >= hiScore)
                    {
                        hiScore = score;
                    }
                    numZombiesAlive--;
                    // When all the zombies are dead (again)
                    if (numZombiesAlive == 0) {
                        state = State::LEVELING_UP;
                    }
                }    
 // Make a splat sound
 splat.play();

            }
        }
    }
}// End zombie being shot

你现在可以玩完整的游戏,并看到僵尸的数量和竞技场增加每波。 仔细选择你的升级:

恭喜你!

总结

我们已经完成了《Zombie Arena》游戏。 这是一段相当长的旅程。 我们已经学习了一大堆 c++ 基础知识,比如引用、指针、面向对象和类。 此外,我们还使用了 SFML 来管理摄像机(视图)、顶点数组和碰撞检测。 我们学习了如何使用精灵表来减少对window.draw的调用次数并提高帧率。 使用 c++ 指针、STL 和一点 OOP,我们构建了一个单例类来管理纹理。 在下一个项目中,我们将扩展这一理念并管理所有游戏资产。

在本书的倒数第二个项目中,我们将发现粒子效果、定向声音和分屏合作游戏。 在 c++ 中,我们还会遇到继承、多态性和其他一些新概念。

常见问题解答

以下是你可能会想到的一些问题:

Q)尽管使用类,我还是发现代码变得非常长,无法管理。

A)最大的问题之一是我们代码的结构。 随着我们对 c++ 学习的深入,我们也将学习使代码更易于管理、通常更短的方法。 我们将在下一个项目和最后一个项目中这样做。 读完这本书,你就会知道一些管理代码的策略。

Q:音效看起来有点平淡和不现实。 如何改进?

A)有效改善玩家从声音中获得的感觉的一种方法是让声音具有方向性,并根据音源与玩家角色的距离改变音量。 在下一个项目中,我们将使用 SFML 的高级声音特性。