CppGuide社区 CppGuide社区
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
首页
  • 🔥最新谷歌C++风格指南(含C++17/20)
  • 🔥C++17详解
  • 🔥C++20完全指南
  • 🔥C++23快速入门
🔥C++面试
  • 第1章 C++ 惯用法与Modern C++篇
  • 第2章 C++开发工具与调试进阶
  • 第3章 C++多线程编程从入门到进阶
  • 第4章 C++网络编程重难点解析
  • 第5章 网络通信故障排查常用命令
  • 第6章 网络通信协议设计
  • 第7章 高性能服务结构设计
  • 第8章 Redis网络通信模块源码分析
  • 第9章 服务其他模块设计
  • 🚀 全部章节.pdf 下载 (opens new window)
  • 🔥C++游戏编程入门(零基础学C++)
  • 🔥使用C++17从零开发一个调试器 (opens new window)
  • 🔥使用C++20从零构建一个完整的低延迟交易系统 (opens new window)
  • 🔥使用C++从零写一个C语言编译器 (opens new window)
  • 🔥从零用C语言写一个Redis
  • leveldb源码分析
  • libevent源码分析
  • Memcached源码分析
  • TeamTalk源码分析
  • 优质源码分享 (opens new window)
  • 🔥远程控制软件gh0st源码分析
  • 🔥Windows 10系统编程
  • 🔥Linux 5.x内核开发与调试 完全指南 (opens new window)
  • TCP源码实现超详细注释版.pdf (opens new window)
  • 高效Go并发编程
  • Go性能调优
  • Go项目架构设计
  • 🔥使用Go从零开发一个数据库
  • 🔥使用Go从零开发一个编译器 (opens new window)
  • 🔥使用Go从零开发一个解释器 (opens new window)
Rust编程指南
  • SQL零基础指南
  • MySQL开发与调试指南
GitHub (opens new window)
  • C++游戏编程入门(零基础学C++) 说明
  • 第1章 欢迎阅读
  • 第2章 变量、运算符与决策:精灵动画
  • 第3章 C++字符串、SFML时间:玩家输入与平视显示器
    • 暂停和重新开始游戏
    • C++字符串
      • 声明字符串
      • 给字符串赋值
      • 字符串拼接
      • 获取字符串长度
      • 用StringStream以另一种方式操作字符串
      • SFML文本和SFML字体
    • 添加分数和消息
    • 添加时间条
    • 总结
    • 常见问题解答
  • 第4章 循环、数组、switch语句、枚举和函数:实现游戏机制
  • 第5章 碰撞、音效与结束条件:让游戏可玩
  • 第6章 面向对象编程——开始开发《乒乓》游戏
  • 第7章 AABB碰撞检测与物理效果——完成乒乓球游戏
  • 第8章 SFML视图——开启丧尸射击游戏
  • 第9章 C++引用、精灵表和顶点数组
  • 第10章 指针、标准模板库和纹理管理
  • 第11章 编写TextureHolder类并创建一群僵尸
  • 第12章 碰撞检测、拾取物与子弹
  • 第13章 视图分层与平视显示器(HUD)的实现
  • 第14章 音效、文件输入输出与完成游戏制作
  • 第15章 快跑!
  • 第16章 声音、游戏逻辑、对象间通信与玩家
  • 第17章 图形、相机与动作
  • 第18章 为平台、玩家动画和控制功能编写代码
  • 第19章 构建菜单与实现下雨效果
  • 第20章 火球与声音空间化
  • 第21章 视差背景与着色器
目录

第3章 C++字符串、SFML时间:玩家输入与平视显示器

# 第3章 C++字符串、SFML时间:玩家输入与平视显示器

几乎每一款游戏都需要在屏幕上显示一些文本,比如分数、角色的台词等等。因此,在本章中,我们将用大约一半的时间学习如何处理文本并在屏幕上显示,另一半时间则用于研究计时,以及如何通过可视化的时间条告知玩家剩余时间,从而在游戏中营造出紧迫感。

我们将涵盖以下内容:

  • 暂停和重新开始游戏
  • C++字符串
  • SFML文本和字体
  • 添加分数和消息
  • 添加时间条

