python项目1--【外星人入侵游戏】之计分系统
python项目1--【外星人入侵游戏】之计分系统
本节将结束游戏《外星人入侵》的开发。我们会添加一个Play
按钮,用于根据需要启动游戏以及在游戏结束后重启游戏,还会修改这个游戏,使其随玩家等级提高而加快节奏,并实现一个计分系统。阅读本节后,你将掌握足够多的知识,能够开始编写随玩家等级提高而加大难度以及显示得分的游戏。
一、添加Play按钮
本节将添加一个Play
按钮,它在游戏开始前出现,并在游戏结束后再次出现,让玩家能够开始新游戏。
当前,这个游戏在玩家运行alien_invasion.py
时就开始了。下面让游戏一开始处于非活动状态,并提示玩家单击Play
按钮来开始游戏。为此,像下面这样修改GameStats
类的方法__init__()
:
现在,游戏一开始将处于非活动状态,待创建Play
按钮后,玩家才能开始游戏。
1. 创建Buttom类
由于Pygame
没有内置创建按钮的方法,我们将编写一个Buttom
类,用于创建带标签的实心矩形,你可在游戏中使用这些代码来创建任何按钮。下面是Buttom
类的第一部分,请将这个类保存为文件buttom.py
:
首先,导入模块pygame.font
,它让Pygame能够将文本渲染到屏幕上。方法__init__()
接受参数self
、对象ai_game
和msg
,其中msg
是要在按钮中显示的文本。设置按钮的尺寸,再通过设置button_color
,让按钮的rect
对象为亮绿色,并通过设置text_color
染发文本为白色。
然后,指定使用什么字体来渲染文本。实参None
让Pygame使用默认字体,而48
指定了文本的字号。为让按钮在屏幕上居中,创建一个表示按钮的rect
对象,并将其center
属性设置为屏幕的center
shuxing .
Pygame处理文本的方式是,将要显示的字符串渲染为图像。最后,调用了_prep_msg()
来处理这样的渲染。
_prep_msg()
的代码如下:
方法_prep_msg()
接受实参self
以及要渲染为图像的文本(msg)
。调用font.render()
将存储在msg
中的文本转换为图像,再将该图像存储在self.msg_image
中。方法font.render()
还接受一个布尔实参,该实参指定开启还是关闭反锯齿功能(反锯齿让文本的方便更平滑)。余下的实参分别是文本颜色和本景色。我们启用了反锯齿功能,并将文本的背景色设置为按钮的颜色。(如果没有指定背景色,Pygame渲染文本时将使用透明背景。)
然后,让文本图像在按钮上居中:根据文本图像创建一个rect
,并将其center
属性设置为按钮的center
属性。
最后,创建方法draw_button()
,用于将这个按钮显示到屏幕上:
我们调用screen.fill()
来绘制表示按钮的矩形,再调用screen.blit()
并向它传递一幅图像以及与该图像相关联的rect
,从而在屏幕上绘制文本图像。至此,Button
类便创建好了。
2. 在屏幕上绘制按钮
我们将在AlienInvasion
中使用Button
类来创建一个Play
按钮。首先,更新import
语句:
只需要一个Play
按钮,因此在AlienInvasion
类的方法__init__()
中创建它。可将这些代码放在方法__init__()
的末尾:
这些代码创建一个标签为Play
的Button
实例,但没有将它显示到屏幕上。为显示该按钮,在_update_screen()
对其调用方法draw_button()
:
为让Play
按钮位于其他所有屏幕元素上面,在绘制其他所有游戏元素后再绘制这个按钮,然后切换到新屏幕。将这些代码放在一个if
代码块中,让按钮仅在游戏处于非活动状态时才出现。
如果现在运行这个游戏,将在屏幕中央看到一个Play
按钮,如下图所示:
3. 开始游戏
为在玩家单击Play
按钮时开始新游戏,在_check_events()
末尾添加如下elif
代码块,以监视与该按钮相关的鼠标事件:
无论玩家单击屏幕的什么地方,Pygame都将检测到一个MOUSEBUTTONDOWN
事件,但我们只想让这个游戏在玩家用鼠标单击Play
按钮时做出响应。为此,使用了pygame.mouse.get_pos()
,它返回一个元组,其中包含玩家单击时鼠标的x
坐标和y
坐标。我们将这些值传递给新方法_check_play_button()
。
方法_ckeck_play_button()
的代码如下,将它放在_check_events()
后面:
这里使用了rect
的方法collidepoint()
检查鼠标单击位置是否在Play
按钮的rect
内。如果是,就将game_active
设置为True
,让游戏开始!
至此,现在应该能够开始这个游戏了。游戏结束时,应将game_active
设置为False
,并重新显示Play
按钮。
4. 重置游戏
前面编写的代码只处理了玩家第一次单击Play
按钮的情况,而没有处理游戏结束的情况,因为没有重置导致游戏结束的条件。
为在玩家每次单击Play按钮时都重置游戏,需要重置统计信息、删除现有的外星人和子弹、创建一群新的外星人并让飞船居中,如下所示:
首先,重置游戏统计信息,给玩家提供三艘新飞船。接下来,将game_active
设置为True
。这样,这个方法的代码执行完毕后,游戏就将开始。清空编组aliens
和bullets
,然后创建一群新的外星人并将飞船居中。
现在,每当玩家单击Play
按钮时,这个游戏都将正确地重置,让玩家想玩多少次就玩多少次!
5. 将Play按钮切换到非活动状态
当前存在一个问题:即便Play
按钮不可见,玩家单击其所在的区域时,游戏依然会做出响应。游戏开始后,如果玩家不小心单击了Play
按钮所处的区域,游戏将重新开始!
为修复这个问题,可让游戏仅在game_active
为False
时才开始:
标志button_clicked
的值为True
或False
。仅当玩家单击了Play
按钮且游戏当前处于非活动状态时,游戏才重新开始。要测试这种行为,可开始新游戏,并不断单击Play
按钮所在的区域。如果一切都想预期的那样工作,单击Play
按钮所处的区域应该没有任何影响。
6. 隐藏鼠标光标
为让玩家能够开始游戏,要让鼠标光标课件,但游戏开始后,光标只会添乱。为修复这种问题,需要在游戏处于活动状态时然后光标不可见。可在方法_check_play_button()
末尾的if
代码块中完成这项任务:
通过向set_visible()
传递False
,让Pygame在光标位于游戏窗口内时将其隐藏起来。
游戏结束后,将重新显示光标,让玩家能够单击Play
按钮来开始新游戏。相关的代码如下:
在_ship_hit()
中,在游戏进入非活动状态后,立即让光标可见。关注这样的细节让游戏显得更专业,也让玩家能够专注于玩游戏而不是去费力理解用户界面。
二、提高等级
当前,将整群外星人消灭干净后,玩家将提高一个等级,但游戏的难度没变。下面来增加一点趣味性:每当玩家将屏幕上的外星人消灭干净后,都加快游戏的节奏,让游戏玩起来更难。
1. 修改速度设置
首先重新组织Settings
类,将游戏设置划分成静态和动态两组。对于随着游戏进行而变化的设置,还要确保在开始新游戏时进行重置。settings.py
的方法__init__()
如下:
依然在__init__()
中初始化静态设置。首先,添加设置speedup_scale
,用于控制游戏节奏的加快速度:2表示玩家没提高一个等级,游戏的节奏就翻一倍;1表示游戏节奏始终不变
。将其设置为1.1
能够将游戏节奏提高到足够快,让游戏既有难度又并非不可完成。最后,调用initialize_dynamic_settings()
初始化随游戏进行而变化的属性。
initialize_dynamic_settings()
的代码如下:
这个方法设置飞船、子弹和外星人的初始速度。随着游戏的进行,将提高这些速度。每当玩家开始新游戏时,都将重置这些速度。在这个方法中,还设置了fleet_direction
,使得游戏刚开始时,外星人总是向右移动。不需要增大fleet_drop_speed
的值,因为外星人移动的速度越快,到达屏幕底端所需的时间越短。
为在玩家的等级提高时提高飞船、子弹和外星人的速度,编写一个名为increase_speed()
的新方法:
为提高这些游戏元素的速度,将每个速度都乘以speedup_scale
的值。
在_check_bullet_alien_collisions()
中,在整群外星人都被消灭后调用increase_speed()
来加快游戏的节奏:
通过修改速度设置ship_speed
、alien_speed
和bullet_speed
的值,足以加快整个游戏的节奏!
2. 重置速度
每当玩家开始新游戏时,都需要将发生了变化的设置重置为初始值,否则新游戏开始时,速度设置将为前一次提高后的值:
现在,游戏《外星人入侵》玩起来更有趣,也更有挑战性了。每当玩家将屏幕上的外星人消灭干净后,游戏都将加快节奏,因此难度更大。如果游戏的难度提高得太快,可降低settings.speedup_scale
的值;如果游戏的挑战性不足,可稍微提高这个设置的值。找出这个设置的最佳值,让难度的提高速度相对合理:一开始的几群外星人很容易消灭干净,接下来的几群消灭起来有一定难度,但也不是不可能,而要将之后的外星人群消灭干净几乎不可能。
三、计分
下面来实现一个计分系统,以实时跟踪玩家的得分,并显示最高得分、等级和余下的飞船数。
得分是游戏的一项统计信息,因此在GameStats
中添加一个score
属性:
为在每次开始游戏时都重置得分,我么在reset_stats()
而不是__init__()
中初始化score
。
1. 显示得分
为在屏幕上显示得分,首先创建一个新类Scoreboard
。当前,这个类只显示当前得分,但后面也将使用它来显示最高得分、等级和余下的飞船数。下面是这个类的前半部分,被保存为文件scoreboard.py
:
由于Scoreboard
在屏幕上显示文本,首先导入模块pygame.font
。接下来,在__init__()
中包含形参ai_game
,以便访问报告跟踪的值所需的对象settings
、screen
和stats
。然后,设置文本颜色并实例化一个字体对象。
为将要显示的文本转换为图像,调用prep_score()
,其定义如下:
在prep_score()
中,将数值stats_score
转换为字符串,再将这个字符串传递给创建图像的render()
。为在屏幕上清晰地显示得分,向render()
传递屏幕背景色和文本颜色。
将得分放在屏幕右上角,并在等分增大导致数变宽时让其向左延伸。为确保得分始终锚定在屏幕右边,创建一个名为score_rect
的rect
,让其右边缘与屏幕右边缘相距20
像素,并让其上边缘与屏幕上边缘也相距20
像素。
接下来,创建方法show_score()
,用于显示渲染好的得分图像:
这个方法在屏幕上显示得分图像,并将其放在score_rect
指定的位置。
2. 创建计分牌
为显示得分,在AlienInvasion
中创建一个Scoreboard
实例。先来更新import
语句:
接下来,在方法__init__()
中创建一个Scoreboard
实例:
然后,在_update_screen()
中将计分牌绘制到屏幕上:
在显示Play
按钮前调用show_score()
。
如果现在运行这个游戏,将在屏幕右上角看到0
。(当前,我们只想在进一步开发计分系统前确认得分出现在正确的地方)。下图显示了游戏开始前的得分。
下面来指定每个外星人值多少分!
3. 在外星人被消灭时更新得分
为在屏幕上实时显示得分,每当有外星人被击中时,都更新stats.score
的值,再调用prep_score()
更新得分图像。但在此之前,需要指定玩家每击落一个外星人将得到多少分:
随着游戏的进行,将提高每个外星人的分数。为确保每次开始新游戏时这个值都会被重置,我们在initialize_dynamic_settings()
中设置它。
在_check_bullet_alien_collisions()
中,每当有外星人被击落时,都更新得分:
有子弹击中外星人时,Pygame返回一个字典(coliisions)
。我们检查这个字典是否存在,如果存在,就将得分加上一个外星人的分数。接下来,调用prep_score()
来创建一幅包含最新得分的新图像。
如果现在运行这个游戏,得分将不断增加!
4. 重置得分
当前,仅在有外星人被射杀之后生成得分。这在大多数情况下可行,但从开始新游戏到有外星人被射杀之间,显示的是上一次的得分。
为修复这个问题,可在开始新游戏时生成得分:
开始新游戏时,我们重置游戏统计信息再调用prep_score()
,此时生成的计分牌上显示的得分为零。
5. 将消灭的每个外星人都计入得分
当前的代码可能会遗漏一些被消灭的外星人。例如,如果在一次循环中,有两颗子弹击中了外星人,或者因子弹较宽而同时击中了多个外星人,玩家将智能得到一个外星人的分数。为修复这种问题,我们来调整检测子弹和外星人碰撞的方式。
在_check_bullet_alien_collisions()
中,与外星人碰撞的子弹都是字典collisions
中的一个键,而与每颗子弹相关的值都是一个列表,其中包含该子弹击中的外星人。我们遍历字典collisions
,确保将消灭的每个外星人都计入得分:
如果字典collisions
存在,就遍历其中的所有值。别忘了,每个值都是一个列表,包含被同一颗子弹击中的所有外星人。对于每个列表,都将其包含的外星人数量乘以一个外星人的分数,并将结果加入当前得分。为测试这一点,请将子弹宽度改为300
像素,并核实得到了其击中的每个外星人的分数,再将子弹宽度恢复正常值。
6. 提高分数
鉴于玩家每提高一个等级,游戏都变得更难,因此处于较高等级时,外星人的分数应更高。为实现这种功能,需要编写在游戏节奏加快时提高分数的代码:
我们定义了分数的提高速度,并称之为score_scale
。较低的节奏加快速度(1.1)
让游戏很快变得极具挑战性,但为了让计分发生显著的变化,需要将根数的提高速度设置为更大的值(1.5)
。现在,在加快游戏节奏的同时,提高了每个外星人的分数。为让分数为整数,使用了函数int()
。
为显示外星人的分数,在Settings
的方法increase_speed()
中调用函数print()
:
现在每当提高一个等级时,你都将在终端窗口看到新的分数值。
注意:确认分数在不断增加后,一定要删除调用函数print()
的代码,否则可能影响游戏的性能,分散玩家的注意力。
7. 舍入得分
大多数街机风格的射击游戏将得分显示为10的整数倍,下面让计分系统遵循这个原则。我们还将设置得分的格式,在大数中添加用逗号表示的千位分隔符。在Scoardboard
中执行这种修改:
函数ronud()
通常让小数精确到小数点后某一位,其中小数位数是都第二个实参指定的。然而,如果将第二个实参指定为负数,round()
将舍入到最近的10
的整数倍,如10
、100
、1000
等。下面是让Python将stats.score
的值舍入到最近的10的整数倍,并将结果存储到rounded_score
中。
接着,使用一个字符串格式设置指令,让Python
将数值转换为字符串时在其中插入逗号。例如,输出为1,000,000
而不是1000000
。如果现在运行这个游戏,看到的得分将是10的整数倍,即便得分很高亦如此。如下图所示。
8. 最高得分
每个玩家都想超过游戏的最高得分技术。下面来跟踪并显示最高得分,给玩家提供要超越的目标。我们将最高得分存储在GameStats
中:
因为在任何情况下都不会重置最高分,所以在__init__()
而不是reset_stats()
中初始化high_score
。
下面来修改Scoreboard
以显示最高得分。先来修改方法__init__()
:
最高得分将与当前得分分开显示,因此需要编写一个新方法prep_htgh_score
,用于准备包含最高得分的图像。
方法prep_high_score()
的代码如下:
将最高得分舍入到最近的10的整数倍,并添加用逗号表示的千分位分隔符。然后,根据最高得分生成一幅图像,使其水平居中,并将其top
属性设置为当前得分图像的top
属性。
现在,方法show_score()
需要在屏幕右上角显示当前得分,并在屏幕顶部中央显示最高得分:
为检查是否诞生了新的最高得分,在Scoreboard
中添加一个新方法check_high_score()
:
方法check_high_score()
比较当前得分和最高得分。如果当前得分更高,就更新high_score
的值,并调用prep_high_score()
来更新包含最高得分的图像。
在_check_bullet_alien_collisions()
中,每当有外星人被消灭时,都需要在更新得分后调用check_high_score()
:
如果字典collisions
存在,就根据消灭了多少外星人更新得分,再调用check_high_score()
。
第一次玩这个游戏时,当前得分就是最高得分,因此两个地方显示的都是当前得分。但再次开始该游戏时,最高得分会出现在中央,而当前得分则出现在右边,如下图所示。
9. 显示等级
为在游戏中显示玩家的等级,首先需要在GameStats
中添加一个表示当前等级的属性。为确保每次开始新游戏时都重置等级,在reset_stats()
中初始化它:
为了让Scoreboard
显示当前等级,在__init__()
中调用一个新方法prep_lavel()
:
prep_level()
的代码如下:
方法prep_level()
根据存储在stats.level
中的值创建一幅图像,并将其right
属性设置为得分的right
属性。然后,将top
属性设置为比得分图像的bottom
属性大10
像素,以便在得分和等级之间留出一定的空间。
还需要更新show_score()
:
新增的代码行在屏幕上显示等级图像。
我们在_check_bullet_alien_collisions()
中提高等级并更新等级图像:
如果郑群外星人都被消灭,就将stats.level
的值加1
,并调用prep_level()
确保正确地显示了新等级。
为确保在开始新游戏时更新等级图像,还需在玩家单击按钮Play
时调用prep_level()
:
这里在调用prep_score()
后立即调用prep_level()
。
现在可以知道到了多少级,如下图所示。
注意:在一些经典游戏中,得分带有标签,如Score
、High Score
和Lavel
。这里没有显示这些标签,游戏开始后,每个数的含义将一目了然。要包含这些标签,只需在Scoreboard
中调用font.render()
前,将它们添加到得分字符串中。
10. 显示余下的飞船数
最后来显示玩家还有多少艘飞船,但使用图形而不是数字。为此,在屏幕左上角绘制飞船图像来指出还余下多少艘飞船,就像众多经典的街机游戏中那样。
首先,需要让Ship
继承Sprite
,以便创建飞船编组:
这里导入了Sprite
,让Ship
继承Sprite
,并在__init__()
的开头调用super()
。
接下来,需要修改Scoreboard
,以创建可供显示的飞船编组。下面是其中的import
语句:
鉴于需要创建飞船编组,导入Group
和Ship
类。
下面是方法__init__()
:
我们将游戏实例赋给一个属性,因为创建飞船时需要用到它。在调用prep_level()
后调用了prep_ships()
。
prep_ships()
的代码如下:
方法prep_ships()
创建一个空编组self.ships
,用于存储飞船实例。为填充这个编组,根据玩家还有多少艘飞船以响应的次数运行一个循环。在这个循环中,创建新飞船并设置其x
坐标,让整个飞船编组都位于屏幕左边,且每艘飞船的左边距都为10
像素。还将y
坐标设置为例屏幕上边缘10像素,让所有飞船都出现在屏幕左上角。最后,将每艘飞船都添加到编组ships
中。
现在需要在屏幕上绘制飞船了:
为在屏幕上显示飞船,对编组调用draw()
。Pygame将绘制每艘飞船。未在游戏开始时让玩家知道自己有多少艘飞船,在开始新游戏时调用prep_ships()
。这是在AlienInvasion
的_check_play_button()
中进行的:
还要在飞船被外星人撞到时调用prep_ships()
,从而在玩家损失飞船时更新飞船图像:
这里在将ships_left
的值减1
后调用prep_ships()
。这样每次损失飞船后,显示的飞船数都是正确的。
下图显示的计分系统,它在屏幕左上角指出还余下多少艘飞船。
四、小结
在本篇中,学习了如何创建用于开始新游戏的Play
按钮,如何检测鼠标事件,以及在游戏处于活动状态时如何隐藏鼠标光标。可以利用学到的知识在游戏中创建其他按钮,如用于显示玩法说明的Help
按钮。还学习了如何随着游戏的进行调整其节奏,如何实现计分系统,以及如何以文本和非文本方式显示信息