我们快到了。 这个简短的章节将演示如何使用 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
的新文件。 在这个文件中,我们将保存玩家的高分。 您可以轻松地打开该文件并添加一个分数。 如果是,请确保它是一个相当低的分数,以便我们可以很容易地测试是否超过该分数会导致添加新的分数。 一定要关闭文件,一旦你完成它,否则游戏将无法访问它。
在下面的代码中,我们将创建一个名为inputFile
的ifstream
对象,并将刚才创建的文件夹和文件作为参数发送给它的构造函数。
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 的块中,我们需要创建一个名为outputFile
的ofstream
对象,将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
你可以玩游戏,你的高分将被保存。 退出游戏,并注意到如果你再次玩游戏,你的高分仍然存在。
让我们制造一些噪音。
在本节中,我们将创建所有的SoundBuffer
和Sound
对象,我们需要为游戏添加一系列的声音效果。
首先添加所需的 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"
现在,继续加入七SoundBuffer
和Sound
对象加载和准备七个声音文件,我们准备在第八章【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
的值确定了竞技场的大小和僵尸的数量。 我们还必须重置弹药和枪支相关变量,并在每款新游戏开始时将wave
和score
设为零。 在游戏循环的事件处理部分找到以下代码,并添加以下突出显示的代码:
// 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键试图重新装填枪支时,播放适当的reload
或reloadFailed
声音:
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 的高级声音特性。