 第13章 视图分层与平视显示器(HUD)的实现
第13章 视图分层与平视显示器(HUD)的实现
  # 第13章 视图分层与平视显示器(HUD)的实现
在本章中,我们将见识到SFML视图的真正价值。我们会添加一系列SFML文本对象,并像在之前的《Timber!!!》项目和《Pong》项目中那样对它们进行操作。新的内容是,我们将使用第二个视图实例来绘制平视显示器(HUD,Head-Up Display)。这样一来,无论背景、玩家、僵尸以及其他游戏对象处于何种状态,平视显示器都能整齐地定位在主要游戏活动的上方。
本章我们将完成以下内容:
- 添加所有文本和HUD对象
- 更新HUD
- 绘制HUD、主界面和升级界面
# 添加所有文本和HUD对象
本章中,我们会对一些字符串进行操作。这样做是为了能够用必要的文本格式化HUD和升级界面。
在ZombieArena.cpp文件中添加额外的包含指令,如下代码中突出显示部分,以便我们能创建一些sstream对象来实现这一目的:
#include   <sstream>
#include <SFML/Graphics.hpp> 
#include "ZombieArena.h"
#include "Player.h"
#include "TextureHolder.h" 
#include "Bullet.h"
#include "Pickup.h" 
using namespace sf;
2
3
4
5
6
7
8
接下来,添加这段相当长但很好理解的代码。为了方便确定添加代码的位置,新代码会突出显示,现有代码则不突出:
int score = 0;  
int hiScore = 0;
//  For  the  home/game  over  screen
Sprite   spriteGameOver;
Texture  textureGameOver  =  TextureHolder::GetTexture("graphics/background.png");
spriteGameOver.setTexture(textureGameOver);   
spriteGameOver.setPosition (0 ,  0 );
//  Create  a  view for  the HUD
View  hudView(sf::FloatRect(0,  0 ,   1920 ,1080 )) ;
//  Create  a  sprite for  the  ammo  icon
Sprite   spriteAmmoIcon;
Texture  textureAmmoIcon  =   TextureHolder::GetTexture("graphics/ammo_icon.png");
spriteAmmoIcon.setTexture(textureAmmoIcon);   
spriteAmmoIcon.setPosition (20 ,  980 );
//  Load  the font
Font  font;
font.loadFromFile ("fonts/zombiecontrol.ttf");
// Paused
Text   pausedText;
pausedText.setFont (font);
pausedText.setCharacterSize (155 );
pausedText.setFillColor(Color::White);   
pausedText.setPosition (400 ,  400 );
pausedText.setString("Press  Enter  \nto   continue");
// Game  Over
Text   gameOverText;
gameOverText.setFont (font);
gameOverText.setCharacterSize (125 );
gameOverText.setFillColor(Color::White);   
gameOverText.setPosition (250 ,  850 );
gameOverText.setString("Press  Enter  to   play");
//  LEVELING  up
Text   levelUpText;
levelUpText.setFont (font);
levelUpText.setCharacterSize (80 );
levelUpText.setFillColor(Color::White);   
levelUpText.setPosition (150 ,  250 );
std::stringstream  levelUpStream;  
levelUpStream   <<
    "1 -   Increased   rate  of  fire"   <<
    "\n2 -  Increased   clip   size(next   reload)"   <<    
    "\n3 -   Increased  max   health"   <<
    "\n4 -   Increased   run   speed"   <<
    "\n5 -  More   and   better   health   pickups"   <<
    "\n6 -  More   and   better   ammo   pickups" ;