在接下来的三章中,随着游戏开发的推进,代码会越来越长。所以,现在正是提前规划并为代码增加一些结构的好时机。我们添加这种结构,以便实现暂停和重新开始游戏的功能。

# 暂停和重新开始游戏

我们将添加代码,使游戏在首次运行时处于暂停状态。玩家可以按下回车键开始游戏,之后游戏会一直运行,直到玩家被压扁或者时间耗尽。

此时,游戏将再次暂停,等待玩家按下回车键重新开始。

让我们逐步进行设置。首先,在主游戏循环之外声明一个新的布尔(bool)变量paused,并将其初始化为true。

//  Variables  to  control  time  itself
Clock clock;

//  Track  whether  the  game  is  running
bool   paused   =   true ;

while (window.isOpen()) {
/*
****************************************
Handle  the players  input
****************************************
*/
1
2
3
4
5
6
7
8
9
10
11
12

现在,每当游戏运行时,我们有一个变量paused,其初始值为true。

接下来,我们添加另一个if语句,该语句的表达式将检查回车键当前是否被按下。如果按下,则将paused设置为false。在其他键盘处理代码之后添加突出显示的代码。

/*
****************************************
Handle  the players  input
****************************************
*/
if (Keyboard::isKeyPressed(Keyboard::Escape)) {
    window.close();
}

// Start  the  game
if  (Keyboard::isKeyPressed(Keyboard::Return))
{
    paused   =  false ;
}

/*
****************************************
Update  the  scene
****************************************
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

现在我们有了一个名为paused的布尔变量,初始值为true,但当玩家按下回车键时会变为false。此时,我们必须让游戏循环根据paused当前的值做出适当的响应。

我们将这样处理:把代码的整个更新部分,包括上一章编写的移动蜜蜂和云朵的代码,都放在一个if语句中。

注意,在接下来的代码中,if块只有在paused不等于true时才会执行。换句话说,游戏在暂停时不会移动或更新。我们也可以用类似的if语句包裹绘制代码,这样可以防止场景被绘制到屏幕上。我们知道,大多数游戏在暂停时,动作暂停但场景仍然可见,这正是我们想要的效果。

仔细查看添加新if语句及其对应的花括号{...}的准确位置。如果放错位置,程序将无法按预期运行。

添加突出显示的代码来包裹代码的更新部分,注意下面显示的上下文。我在几行代码中添加了省略号...来表示未显示的代码。当然,...不是真正的代码,不应添加到游戏中。你可以根据周围未突出显示的代码来确定新代码(突出显示部分)在开头和结尾的放置位置。

/*
****************************************
Update  the  scene
****************************************
*/
if   (!paused) {
    // Measure  time
   ...
   ...
   ...

    // Has  the  cloud  reached  the  right  hand  edge  of  the  screen?
    if (spriteCloud3.getPosition().x > 1920) {
        // Set  it  up  ready  to  be  a  whole  new  cloud  next frame
        cloud3Active = false;
    }
}  //  End  if(!paused)

/*
****************************************
Draw  the  scene
****************************************
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

注意,当你放置新if块的结束花括号时,Visual Studio会自动调整缩进,使代码保持整洁。不过,这可能因Visual Studio的设置而异。如果if块内的代码没有向右缩进一个制表符的距离,你可以像在任何文本应用程序中一样,通过点击并拖动鼠标选中if块内的所有代码,然后按键盘上的制表符(Tab键),这样代码就会整齐地缩进。

现在你可以运行游戏,在按下回车键之前,一切都是静止的。现在我们可以开始为游戏添加功能了,只需记住,当玩家死亡或时间耗尽时,需要将paused变量设置为true。

在上一章中,我们初步了解了C++字符串。为了实现玩家的平视显示器(HUD),我们还需要进一步学习字符串的相关知识。

# C++字符串

在上一章中,我们简要提到了字符串,并了解到字符串可以存储字母数字数据,范围从单个字符到整本书的内容都可以。但我们没有深入学习如何声明、初始化或操作字符串,现在就来学习一下。

# 声明字符串

声明一个字符串变量很简单,先指定类型,再跟上变量名。

String levelName;
String playerName;
1
2

声明完字符串变量后,我们就可以给它赋值。

# 给字符串赋值

给字符串赋值和给普通变量赋值一样,只需写出变量名,接着是赋值运算符,然后是要赋的值。

levelName = "Dastardly Cave";
playerName = "John Carmack";
1
2

注意,值需要用引号括起来。和普通变量一样,我们也可以在一行内完成声明和赋值。

String score = "Score = 0";
String message = "GAME OVER!!";
1
2

为了内容的完整性,我要提一下,也可以使用统一初始化的方式声明和初始化字符串,就像我们在第2章讨论的那样,如下所示:

//  Using  uniform  initialization for  a  string
string playerName{"Rob Hubbard"};
1
2

在游戏开发中,C++字符串对于处理基于文本的数据至关重要。无论是像上面提到的显示玩家名字、显示消息,还是记录谁获得了最高分,了解如何使用字符串都非常有用。让我们从字符串拼接开始,进一步探索字符串的用法。

# 字符串拼接

在下面的代码示例中,我们使用C++的cout将文本输出到控制台窗口。你可以将代码复制粘贴到当前项目主函数的起始花括号内来尝试运行,如果你想单独测试,也可以新建一个项目。如果新建项目,无需添加第1章中配置SFML的那些代码,只需创建一个控制台应用程序,取个名字,将代码粘贴到主函数中,并添加#include <iostream>和#include <string>这两个头文件,以实现字符串和cout的功能。下面是代码,你可以试试,或者先看看,然后我们再讨论。

//  Before  the main function
#include <iostream>
#include <string>

//  Inside  the main function
std::string playerName = "Player1";
std::string message = "Welcome to the game, " + playerName + "!";
std::cout << message << std::endl;
1
2
3
4
5
6
7
8

在前面的代码中,我们展示了如何在C++中创建和操作字符串。它初始化了一个名为playerName的变量,并构造了一个包含玩家名字的字符串message,然后使用std::cout将其显示在屏幕上。注意,在中间那行代码中,我们使用+运算符拼接(连接)字符串。

注意,和SFML中的s一样,你可以在包含指令之后添加一行代码using namespace std;,这样就可以省略所有的std::。

关于字符串还有很多可以探索的地方,让我们继续。

# 获取字符串长度

在下面的代码中,我们进一步深入了解字符串的用法,并使用length函数。这里我们稍微超前了一点,因为这展示了如何在类的实例上调用函数,但正如你所见,它非常直观。

string playerName = "Player1";
int playerNameLength = playerName.length();
cout << "Player name has " << playerNameLength << " characters." << endl;
1
2
3

在前面的代码中,我省略了上一个示例中出现的所有std::说明符,所以如果你想在Visual Studio中运行这段代码,需要在包含指令之后添加using namespace std语法。

在前面的代码中,我们声明并初始化了一个字符串和一个整数。然后使用length()函数返回字符串中的字符数,并将结果存储在playerNameLength变量中,该变量的类型为int。接着,我们使用cout将结果打印到控制台窗口。

很明显,<<用于连接输出的各个部分。<<是一个按位运算符,但你可能想进一步了解它。

<<运算符是按位运算符之一。不过,C++允许你编写自己的类,并在类的上下文中重写特定运算符的功能。iostream类就是这样做的,从而使<<运算符能按我们看到的方式工作。其复杂性隐藏在类中,我们可以使用它的功能,而无需担心其内部实现原理。

我们差不多准备好为游戏添加更多功能了。首先,让我们看看如何用另一种方式修改字符串变量。

# 用StringStream以另一种方式操作字符串

我们可以使用#include <sstream>指令,为操作字符串增添更多能力。sstream类使我们能够“相加”一些字符串,这是另一种拼接字符串的方式。

String part1 = "Hello ";
String part2 = "World";

sstream ss;
ss << part1 << part2;

//  ss  now  holds  "Hello  World"
1
2
3
4
5
6
7

除此之外,使用sstream对象,字符串变量甚至可以和不同类型的变量进行拼接。下面的代码开始展示字符串对我们的用处。

String scoreText = "Score = ";
int score = 0;

//  Later  in  the  code
score ++;

sstream ss;
ss << scoreText << score;

//  ss  now  holds  "Score  =  1"
1
2
3
4
5
6
7
8
9
10

现在我们了解了C++字符串的基础知识,以及如何使用sstream,接下来看看如何使用一些SFML类将字符串显示在屏幕上。

# SFML文本和SFML字体

在实际为游戏添加代码之前,让我们通过一些假设的代码来了解一下SFML的Text和Font类。

在屏幕上绘制文本的第一步是要有字体。在第1章中,我们向项目文件夹添加了一个字体文件。现在我们可以将字体加载到一个SFML的Font对象中,以备使用。

实现这一功能的代码如下:

Font font;
font.loadFromFile("myfont.ttf");
1
2

在前面的代码中,我们首先声明一个Font对象,然后将一个实际的字体文件加载到其中。注意,myfont.ttf是一个假设的字体文件,我们可以使用项目文件夹中的任何字体。

加载字体后,我们需要一个SFML的Text对象。

Text myText;
1

现在我们可以配置Text对象,包括设置文本大小、颜色、在屏幕上的位置、要显示的消息字符串,当然还有将其与我们的字体对象关联起来。

// Assign  the  actual message
myText.setString("Press Enter to start!");

//  assign  a  size
myText.setCharacterSize(75);

//  Choose  a  color
myText.setFillColor(Color::White);

// Set  the font  to  our  Text  object
myText.setFont(font);
1
2
3
4
5
6
7
8
9
10
11

这里值得稍微插几句。在介绍到目前为止使用的几乎每个SFML类之后,我都可以这样插几句。怎么强调SFML这个出色的库为我们节省的工作量都不为过,Font和Text类就是两个很好的例子。

SFML在“幕后”所做的,是为处理字体和文本渲染提供了非常简化的抽象,与直接使用OpenGL处理这些任务相比,大大降低了难度。

SFML中的Font类表示一种可用于渲染文本的字体,它提供了从文件、内存缓冲区或系统字体加载字体的函数。Text类负责使用给定的字体渲染文本,它封装了要显示的字符串、字体以及各种与文本相关的属性。

SFML几乎将使用OpenGL渲染文本所涉及的所有复杂性都抽象掉了,它在幕后处理纹理创建、着色器管理和其他OpenGL细节。使用SFML进行文本渲染极大地简化了直接使用OpenGL的复杂操作,让我们能够更专注于游戏开发,而不是OpenGL的底层数学运算。

SFML是由洛朗·戈米拉(Laurent Gomila)创建的。SFML的开发始于2006年左右,多年来经过了多次更新和改进。洛朗近二十年来对维护SFML的奉献精神怎么强调都不为过,在我看来,这令人惊叹。我只是觉得应该提一下,这样每次你轻松地在屏幕上绘制一个精灵时,就能想到背后付出的不懈努力。

现在我们已经掌握了足够多的知识,可以为游戏添加一些功能了。让我们给《Timber!!!》添加一个平视显示器(HUD)。

# 添加分数和消息

现在,我们对字符串、SFML文本(SFML Text)和SFML字体(SFML Font)有了足够的了解,可以着手实现平视显示器(HUD,Heads-Up Display)了。HUD最初指的是飞机驾驶舱内的仪表显示,飞行员无需低头查看。在电子游戏中,尤其是游戏内的用户界面,通常也被称为HUD,因为它们与驾驶舱HUD的功能相同。

接下来,我们需要在代码文件顶部添加另一个#include指令。我们已经了解到,sstream类提供了一些有用的功能,可以将字符串和其他变量类型组合成一个字符串。

添加以下突出显示的代码行。

#include <sstream>
#include <SFML/Graphics.hpp>
using namespace sf;

int main() {
1
2
3
4
5

接下来,我们将设置SFML文本对象:一个用于显示根据游戏状态变化的消息,另一个用于显示分数,并且需要定期更新。

下面的代码声明了文本(Text)和字体(Font)对象,加载字体,将字体分配给文本对象,然后添加字符串消息、颜色和大小。在上一节的讨论中,这些操作应该看起来很熟悉。此外,我们添加了一个新的整型变量score,用于记录玩家的分数。

请记住,如果你在第1章《欢迎来到C++游戏编程入门(第三版)》中选择了与KOMIKAP_.ttf不同的字体,那么你需要更改代码中相应的部分,使其与你在Visual Studio Stuff/Projects/Timber/fonts文件夹中的.ttf文件匹配。

添加突出显示的代码后,我们就可以继续更新HUD了。

// 跟踪游戏是否正在运行
bool paused = true;

// 绘制一些文本
int score = 0;

Text messageText;
Text scoreText;

// 我们需要选择一种字体
Font font;
font.loadFromFile("fonts/KOMIKAP_.ttf");

// 将字体设置到我们的消息文本上
messageText.setFont(font);
scoreText.setFont(font);

// 分配实际的消息内容
messageText.setString("Press Enter to start!");
scoreText.setString("Score = 0");

// 把字体设置得很大
messageText.setCharacterSize(75);
scoreText.setCharacterSize(100);

// 选择一种颜色
messageText.setFillColor(Color::White);
scoreText.setFillColor(Color::White);

while (window.isOpen()) {
    /*
    ****************************************
    处理玩家输入
    ****************************************
    */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

接下来的代码可能看起来有点复杂,甚至有些费解。但是,只要稍微分解一下,就会发现其实很简单。仔细查看并添加新代码,然后我们再来详细讲解。

// 选择一种颜色
messageText.setFillColor(Color::White);
scoreText.setFillColor(Color::White);

// 定位文本
FloatRect textRect = messageText.getLocalBounds();

messageText.setOrigin(textRect.left + textRect.width / 2.0f,
                      textRect.top + textRect.height / 2.0f);

messageText.setPosition(1920 / 2.0f, 1080 / 2.0f);
scoreText.setPosition(20, 20);

while (window.isOpen()) {
    /*
    ****************************************
    处理玩家输入
    ****************************************
    */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

我们有两个Text类型的对象要显示在屏幕上。我们希望将scoreText定位在屏幕左上角,并留出一点边距。这并不难,我们只需使用scoreText.setPosition(20, 20),这样它就会在左上角,水平和垂直方向都留出20像素的边距。

然而,定位messageText就没那么容易了。我们想把它定位在屏幕的正中间。一开始,这似乎不是问题,但我们要记住,我们绘制的所有内容的原点都在左上角。所以,如果我们只是将屏幕的宽度和高度除以2,然后在messageText.setPosition...中使用结果,那么文本的左上角就会在屏幕中心,而文本会向右杂乱地散开。

我们需要一种方法,将messageText的中心设置到屏幕的中心。你刚刚添加的那段看起来有点复杂的代码,就是将messageText的原点重新定位到它自身的中心。为了方便,下面再次给出这段代码。

// 定位文本
FloatRect textRect = messageText.getLocalBounds();

messageText.setOrigin(textRect.left + textRect.width / 2.0f,
                      textRect.top + textRect.height / 2.0f);
1
2
3
4
5

在这段代码中,我们首先声明了一个新的FloatRect类型的对象textRect。顾名思义,FloatRect对象用于存储一个带有浮点坐标的矩形。

然后,代码使用messageText.getLocalBounds函数,用包围messageText的矩形坐标来初始化textRect。

接下来的代码由于比较长,分了四行书写。它使用messageText.setOrigin函数,将原点(用于绘制的点)更改为textRect的中心。当然,textRect存储的矩形坐标与包围messageText的矩形坐标完全匹配。然后,执行下一行代码:

messageText.setPosition(1920 / 2.0f, 1080 / 2.0f);
1

现在,messageText就会整齐地定位在屏幕的正中心。每次我们更改messageText的文本内容时,都要使用这段代码,因为更改消息会改变messageText的大小,所以需要重新计算它的原点。

接下来,我们声明一个stringstream类型的对象ss。注意,我们使用了包含命名空间的完整名称std::stringstream。我们也可以在代码文件顶部添加using namespace std来避免这种写法,但我们很少使用stringstream,所以不这样做。查看下面的代码并添加到游戏中,然后我们再详细讲解。因为我们只希望这段代码在游戏未暂停时执行,所以一定要像下面这样,将它与其他代码一起添加到if(!paused)代码块内。

else
{
    spriteCloud3.setPosition(
        spriteCloud3.getPosition().x + (cloud3Speed * dt.asSeconds()),
        spriteCloud3.getPosition().y);

    // 云朵是否到达了屏幕的右边缘?
    if (spriteCloud3.getPosition().x > 1920) {
        // 为下一帧设置它,使其成为一朵全新的云朵
        cloud3Active = false;
    }
}

// 更新分数文本
std::stringstream ss;
ss << "Score = " << score;
scoreText.setString(ss.str());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

我们使用ss和<<运算符提供的特殊功能,它可以将变量连接到stringstream中。所以,代码ss << "Score = " << score的作用是创建一个包含"Score = "的字符串,并且将score的值连接到后面。例如,游戏刚开始时,score等于0,所以ss将保存值"Score = 0"。如果score发生变化,ss会在每一帧进行相应的调整。

下一行代码只是将ss中包含的字符串显示/设置到scoreText中。

scoreText.setString(ss.str());
1

现在,它已经准备好绘制到屏幕上了。

接下来的代码绘制两个文本对象(scoreText和messageText),但请注意,绘制messageText的代码包含在一个if语句中。这个if语句使得messageText只在游戏暂停时绘制。

添加下面突出显示的代码。

// 现在绘制昆虫
window.draw(spriteBee);

// 绘制分数
window.draw(scoreText);

if (paused) {
    // 绘制我们的消息
    window.draw(messageText);
}

// 显示我们刚刚绘制的所有内容
window.display();
1
2
3
4
5
6
7
8
9
10
11
12
13

现在我们可以运行游戏,看到HUD显示在屏幕上。你会看到SCORE = 0和PRESS ENTER TO START!的消息。按下回车键后,后者会消失。

img 图3.1:运行中的HUD

如果你想看到分数更新,可以在while(window.isOpen)循环中的任意位置添加一行临时代码score++;。如果你添加了这行临时代码,你会看到分数快速上升,非常快!

img 图3.2:分数

如果你添加了临时代码score++;,在继续之前一定要删除它。

# 添加时间条

时间是游戏中的一个关键机制,有必要让玩家清楚时间情况。他们需要知道自己的6秒倒计时是否即将结束。在游戏接近尾声时,这会给他们带来紧迫感;如果他们表现出色,能够维持或增加剩余时间,也会让他们有成就感。

在屏幕上直接显示剩余秒数,玩家在专注砍树枝时并不容易看清,而且也不是实现这一目标的有趣方式。

我们需要的是一个时间条。我们的时间条将是一个简单的红色矩形,醒目地显示在屏幕上。它一开始又宽又大,但随着时间流逝会迅速缩小。当玩家的剩余时间达到0时,时间条会完全消失。

在添加时间条的同时,我们还要添加必要的代码来跟踪玩家的剩余时间,并在时间耗尽时做出相应反应。让我们一步一步来。

找到之前声明的Clock clock;,在其后添加突出显示的代码,如下所示。

// 控制时间的变量
Clock clock;

// 时间条
RectangleShape timeBar;
float timeBarStartWidth = 400;
float timeBarHeight = 80;
timeBar.setSize(Vector2f(timeBarStartWidth, timeBarHeight));
timeBar.setFillColor(Color::Red);
timeBar.setPosition((1920 / 2) - timeBarStartWidth / 2, 980);

Time gameTimeTotal;
float timeRemaining = 6.0f;
float timeBarWidthPerSecond = timeBarStartWidth / timeRemaining;

// 跟踪游戏是否正在运行
bool paused = true;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

首先,我们声明一个RectangleShape类型的对象,并将其命名为timeBar。RectangleShape是SFML库中的一个类,非常适合绘制简单的矩形。

接下来,我们添加了两个float类型的变量timeBarStartWidth和timeBarHeight,并分别初始化为400和80。这些变量将帮助我们确定每帧绘制timeBar所需的大小。

然后,我们使用timeBar.setSize函数设置timeBar的大小。我们没有直接传入这两个新的float变量,而是先创建了一个Vector2f类型的新对象。这里的不同之处在于,我们没有给这个新对象命名,只是用两个float变量对其进行初始化,然后直接将其传入setSize函数。

Vector2f是一个包含两个float变量的类,本书还会介绍它的其他功能。

之后,我们使用setFillColor函数将timeBar的颜色设置为红色。

在上段代码中,我们对timeBar做的最后一件事是设置它的位置。垂直坐标的设置很直接,但水平坐标的设置方式有点复杂。计算方式如下:

(1920 / 2) - timeBarStartWidth / 2
1

代码先将1920除以2,再将timeBarStartWidth除以2,最后用前者减去后者。

这样的计算结果能让timeBar在屏幕水平方向上整齐地居中显示。

我们刚刚讨论的最后三行代码声明了一个新的Time对象gameTimeTotal、一个新的float变量timeRemaining(初始化为6),以及一个名字有点奇怪的float变量timeBarWidthPerSecond,接下来我们会进一步讨论它。

timeBarWidthPerSecond变量是用timeBarStartWidth除以timeRemaining进行初始化的。结果正好是游戏每秒钟timeBar需要缩小的像素数。这在游戏循环的每一帧中调整timeBar大小时会很有用。

显然,每次玩家开始新游戏时,我们都需要重置剩余时间。合理的做法是在玩家按下回车键时进行重置,同时我们还可以将分数重置为0。现在,添加突出显示的代码来实现这一点。

// 开始游戏
if (Keyboard::isKeyPressed(Keyboard::Return)) {
    paused = false;

    // 重置时间和分数
    score = 0;
    timeRemaining = 6;
}
1
2
3
4
5
6
7
8

现在,在每一帧中,我们都必须减少剩余时间,并相应地调整timeBar的大小。在更新部分添加以下突出显示的代码,如下所示。

/*
****************************************
更新场景
****************************************
*/
if (!paused) {
    // 测量时间
    Time dt = clock.restart();

    // 从剩余时间中减去
    timeRemaining -= dt.asSeconds();
    // 调整时间条大小
    timeBar.setSize(Vector2f(timeBarWidthPerSecond * timeRemaining, timeBarHeight));

    // 设置蜜蜂
    if (!beeActive) {
        // 蜜蜂速度
        srand((int)time(0) * 10);
        beeSpeed = (rand() % 200) + 200;

        // 蜜蜂高度
        srand((int)time(0) * 10);
        float height = (rand() % 1350) + 500;
        spriteBee.setPosition(2000, height);
        beeActive = true;
    }
    else
        // 移动蜜蜂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

在上述代码中,首先我们用下面这行代码,根据上一帧的执行时长来减少玩家的剩余时间:

timeRemaining -= dt.asSeconds();
1

然后,我们用以下代码调整timeBar的大小:

timeBar.setSize(Vector2f(timeBarWidthPerSecond * timeRemaining, timeBarHeight));
1

Vector2F的x值是用timebarWidthPerSecond乘以timeRemaining进行初始化的。这会根据玩家剩余的时间,精确地得出正确的宽度。高度保持不变,timeBarHeight没有经过任何处理就直接使用。

当然,我们必须检测时间是否耗尽。目前,我们只是检测时间耗尽的情况,暂停游戏,并更改messageText的文本内容。之后,我们会在这里做更多工作。在刚刚添加的代码之后添加突出显示的代码,我们会更详细地分析它。

// 测量时间
Time dt = clock.restart();

// 从剩余时间中减去
timeRemaining -= dt.asSeconds();

// 调整时间条大小
timeBar.setSize(Vector2f(timeBarWidthPerSecond * timeRemaining, timeBarHeight));

if (timeRemaining <= 0.0f) {
    // 暂停游戏
    paused = true;

    // 更改显示给玩家的消息
    messageText.setString("Out of time!!");

    // 根据新大小重新定位文本
    FloatRect textRect = messageText.getLocalBounds();
    messageText.setOrigin(textRect.left + textRect.width / 2.0f, textRect.top + textRect.height / 2.0f);
    messageText.setPosition(1920 / 2.0f, 1080 / 2.0f);
}

// 设置蜜蜂
if (!beeActive) {
    // 蜜蜂速度
    srand((int)time(0) * 10);
    beeSpeed = (rand() % 200) + 200;

    // 蜜蜂高度
    srand((int)time(0) * 10);
    float height = (rand() % 1350) + 500;
    spriteBee.setPosition(2000, height);
    beeActive = true;
}
else
    // 移动蜜蜂
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

逐步分析上述代码:

  • 首先,我们用if(timeRemaining <= 0.0f)检测时间是否耗尽。
  • 然后,将paused设置为true,这样这将是代码更新部分最后一次执行(直到玩家再次按下回车键)。
  • 接着,我们更改messageText的消息内容,计算其新的中心位置并将其设置为原点,然后将其定位在屏幕中心。

最后,对于这部分代码,我们需要绘制timeBar。这段代码中没有我们没见过的新内容。只需注意,我们在绘制树之后绘制timeBar,这样它就不会被部分遮挡。添加突出显示的代码来绘制时间条。

// 绘制分数
window.draw(scoreText);

// 绘制时间条
window.draw(timeBar);

if (paused) {
    // 绘制我们的消息
    window.draw(messageText);
}

// 显示我们刚刚绘制的所有内容
window.display();
1
2
3
4
5
6
7
8
9
10
11
12
13

现在你可以运行游戏,按下回车键开始,然后看着时间条平稳地缩小直至消失。

img 图3.3:时间条消失

然后游戏暂停,“OUT OF TIME!!”的消息会整齐地出现在屏幕中央。

img 图3.4:时间结束

当然,你可以再次按下回车键,让游戏重新开始。

# 总结

在本章中,我们学习了字符串、SFML的Text和Font。通过它们,我们能够在屏幕上绘制文本,为玩家提供了一个平视显示器(HUD)。我们还使用了sstream,它允许我们连接字符串和其他变量来显示分数。

我们探索了SFML的RectangleShape类,它正如其名。我们使用RectangleShape类型的对象和一些精心设计的变量,绘制了一个时间条,向玩家显示他们还剩多少时间。一旦我们实现了砍树枝和移动树枝压扁玩家的功能,时间条将营造出紧张感和紧迫感。

接下来,我们将学习一系列新的C++特性,包括循环、数组、开关语句、枚举和函数。这将使我们能够移动树枝、跟踪它们的位置,并实现压扁玩家的效果。

# 常见问题解答

**问:**我预见到按精灵的左上角来定位有时可能不太方便。有其他方法吗?

**答:**幸运的是,你可以选择精灵的哪个点作为定位点/原点像素,就像我们对messageText所做的那样,使用setOrigin函数。

**问:**代码变得相当长了,我很难搞清楚各个部分的位置。我们怎么解决这个问题呢?

**答:**我同意你的看法。在下一章中,我们将学习一些整理代码、提高代码可读性的方法。在学习编写C++函数时,我们会看到相关内容。此外,在学习C++数组时,我们将学习一种处理相同类型的多个对象/变量(比如云朵)的新方法。

**问:**我的字体加载失败了。我怎么知道背后发生了什么?我怎么知道我输入的文件路径是否正确,或者字体文件名是否拼写错误呢?

**答:**我们可以将字体加载代码放在if语句中,并使用cout包含一些错误处理代码。例如:

if (!font.loadFromFile("arial.ttf")) {
    // 如果加载失败,显示错误消息
    cout << "Error loading font!";
}
1
2
3
4

现在,如果字体加载失败,程序会继续运行,但会缺少文本,同时你会在控制台看到一条错误消息通知你。加载纹理时也可以这样做,如下代码所示:

if (!texture.loadFromFile("texture.png")) {
    // 如果加载失败,显示错误消息
    cout << "Error loading texture!";
}
1
2
3
4
第2章 变量、运算符与决策:精灵动画
第4章 循环、数组、switch语句、枚举和函数:实现游戏机制

← 第2章 变量、运算符与决策:精灵动画 第4章 循环、数组、switch语句、枚举和函数:实现游戏机制→

最近更新
01
C++语言面试问题集锦 目录与说明
03-27
02
第四章 Lambda函数
03-27
03
第二章 关键字static及其不同用法
03-27
更多文章>
Copyright © 2024-2025 沪ICP备2023015129号 张小方 版权所有
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式