levelUpText.setString(levelUpStream.str());
// Ammo
Text   ammoText;
ammoText.setFont (font);
ammoText.setCharacterSize (55 );
ammoText.setFillColor(Color::White);   
ammoText.setPosition (200 ,  980 );
// Score
Text   scoreText;
scoreText.setFont (font);
scoreText.setCharacterSize (55 );
scoreText.setFillColor(Color::White);   
scoreText.setPosition (20 ,  0 );
// Hi Score
Text  hiScoreText;
hiScoreText.setFont (font);
hiScoreText.setCharacterSize (55 );
hiScoreText.setFillColor(Color::White);   
hiScoreText.setPosition (1400 ,  0 );
std::stringstream  s;
s   <<   "Hi   Score:"   <<   hiScore;
hiScoreText.setString(s.str());
// Zombies  remaining
Text   zombiesRemainingText;
zombiesRemainingText.setFont (font);
zombiesRemainingText.setCharacterSize (55 );
zombiesRemainingText.setFillColor(Color::White);   
zombiesRemainingText.setPosition (1500 ,  980 );
zombiesRemainingText.setString("Zombies:  100" );
// Wave  number
int  wave   =   0;
Text  waveNumberText;
waveNumberText.setFont (font);
waveNumberText.setCharacterSize (55 );
waveNumberText.setFillColor(Color::White);   
waveNumberText.setPosition (1250 ,  980 );
waveNumberText.setString("Wave:  0" );
// Health  bar
RectangleShape   healthBar;
healthBar.setFillColor(Color::Red); 
healthBar.setPosition (450 ,  980 );
//  The  main  game  loop
while (window.isOpen())
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
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
前面的代码非常简单,没有什么新内容。它主要创建了大量的SFML文本对象,为它们指定颜色和大小,然后使用我们之前见过的函数来设置它们的位置。
需要注意的最重要的一点是,我们创建了另一个名为hudView的视图对象,并将其初始化为适合屏幕分辨率的大小。
如我们所见,主视图对象会随着玩家的移动而滚动。相比之下,我们永远不会移动hudView。这样做的结果是,如果在绘制HUD元素之前切换到这个视图,就会产生一种效果,即游戏世界在下方滚动,而玩家的HUD保持静止。
打个比方,你可以想象在电视屏幕上放置一张写有文字的透明塑料片。电视会正常播放动态画面,而塑料片上的文字会保持在原位,无论下方播放的内容是什么。在下一个项目中,当我们创建一个具有移动游戏世界视图的平台游戏时,我们会进一步拓展这个概念。
然而,接下来要注意的是,最高分并没有以任何有意义的方式设置。我们需要等到下一章研究文件输入/输出(I/O,Input/Output)时,才能保存和读取最高分。
另一个值得注意的点是,我们声明并初始化了一个名为healthBar的RectangleShape(矩形形状)对象,它将直观地表示玩家剩余的生命值。它的工作方式与《Timber!!!》项目中的时间条几乎相同,只是它代表的是生命值而不是时间。
在前面的代码中,有一个新的Sprite(精灵)实例ammoIcon,它为我们将在屏幕左下角绘制在其旁边的子弹和弹夹统计信息提供了背景说明。
虽然我们刚刚添加的大量代码并没有什么新的技术内容,但一定要熟悉其中的细节,尤其是变量名,这样本章后面的内容会更容易理解。
现在,让我们来了解一下如何更新HUD变量。
# 更新HUD
正如你可能预期的那样,我们将在代码的更新部分更新HUD变量。然而,我们不会在每一帧都这样做。原因是没有必要,而且这样做还会拖慢游戏循环的速度。
例如,假设玩家杀死一个僵尸并获得了更多分数。显示分数的文本对象是在千分之一秒、百分之一秒,甚至十分之一秒内更新,这对玩家来说并没有什么区别。这意味着没有必要在每一帧都重新构建我们为文本对象设置的字符串。
因此,我们可以安排更新HUD的时间和频率。添加以下突出显示的变量:
// Health  bar
RectangleShape healthBar;
healthBar.setFillColor(Color::Red); 
healthBar.setPosition(450, 980);
// When  did  we  last  update  the  HUD?
int  framesSinceLastHUDUpdate   =   0;
// How often  (in frames)  should  we  update  the HUD
int  fpsMeasurementFrameInterval   =   1000;
//  The  main  game  loop
while (window.isOpen())
2
3
4
5
6
7
8
9
10
11
12
在前面的代码中,我们有变量来跟踪自上次更新HUD以来已经过了多少帧,以及我们希望在两次HUD更新之间等待的间隔(以帧数为单位)。
现在,我们可以使用这些新变量,并在每一帧更新HUD。不过,在我们开始在下一章中操作诸如wave等最终变量之前,我们不会看到HUD的所有元素都发生变化。
在游戏循环的更新部分添加以下突出显示的代码:
// Has  the player  touched  ammo pickup
if (player.getPosition().intersects
(ammoPickup.getPosition()) && ammoPickup.isSpawned()) {
bulletsSpare += ammoPickup.gotIt(); 
}
// size  up  the  health  bar
healthBar.setSize (Vector2f (player.getHealth()  *  3 ,  50 ));
// Increment  the  number  of frames  since  the previous  update
framesSinceLastHUDUpdate++;
// re-calculate  every fpsMeasurementFrameInterval frames
if   (framesSinceLastHUDUpdate   >  fpsMeasurementFrameInterval)    {
    // Update  game HUD  text
    std::stringstream   ssAmmo;
    std::stringstream  ssScore;
    std::stringstream  ssHiScore;
    std::stringstream  ssWave;
    std::stringstream  ssZombiesAlive;
    // Update  the  ammo  text
        ssAmmo  <<   bulletsInClip  <<   "/"   <<   bulletsSpare;       
    ammoText.setString(ssAmmo.str());
    // Update  the  score  text
    ssScore   <<   "Score:"   <<   score;
    scoreText.setString(ssScore.str());
    // Update  the  high  score  text
    ssHiScore  <<   "Hi   Score:"   <<   hiScore;
    hiScoreText.setString(ssHiScore.str());
    // Update  the  wave
    ssWave   <<   "Wave:"   <<  wave;
    waveNumberText.setString(ssWave.str());
    // Update  the  high  score  text
    ssZombiesAlive   <<   "Zombies:"   <<   numZombiesAlive;
        zombiesRemainingText.setString(ssZombiesAlive.str());       
    framesSinceLastHUDUpdate   =  0;
    }//  End HUD  update
}//  End  updating  the  scene
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
37
38
39
40
41
在新代码中,我们更新了healthBar精灵的大小,然后增加framesSinceLastHUDUpdate变量的值。
接下来,我们开始一个if代码块,测试framesSinceLastHUDUpdate是否大于我们设定的间隔,这个间隔存储在fpsMeasurementFrameInterval中。
在这个if代码块中才是所有操作发生的地方。首先,我们为每个需要设置给文本对象的字符串声明一个stringstream对象。
然后,我们依次使用这些stringstream对象,并使用setString函数将结果设置给相应的文本对象。
最后,在退出if代码块之前,将framesSinceLastHUDUpdate重置为0,以便重新开始计数。
现在,当我们重新绘制场景时,新的值将显示在玩家的HUD中。
# 绘制平视显示器(HUD)、主界面和升级界面
以下三个代码块中的所有代码都位于游戏循环的绘制阶段。我们所要做的就是在主游戏循环的绘制部分,在适当的游戏状态下绘制相应的文本对象。
在“游玩(PLAYING)”状态下,添加以下突出显示的代码:
//Draw  the  crosshair
window.draw(spriteCrosshair);
// Draw  the player
window.draw(player.getSprite());
// Switch  to  the HUD  view
window.setView(hudView);
// Draw  all  the HUD  elements
window.draw(spriteAmmoIcon);  
window.draw(ammoText);
window.draw(scoreText);
window.draw(hiScoreText);
window.draw(healthBar);
window.draw(waveNumberText);
window.draw(zombiesRemainingText);
}
if (state == State::LEVELING_UP)
{
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
请注意,在前面的代码块中,我们切换到了HUD视图。这使得所有内容都能在我们为HUD的每个元素指定的精确屏幕位置上绘制出来。它们永远不会移动,因为我们从未更改HUD视图。
在“升级(LEVELING_UP)”状态下,添加以下突出显示的代码:
if (state == State::LEVELING_UP) {
    window.draw(spriteGameOver);
    window.draw(levelUpText);
}
2
3
4
在“暂停(PAUSED)”状态下,添加以下突出显示的代码:
if (state == State::PAUSED) {
    window.draw(pausedText);
}
2
3
在“游戏结束(GAME_OVER)”状态下,添加以下突出显示的代码:
if (state == State::GAME_OVER) {
    window.draw(spriteGameOver);  
    window.draw(gameOverText);        
    window.draw(scoreText);
    window.draw(hiScoreText);
}
2
3
4
5
6
现在,我们可以运行游戏,在游戏过程中看到HUD的更新:

图13.1:游戏过程中HUD的更新
以下屏幕截图展示了主界面/游戏结束界面上的最高分和得分:

图13.2:主界面/游戏结束界面上的最高分和得分
接下来,我们可以看到告知玩家升级选项的文本,不过这些选项目前还没有实际功能:

图13.3:告知玩家升级选项的文本
在这里,我们可以看到暂停屏幕上有一条有用的消息,提示玩家开始新游戏:
 图13.4:暂停屏幕上提示玩家开始新游戏的消息
图13.4:暂停屏幕上提示玩家开始新游戏的消息
SFML视图的功能比这个简单的HUD所展示的要强大得多。若想深入了解SFML视图类(SFML View class)的潜力以及使用的便捷性,可以查看SFML网站上关于视图的教程,网址为https://www.sfml-dev.org/tutorials/2.5/graphics-view.php (opens new window)。此外,在最终项目中,我们将使用多个视图实例来创建小地图功能。
看到我们的游戏逐渐成型,希望这能让你感到满意。菜单就像是将游戏其他部分粘合在一起的胶水,使游戏具备可玩性。但我们还有更多工作要做,让我们继续吧。
# 总结
这是简短的一章。我们学习了如何使用sstream显示不同类型变量所存储的值,然后又了解了如何使用第二个SFML视图对象,在主游戏画面上方绘制这些值。
现在,我们差不多完成《僵尸竞技场》(Zombie Arena)这款游戏的开发了。我们添加并了解了如何更新HUD,包括升级界面和主界面。本章中的所有屏幕截图展示的都是一个小场地,并没有充分利用整个显示器的空间。
在下一章,也是本项目的最后一章,我们将进行一些收尾工作,比如升级功能、音效以及保存最高分。之后,游戏场地的大小可以扩展到与显示器相同,甚至更大。
