A Python Game Project. To study how to use python doing a game project.
开发一个游戏. 使用Python的一组功能强大而有趣的模块, 可用于管理图形/动画/声音, 让你能够更轻松地开发复杂的游戏.
在本章, 创建一艘能根据用户输入而左右移动和射击的飞船. 在本章, 还学习管理包含多个文件的项目. 会重构很多代码, 以提高代码的效率, 并管理文件的内容, 以确保项目组织有序.
先做好规划, 确保不偏离轨道, 从而提高项目成功的可能性. 编写关于外星人入侵的描述, 让你清楚地知道该如何动手开发它.
游戏简介: 在游戏外星人入侵中, 玩家控制一艘最初出现在屏幕底部中央的飞船. 玩家可以使用箭头键左右移动飞船, 还可以使用 空格键进行射击. 游戏开始时, 一群外星人出现在天空中, 他们在屏幕中向下移动. 玩家的任务时射杀这些外星人. 玩家将所有外星人都消灭干净后, 将出现一群新的外星人, 他们移动的速度更快. 只要有外星人撞到了玩家的飞船或到达了屏幕底部, 玩家就损失一艘飞船. 玩家损失三艘飞船后, 游戏结束.
首先创建一个空的Pygame窗口, 供后面用来绘制游戏元素, 如飞船和外星人. 我们还将让这个游戏响应用户输入, 设置背景色以及加载飞船图像.
详细解释见
alien_invasion.py
的run_game()
函数.
每次给游戏添加新功能时, 通常也将引入一些新设置.
下面来编写一个名为settings
的模块, 其中包含一个名为Settings
的类, 用于将所有设置存储在一个地方, 以免在代码中
到处添加设置. 这样, 我们就能传递一个设置对象, 而不是众多不同的设置.
另外, 这让函数调用更简单, 且在项目增大时修改游戏的外观更容易:
要修改游戏, 只需修改settings.py
的一些值, 而无需查找散布在文件中的不同设置.
在主程序文件中, 导入Settings
类, 调用pygame.init()
, 再创建一个Settings
实例, 并将其存储在变量ai_settings
中.
为了在屏幕上绘制飞船, 我们将加载一幅图像, 再使用Pygame方法blit()
绘制.
选择素材时, 务必注意许可.
在游戏中几乎可以使用任何类型的图像文件, 但使用位图(.bmp)文件最为简单, 因为Pygame默认加载位图. 选择图像时, 特别注意背景色. 尽可能选择背景透明的图像. 在主项目文件夹中创建一个文件夹images, 用来保存图像文件.
创建一个ship
模块, 其中包含Ship
类, 负责管理飞船的大部分行为.
加载图像后, 我们使用get_rect()
获取相应surface的属性rect. Pygame之所以效率高, 一个原因是它让你能够像处理
矩形(rect对象)一样处理游戏元素, 即便他们的形状并非矩形.
像矩形一样处理游戏元素之所以高效, 是因为矩形是简单的几何形状. 而玩家不会注意到我们处理的不是游戏元素的实际形状.
处理rect对象, 可使用矩形四角和中心的x和y坐标. 可通过设置这些值来指定矩形位置.
要将游戏元素居中, 可设置相应rect对象的属性center/centerx/centery. 要让游戏元素与屏幕边缘对齐, 可使用属性top/bottom/left/right; 要调整游戏元素的水平或垂直位置, 可使用属性x和y, 分别对应矩形左上角的x和y坐标.
在pygame中, 原点(0, 0)位于屏幕左上角, 向右下方移动时, 坐标值将增大. 在1200×800的屏幕上, 原点位于左上角, 右下角为(1200, 800)
在大型项目中, 经常需要在添加新代码前重构既有代码. 重构旨在简化既有代码的结构, 使其更容易扩展.
在本节中, 创建一个名为game_functions
的新模块, 它将存储大量让游戏外星人入侵运行的函数.
通过创建模块game_functions
, 可避免alien_invasion.py
太长, 并使其逻辑更容易理解.
首先把管理事件的代码移到名为check_events()
的函数中, 以简化run_game()
并隔离事件管理循环.
通过隔离事件循环, 可将事件管理与游戏的其他方面(如更新屏幕)分离.
为进一步简化run_game()
, 将屏幕更新的代码移到一个名为update_screen()
的函数中.
这两个函数让while循环更简单, 并让后续开发更容易: 在模块game_functions
而不是run_game()
中完成大部分工作.
鉴于一开始只想使用一个文件, 因此没有立刻引入模块game_functions
. 这让我们能了解实际的开发过程: 一开始将代码
编写得尽可能简单, 并在项目越来越复杂时进行重构.
能左右移动飞船. 先专注于向右移动, 再使用同样的原理来控制向左移动.
每当用户按键时, 都将在Pygame中注册一个事件. 事件都是通过方法 pygame.event.get()
获取的. 因此再函数
check_events()
中, 我们需要指定要检查哪些类型的事件. 每次按键都被注册为一个 KEYDOWN
事件.
检测到KEYDOWN
事件时, 需要检查按下的是否是特定的键. 例如, 按下的时右箭头键, 增大飞船的rect.centerx
值, 将飞船向右移动:
见
game_functions.check_events()
在函数check_events()
中包含形参ship, 因为玩家按向右箭头时, 需要将飞船向右移动.
现在的效果是, 每按一次右箭头, 飞船向右移动1像素. 这并非控制飞船的高效形式. 下面来进行改进, 允许持续移动.
玩家按住右箭头不放时, 我们希望飞船不断地向右移动, 直到玩家松开为止. 我们让游戏检测pygame.KEYUP
事件, 以便玩家松开
右箭头时我们能够知道这一点; 然后, 我们将结合使用KEYDOWN
和KEYUP
事件, 以及一个名为moving_right
的标志来实现持续移动.
飞船不动时, 标志moving_right
将为False
. 玩家按下右箭头时, 我们将这个标志设置为True
; 而玩家松开时, 我们将这个
标志重新设置为False
.
飞船的属性都由Ship
类控制, 因此我们将给这个类添加一个名为moving_right
的属性和一个名为update()
的方法.
方法update()
检查标志moving_right
的状态, 如果这个标志为True
, 就调整飞船的位置. 每当需要调整飞船的位置时,
我们都调用这个方法.
见修改后的
ship.py
在方法__init__()
中, 添加了属性self.moving_right
, 并将其初始值设置为False
.
添加方法update()
, 它在前述标志为True
时向右移动飞船.
下面来修改check_events()
, 使其在玩家按下右箭头时将moving_right
设置为True
,
并在玩家松开时将moving_right
设置为False
.
见修改后的
game_functions.py
- 修改了游戏在玩家按下右箭头时响应的方式; 不直接调整飞船的位置, 而只是将
moving_right
设置为True
. - 添加了一个新的
elif
块, 用于响应KEYUP
事件: 玩家松开右箭头, 我们将moving_right
设置为False
最后, 修改alien_invasion.py
中的while
循环, 以便每次执行循环时都调用飞船的方法update()
.
飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新. 这样, 玩家输入时, 飞船的位置将更新, 从而确保使用更新后的位置将
飞船绘制到屏幕上.
见代码
当前, 每次执行while循环时, 飞船最多移动1像素, 但我们可以在Settings类中添加属性ship_speed_factor
, 用于控制飞船的速度.
具体见
settings.py
通过将速度设置指定为小数值, 可在后面加快游戏的节奏时更细致地控制飞船的速度. 然而, rect的centerx等属性只能存储整数值, 因此需要对Ship类做修改.
见
ship.py
- 在
__init__()
的形参列表中加入ai_settings
, 让飞船能够获取其速度设置 - 将形参
ai_settings
的值存储在一个属性中, 以便能够在update()
中使用它. rect
只能存储整数部分, 定义一个可存储小数的新属性self.center
. 使用float()
将self.rect.centerx
的值转换为小数, 并存储到self.center
中update()
调整飞船位置时, 将self.center
的值+或-ai_settings.ship_speed_factor
的值. 更新self.center
后, 再根据它来更新飞船位置self.rect.centerx
. (self.rect.centerx
只保存整数部分, 但是显示飞船看起来效果差不多.)- 在
alien_invasion.py
中创建Ship实例时, 需要传入实参ai_settings
见
alien_invasion.py
这样, 有助于让飞船反应速度足够快, 还能随着游戏进行加快游戏节奏.
当前, 如果玩家一直按住箭头, 飞船会移动到屏幕外面, 消失. 下面来修复该问题, 让飞船到达边缘后停止移动.
见
ship.py
- 在修改
self.center
的值之前检查飞船位置. self.rect.right
返回飞船外接矩形的右边缘x坐标, 如果小于self.screen_rect.right
值, 说明飞船未触及屏幕右边缘.- 左边缘同理, 左边缘x坐标为0.
随着游戏开发的进行, 函数check_events()
将越来越长, 我们将其部分代码放在2个函数中: 一个处理KEYDOWN事件,
另一个处理KEYUP事件.
见
game_functions.py
创建了2个新函数, 包含形参event和ship. 将函数check_events
中相应代码替换成对这两个函数的调用. 这样, 函数check_events()
更简单, 代码结构更清晰.
当前, 有4个文件.
主文件. 创建一系列整个游戏都要用到的对象:
- 存储在
ai_settings
中的设置 - 存储在
screen
中的主显示surface - 飞船实例
- 游戏主循环
- 调用
check_events()
ship.update()
update_screen()
- 调用
要玩游戏, 只需要运行文件alien_invasion.py
. 其他文件(settings.py game_functions.py ship.py)包含的代码被直接或间接地
导入到这个文件中.
包含Settings
类, 该类只包含__init__()
方法, 初始化控制游戏外观和飞船速度的属性.
包含一系列函数, 游戏的大部分工作都由它们完成.
check_events()
检测相关事件, 如按键和松开, 并使用辅助函数check_keydown_events()
和check_keyup_events()
来处理事件. 目前, 这些函数管理飞船的移动.update_screen()
, 用于在每次执行主循环时重绘屏幕
包含Ship类.
__init__()
- 管理飞船位置的方法
update()
- 在屏幕上绘制飞船的方法
blitme()
- 飞船图像存储在文件夹images下的ship.bmp中.
添加射击功能 - 玩家按空格键发射子弹(小矩形), 子弹在屏幕中向上穿行, 抵达屏幕边缘后消失.
更新settings.py
, 在__init__()
存储新类Bullet
所需的值:
具体见
settings.py
具体见
bullet.py
继承了模块pygame.sprite
中的Sprite
类. 通过使用精灵, 可将游戏中相关元素编组, 进而同时操作编组中的所有元素.
- 创建子弹实例, 需要
ai_settings
screen
ship
实例, 还调用super()
来继承Sprite
- 创建子弹属性rect. 子弹并非基于图像, 因此必须使用
pygame.Rect()
类从空白开始创建矩形. 创建这个类的实例时, 必须提供矩形 左上角的x y 坐标, 还有矩形的高度和宽度. - 在(0, 0)处创建, 并移动到正确的位置. 子弹初始位置取决于飞船的当前位置. 子弹宽度和高度从
ai_settings
中获取. - 将子弹centerx设置为飞船的
rect.centerx
. 子弹应从飞船顶部射出, 因此子弹的rect的top设置为飞船的rect的top属性. - 将子弹的y坐标存储为小数值, 以便能微调子弹速度.
- 将子弹的颜色/速度存储在
self.color
和self.speed_factor
中.
下面是bullet.py
的第二部分 -- 方法update()
和draw_bullet()
.
- 方法
update()
管理子弹的位置. 子弹发射出去向上移动, y坐标不断减小.- 属性
speed_factor
能随着游戏进行或根据需要提高速度
- 属性
- 调用
draw_bullet()
绘制子弹. 函数draw.rect()
使用存储在self.color
中的颜色填充表示子弹rect占据的屏幕部分.
玩家每次按空格键时都发射一枚子弹.
在alien_invasion.py
中创建一个编组(group), 用于存储所有有效的子弹, 以便能够管理发射出去的所有子弹.
这个编组是pygame.sprite.Group
类的一个实例; pygame.sprite.Group
类类似于列表, 但提供了有助于开发游戏的额外功能.
在主循环中, 将使用这个编组在屏幕上绘制子弹, 以及更新每颗子弹的位置:
具体见
alien_invasion.py
创建了一个Group实例, 并将其命名为bullets. 这个编组是在while循环外面创建的.
如果在循环内部创建这样的编组, 游戏运行时将创建数千个子弹编组, 导致游戏慢的像蜗牛.
将bullets传递给check_events()
和update_screen()
.
- 在
check_events()
中, 需要在玩家按空格键时处理bullets - 在
update_screen()
中, 需要更新要绘制到屏幕上的bullets
对编组调用update()
时, 编组将自动对其中的每个精灵调用update()
, 因此代码行bullets.update()
将为编组
bullets中的每颗子弹调用bullet.update()
在game_functions.py
中, 需要修改check_keydown_events()
, 以便在玩家按空格键时发射子弹.
无需修改check_keyup_events()
, 因为玩家松开空格键时啥也不会发生.
还需要修改update_screen()
, 确保在调用flip()
前在屏幕上重绘每颗子弹.
具体见
game_functions.py
编组bullets传递给check_keydown_events()
. 玩家按下空格键时, 创建一颗新子弹(一个名为new_bullet的Bullet实例),
并使用方法add()
将其加入到编组bullets中; 代码bullets.add(new_bullet)
将子弹存储到编组bullets中.
在update_screen()
中, 方法bullets.sprites()
返回一个列表, 包含编组bullets中的所有精灵. 为在屏幕上绘制发射的
所有子弹, 遍历编组bullets中的精灵, 并对每个精灵都调用draw_bullet()
.
当前, 子弹抵达屏幕顶端后消失, 这仅仅是因为Pygame无法在屏幕外绘制它们. 这些子弹仍然存在, 它们的y坐标为负数, 且越来越小. 这是个问题, 因为它们将继续消耗内存和处理能力.
我们需要将这些已消失的子弹删除, 否则游戏所做的无谓工作越来越多, 进而变得越来越慢. 为此, 检测子弹的rect的bottom属性为0. 表示子弹已穿过屏幕顶端.
具体见
alien_invasion.py
在for循环中, 不应从列表或编组中删除条目, 因此必须遍历编组的副本. 使用方法copy()
来设置for循环, 这让我们能够在循环中
修改bullets.
对同时出现在屏幕上的子弹数量进行限制, 以鼓励玩家有目标地射击.
在settings.py
中存储所允许的最大子弹数.
在game_functions.py
的check_keydown_events()
中, 在创建新子弹前检查未消失的子弹数是否小于该设置.
玩家按空格键, 检查bullets长度. 如果小于3, 创建一个新子弹; 如有已有3颗, 按空格什么都不会发生.
将检查子弹的代码移到模块game_functions
中, 以让主程序文件尽可能简单.
创建一个update_bullets()
的新函数.
这样, 主循环变得很简单, 只要看函数名就能迅速知道游戏中发生的情况.
- 检查玩家输入
- 更新飞船位置
- 更新未消失子弹的位置
- 使用更新后的位置来绘制新屏幕
将发射子弹的代码移到一个独立的函数中, 这样, 在check_keydown_events()
中只需使用一行代码来发射子弹.
学习了:
- 游戏开发计划的制定
- 使用Pygame编写游戏的基本结构
- 设置背景色, 如何将设置存储在可供游戏的各个部分访问的独立类中;
- 如何在屏幕上绘制图像, 如何让玩家控制游戏元素的移动
- 如何创建自动移动的元素, 如何删除不再需要的对象;
- 如何定期重构项目的代码
在本章, 为游戏添加外星人. 首先, 在屏幕边缘附近添加一个外星人, 然后生成一群外星人. 让外星人向两边和下面移动, 并删除被子弹击中的外星人. 最后, 显示玩家拥有的飞船数量, 并在玩家的飞船用完后结束游戏.
通过本章, 更深入地了解Pygame和大型项目的管理. 学习如何检测游戏对象之间的碰撞, 如子弹和外星人的碰撞. 检测碰撞有助于定义游戏元素间的交互. 还将时不时查看游戏开发计划, 确保编程工作不偏离轨道.
开发较大项目时, 进入每个开发阶段前回顾一下开发计划, 搞清楚接下来要通过编写代码来完成哪些任务. 本章内容:
- 研究既有代码, 确定实现新功能前是否要进行重构
- 在屏幕左上角添加一个外星人, 并指定合适的边距
- 根据第一个外星人的边距和屏幕尺寸计算屏幕上可容纳多少个外星人. 编写一个循环来创建一系列外星人, 填满屏幕上半部分.
- 让外星人群向两边和下方移动, 直到外星人被全部击落, 有外星人撞到飞船, 或有外星人抵达屏幕底端. 如果整群外星人都被击落, 将再创建一群外星人. 如果有外星人撞到飞船或抵达屏幕底端, 销毁飞船并再创建一群外星人.
- 限制玩家可用的飞船数量, 配给的飞船用完后, 游戏结束.
将每个外星人的左边距都设置为外星人的宽度, 上边距设置为外星人的高度.
在alien_invasion.py
中创建Alien实例.
要绘制一群外星人, 需要确定一行能容纳多少个外星人以及要绘制多少行外星人. 将首先计算外星人之间的水平间距, 并创建一行 外星人, 再确定可用的垂直空间, 并创建整群外星人.
暂定, 后续可随时调整
- 两边共留2个外星人宽度的空间
- 每个外星人占用2个外星人的空间
available_space_x = ai_settings.screen_width - (2 * alien_width)
number_aliens_x = available_space_x / (2 * alien_width)
为创建一行外星人, 首先在alien_invasion.py
中创建一个名为aliens的空编组, 用于存储全部的外星人, 再调用
game_functions.py
中创建外星人群的函数.
不再在alien_invasion.py
中直接创建外星人, 因此不需要导入Alien类.
- 先创建一个空编组, 用于存储所有的外星人.
- 调用函数
create_fleet()
- 修改对
update_screen()
的调用, 让它能访问外星人编组. - 修改
update_screen()
函数, 对编组调用draw()
时, Pygame自动绘制编组的每个元素, 绘制位置由元素的属性rect决定.
为放置外星人, 需要知道外星人的宽度和高度, 因此在计算前, 先创建一个外星人. 这个外星人不是外星人群的成员, 没有加入编组.
从外星人的rect属性中获取外星人宽度, 并将这个值存储到alien_width
中, 以免反复访问属性rect.
使用地板除, 保证外星人数量为整数.
接下来, 就是创建第一行外星人的循环.
这行外星人在屏幕上偏左, 后续会让外星人群右移, 触及屏幕边缘后往下移动, 然后往左移动...
重构为3个函数:
get_number_aliens_x()
代码都来自之前的create_fleet()
, 且未做修改create_alien()
create_fleet()
改为对上两个函数的调用.
通过这样的重构, 添加新行进而创建整群外星人将更容易.
计算外星人行数的可用空间:
available_space_y = ai_settings.screen_height - 3 * alien_height - ship_height
在飞船上方留出一定的空白区域, 给玩家留出射杀外星人的时间.
计算外星人行数:
number_rows = available_space_y / (2 * alien_height)
重复执行创建一行的外星人代码来创建多行.
为创建多行, 使用2个嵌套循环. 内部循环创建一行外星人, 外部循环创建行数.
在create_alien()
中, 修改外星人的y坐标.
先向右移动, 撞到边缘向下移动一定距离, 在沿反方向移动.
为移动外星人, 使用alien.py
中的方法update()
, 且对外星人群每个外星人都调用.
- 添加控制外星人速度的设置
- 使用这个设置来实现
update()
- 每次更新外星人位置, 都向右移动, 移动量就是之前设置的值,
- 使用属性
self.x
跟踪每个外星人的准确位置. - 然后使用
self.x
的值来更新rect的值
- 主while循环中, 在更新子弹后更新外星人位置(因为要检查是否有子弹撞到外星人)
- 在文件
game_functions.py
末尾添加新函数update_aliens()
目前的效果是: 外星人群向右移动, 在屏幕右边缘逐渐消失.
让外星人撞到屏幕右边后向下移动, 再向左移动的设置.
具体见
settings.py
外星人方向使用1和-1来表示, 在改变方向时在这两个值之间切换.
在check_fleet_edges()
中, 遍历外星人群, 并对其中的每个外星人调用check_edges()
. 如果返回True, 则相应外星人位于边缘,
需要改变外星人方向, 因此调用change_fleet_direction()
并退出循环.
在change_fleet_direction()
中, 遍历所有外星人, 将每个外星人下移fleet_drop_speed
的值;
然后, 将fleet_direction
值修改为其当前值与-1的乘积.
修改了update_aliens()
, 在其中调用check_fleet_edges()
来确定是否有外星人位于屏幕边缘.
后续工作: 射杀外星人, 检查是否有外星人撞到飞船, 或抵达屏幕底端.
目前还没有碰撞检测. 游戏编程中, 碰撞指的是游戏元素重叠在一起. 使用sprite.groupcollide()
检测两个编组成员间的碰撞.
更新子弹位置后立即检测碰撞.
方法sprite.groupcollide()
将每个子弹的rect同每个外星人的rect进行比较, 返回一个字典, 包含了发生碰撞的子弹和外星人.
在这个字典中, 每个键都是一个子弹, 相应的值是被击中的外星人(后续实现积分系统时, 也会用到这个字典)
具体见
game_functions.py
的update_bullets()
新增一行代码遍历编组bullets中的每颗子弹, 再遍历编组aliens中的每个外星人. 每当有子弹和外星人的rect重叠时, groupcollide()
就在它返回的子弹中添加一个键值对.两个实参True告诉Pygame删除发生碰撞的子弹和外星人.(要模拟能够穿行到屏幕顶端的高能子弹 -
消灭它击中的每个外星人, 可将第一个布尔值设为False, 第二个为True. 这样被击中的外星人将消失, 但所有的子弹都始终有效)
测试有些功能时, 可以修改游戏的某些设置, 以便专注于游戏的特定方面. 例如, 可以缩小屏幕以减少外星人数量, 也可以提高子弹 速度. 可以增大子弹的尺寸, 使其在击中外星人后依然有效. 类似这样的修改可以提高测试效率, 还能激发处如何赋予玩家更大威力.
外星人无穷无尽, 一个外星人群被消灭后, 又出现一群外星人.
首先检查编组aliens是否为空, 如果为空, 就调用create_fleet()
. 在update_bullets()
中执行检查, 因为外星人在这被消灭.
使用方法empty()
删除编组中余下的所有精灵, 从而删除现有的所有子弹.
重构该函数, 使其不再完成那么多任务. 把处理子弹和外星人碰撞的代码移到一个独立的函数中.
在更新外星人的位置后立即检测外星人和飞船之间的碰撞.
具体见
game_functions.py
的update_aliens()
方法spritecollideany()
接受两个实参: 一个精灵一个编组. 检查编组是否有成员与精灵发生了碰撞, 并在找到与精灵发生了碰撞
的成员后就停止遍历编组.
如果没有发生碰撞, spritecollideany()
返回None. 如果找到了与飞船发生碰撞的外星人, 就返回这个外星人.
(有外星人撞到飞船时, 需要执行的任务很多: 删除余下的所有外星人和子弹, 让飞船重新居中, 创建一群新的外星人.
编写这些任务前, 需要确定检测外星人和飞船碰撞的方法是否可行. 为确定这一点, 最简单的就是打印print语句.)
外星人与飞船碰撞时, 不销毁ship, 而是跟踪游戏的统计信息, 来记录飞船被撞了多少次(跟踪统计信息有助于记分).
编写一个用于跟踪游戏统计信息的新类 -- GameStats. 并将其保存为文件game_stats.py
当有外星人撞到飞船时, 我们将余下的飞船数量-1, 创建一群新的外星人, 并将飞船重新放到屏幕底端中央(我们还将让游戏暂停一段 时间, 让玩家在新外星人群出现前注意到发生了碰撞, 并将重建外星人群).
具体见
game_functions.py
的ship_hit()
使用time.sleep()
让游戏暂停.
新函数ship_hit()
在飞船被外星人撞到时做出响应.
- 余下飞船-1
- 清空编组aliens和bullets
- 创建一群新的外星人
- 将飞船居中
- 更新元素后(将修改显示在屏幕前)暂停, 让玩家直到飞船被撞.
注意: 根本没有创建多艘飞船, 在整个游戏运行期间, 都只创建了一个飞船实例, 并在该飞船被撞到时将其居中. 统计信息ships_left让我们直到飞船是否用完.
如果有外星人到达屏幕底端, 我们将像有外星人撞到飞船那样做出响应.
具体见
game_functions.py
的check_aliens_bottom()
检查是否有外星人到达了屏幕底端. 到达后, 调用ship_hit()
更新所有外星人位置并检测是否有外星人和飞船发生碰撞后调用check_aliens_bottom()
目前游戏永远都不会结束, 只是ships_left不断变成更小的负数. 在GameStats中添加一个作为标志的属性game_active, 以便在飞船用尽后结束游戏:
在ship_hit()
中添加代码, 在玩家飞船都用完后将game_active
设置为False:
如果飞船数>0, 继续执行. 如果没有飞船, 将game_active设为False.
在alien_invasion.py
中, 我们需要确定游戏的哪些部分在任何情况下都应运行, 哪些部分尽在游戏处于活动状态时运行.
在主循环中, 在任何情况都需要调用check_events()
, 以便游戏处于非活动状态时检测是否要退出游戏.
还需要不断更新屏幕, 以便等待玩家是否选择开始新游戏时能够修改屏幕.
其他函数仅在游戏处于活动状态才需要调用, 因为游戏处于非活动状态时, 不用更新游戏元素的位置.
现在, 它将在飞船用完后停止不动.
本章学习内容:
- 如何在游戏中添加大量相同的元素, 如创建一群外星人;
- 如何使用嵌套循环来创建元素网格, 还通过调用每个元素的方法update()移动了大量的元素;
- 如何控制对象在屏幕上移动的方向, 如何响应事件, 如有外星人到达屏幕边缘;
- 如何检测和响应子弹和外星人碰撞以及外星人和飞船碰撞;
- 如何在游戏中跟踪统计信息, 以及如何使用标志
game_active
来判断游戏是否结束
在后续一章中, 将添加一个Play按钮, 让玩家能够开始游戏, 以及游戏结束后再玩. 每当消灭一群外星人后, 我们都将加快游戏的节奏, 并添加一个积分系统.
在本章, 会结束游戏<外星人入侵>的开发.
- 添加一个Play按钮, 用于根据需要启动游戏以及在游戏结束后重启游戏;
- 在玩家等级提高时加快节奏, 并实现记分系统
添加一个Play按钮, 在游戏开始前出现, 在游戏结束后再次出现, 让玩家能够重开游戏. 当前, 游戏在玩家运行alien_invasion.py就开始了. 下面让游戏一开始进入非活动状态, 并提示玩家单击Play按钮来开始游戏.
具体见game_stats.py
Pygame没有内置创建按钮的方法, 我们创建一个Button类, 用于创建带标签的实心矩形.
具体见button.py
- 导入模块
pygame.font
, 它让Pygame能将文本渲染到屏幕上. __init__()
方法接收4个参数,msg
是要在按钮中显示ID文本.- 设置按钮尺寸
- 设置
button_color
让按钮的rect对象为亮绿色, 并设置text_color
让文本为白色 - 指定使用什么字体来渲染文本. 实参
None
让Pygame使用默认字体, 48指定文本的字号. - 创建一个表示按钮的rect对象, 并将其center属性设置为屏幕的center属性
- Pygame通过将要显示的文字渲染为图像来处理文本. 调用
prep_msg()
来处理.
具体见button.py的
prep_msg()
prep_msg()
接受实参self及要渲染为图像的文本(msg).- 调用
font.render()
将存储在msg中的文本转换为图像, 将该图像存储在msg_image中.font.render()
方法接收一个布尔实参, 指定开启还是关闭抗锯齿功能(抗锯齿让文本边缘更平滑)- 余下的两个实参分别是文本颜色和背景色, 将文本背景色设置为按钮的颜色(如不指定背景色, Pygame以透明渲染文本)
- 让文本图像在按钮上居中: 根据文本图像创建一个rect, 并将其center属性设置为按钮的center属性
创建方法draw_button()
, 调用它将按钮显示在屏幕上:
见button.py的
draw_button()
- 调用
screen.fill()
来绘制表示按钮的矩形 - 再调用
screen.blit()
, 向它传递一副图像以及该图像关联的rect对象, 从而在屏幕上绘制文本图像.
至此, Button 类创建好了
使用Button类来创建一个Play按钮.
具体见alien_invasion.py
导入Button类, 并创建一个play_button
实例, 将play_button
实例传递给update_screen()
, 以便更新屏幕时显示按钮
接下来, 修改update_screen()
, 以便在游戏处于非活动状态时显示Play按钮:
具体见game_functions.py的
update_screen()
为让Play按钮显示在所有其他元素的上面, 在绘制其他游戏元素后再绘制该按钮, 然后切换到新屏幕.
为在玩家单击Play按钮时开始新游戏, 需在game_functions.py中添加如下代码, 以监视与这个按钮相关的鼠标事件.
修改了check_events()
的定义, 在其中添加了形参stats和play_button. 将使用stats来访问标志game_active, 使用play_button
来检查玩家是否单击了Play按钮.
无论玩家单机屏幕的什么地方, Pygame都将检测到一个MOUSEBUTTONDOWN事件, 但我们只想让游戏在玩家用鼠标单机Play按钮时做出响应.
为此, 使用pygame.mouse.get_pos()
, 它返回一个元组, 包含玩家单击时的x y坐标. 将这些值传递给函数check_play_button()
,
这个函数使用collidepoint()
检查鼠标单击位置是否在Play按钮的rect内. 如果是这样, 将game_active
设为True, 游戏开始.
更新alien_invasion.py中的传参.
前面编写的代码只处理了玩家第一次单击Play按钮的情况, 而没有处理游戏结束的情况. 为在玩家每次单击Play按钮时都重置游戏, 需要重置统计信息, 删除现有的外星人和子弹, 创建一群新的外星人, 并让飞船居中.
具体见game_functions.py的
check_play_button()
更新了check_play_button()
的定义
- 重置游戏统计信息, 给玩家提供三艘新飞船.
- 将
game_active
设置为True(这样, 这个函数的代码执行完毕后, 游戏就会开始) - 清空编组aliens和bullets
- 创建一群新外星人
- 飞船居中
check_events()
定义需要修改.
现在, 每当玩家单击Play按钮时, 整个游戏都将正确地重置.
当前, Play按钮存在一个问题, 那就是即便Play按钮不可见, 玩家单击其所在的区域时, 游戏依然会作出响应. 如果玩家不小心单击了 Play按钮原来所处的区域, 游戏将重新开始. 为修复这个问题, 可让游戏仅在game_active为False时才开始:
见game_functions.py
仅当玩家单击了Play按钮且游戏当前处于非活动状态时, 游戏才重新开始.
为让玩家能够开始游戏, 我们要让光标可见, 但游戏开始后, 光标需要不可见.
见game_functions.py的
check_play_button()
游戏结束后, 重新显示光标.
见game_funcdtions.py的
ship_hit()
添加让玩家在按 P 时开始游戏的代码.
- 将
check_play_button()
的一些代码提出出来, 放到一个start_game()
的函数中 - 并在
check_play_button()
和check_keydown_events()
中调用该参数
每当玩家将屏幕上的外星人消灭干净后, 加快游戏节奏, 让游戏玩起来更难.
重新组织Settings类, 将游戏设置划分为静态和动态两组. 对于随着游戏进行而变化的设置, 确保它们在开始新游戏时被重置.
具体见settings.py
在__init__()
中初始化静态设置. 添加了speedup_scale
, 用于控制游戏节奏的加快速度: 2表示玩家每提高一个等级, 游戏节奏翻倍; 1表示游戏节奏
始终不变. 将其设置为1.1能够将游戏节奏提高到够快, 让游戏既有难度, 又并非非不可完成. 最后, 调用initialize_dynamic_settings()
,
以初始化随游戏进行而变化的属性.
该方法设置飞船/子弹/外星人的初始速度. 随游戏进行, 将提高这些速度, 而每当玩家开始新游戏时, 都将重置这些速度. 还设置了fleet_direction
,
使得游戏刚开始时, 外星人总是向右移动. 每当玩家提高一个等级, 都使用increase_speed()
来提高飞船/子弹/外星人的速度:
见settings.py中的
increase_speed()
为提高这些游戏元素的速度, 我们将每个速度设置都乘以speedup_scale
的值
在check_bullet_alien_collisions()
中, 我们在整群外星人都被消灭后调用increase_speed()
来加快游戏节奏, 再创建一群新的外星人.
具体见game_functions.py
每当玩家开始新游戏时, 我们都需要将发生了变化的设置重置为初始值, 否则新游戏开始时, 速度设置将是前一次游戏增加了的值:
具体见game_fuctions.py的
check_play_button()
实现一个计分系统, 以实时地跟踪玩家的得分, 并显示最高得分/当前等级和余下的飞船数.
具体见game_stats.py中的
GameStats()
为在每次开始游戏时都重置得分, 在reset_stats()
而不是__init__()
中初始化score
为在屏幕上显示得分, 我们首先创建一个新类Scoreboard.
具体见scoreboard.py
为将要显示的文本转换为图像, 调用prep_score()
具体见
prep_score()
首先将数字值stats.score
转换为字符串, 再将这个字符串传递给创建图像的render()
. 向render()传递屏幕背景色, 文本颜色.
将得分放在右上角, 并在得分增大导致这个数字更宽时让它向左延伸. 创建一个名为score_rect
的rect, 让其右边缘与屏幕右边缘相距20像素,
上边缘与屏幕上边缘相距20像素.
最后, 创建方法show_score()
, 用预显示渲染好的得分图像
见
show_score()
为显示得分, 在alien_invasion.py中创建一个ScoreBoard实例.
为显示得分, 将update_screen()
修改.
具体见game_functions.py
在update_screen()
的形参列表中添加了sb, 并在绘制Play按钮前调用show_score()
.
现在运行游戏, 会在屏幕右上角看到0.
下面来指定每个外星人值多少点!
为在屏幕上实时显示得分, 每当有外星人被击中, 都更新stats.score的值, 再调用prep_score()
更新得分图像.
在此之前, 需要制定玩家每击落一个外星人得多少点.
见settings.py
随着游戏进行, 将提高每个外星人的点数. 为确保该值重开游戏时被重置, 所以设置在initialize_dynamic_settings()
.
在check_bullet_alien_collisions()
中, 每当有外星人被击落时, 更新得分.
见game_functions.py
当有子弹撞到外星人, Pygame返回一个字典(collisions). 检查该字典是否存在, 如果存在, 就将得分加上一个外星人的点数.
再调用prep_score()
来创建一幅显示最新得分的新图像.
需要修改update_bullets()
, 确保在函数之间传递合适的实参.
还需要修改主while循环中调用update_bullets()
的代码.
当前, 可能遗漏一些被消灭外星人的得分. 如: 如果在一次循环中有2颗子弹射中外星人, 或者因子弹更宽而同时击中多个外星人, 玩家只能得到一个被消灭
的外星人的点数. 为修复这种问题, 调整检测子弹和外星人碰撞的方式.
在check_bullet_alien_collisions()
中, 与外星人碰撞的子弹都是字典collisions中的一个键; 而与每颗子弹相关的值都是一个列表, 其中包含
该子弹撞到的外星人. 遍历字典collisions, 确保将消灭的每个外星人的点数都计入得分.
如果字典collisions存在, 遍历其中的所有值. 每个值都是一个列表, 包含被同一个子弹击中的所有外星人. 对于每个列表, 都将一个外星人的点数
乘以其中包含的外星人数量, 并将结果加入当前得分中. 为测试这一点, 将子弹宽度设置为300, 进行核实.
处于较高等级时, 外星人点数应更高.
见settings.py
大多数街机风格的射击游戏都将得分显示为10的整数倍. 还将设置得分的格式, 在大数字中添加用逗号表示的千位分隔符.
见scoreboard.py
函数round()
通常让小数精确到小数点后多少位, 小数位数是由第二个实参指定的. 如果第二个实参指定为负数, round()
将圆整到最近的10, 100, 1000
等整数倍.
跟踪并显示最高得分, 给玩家提供要超越的目标. 将最高得分储存在GameStats
中.
在任何情况下都不会重置最高得分, 在__init__()
中而不是reset_stats()
中初始化high_score
.
修改ScoreBoard以显示最高分.
见scoreboard.py
方法show_score()
需要在屏幕右上角显示当前得分, 并在顶部中央显示最高得分.
见scoreboard.py
为检查是否诞生了新的最高分, 在game_functions.py中添加一个新函数check_high_score()
为在游戏中显示玩家的等级, 首先需要在GameStats中添加一个表示当前等级的属性. 为确保每次开始新游戏时都重置等级, 在reset_stats()
中初始化.
见game_stats.py
再在ScoreBoard中在当前等分下方显示等级.
见scoreboard.py
在check_bullet_alien_collisions()
中提高等级, 并更新等级图像.
见game_functions.py
为确保开始新游戏时更新积分和等级图像, 在按钮Play被单击时触发重置
见game_functions.py的
check_play_button()
并更新alien_invasion.py中的相关代码.
显示玩家还有多少艘飞船, 使用图像而不是数字. 在屏幕左上角绘制飞船图像. 首先, 让Ship继承Sprite, 以便能够创建飞船编组.
见ship.py
接下来, 修改ScoreBoard, 在其中创建一个可供显示的飞船编组.
见scoreboard.py
为在游戏开始时让玩家知道他有多少艘飞船, 我们在开始新游戏时调用prep_ships()
见game_functions.py
还在飞船被外星人撞到时调用prep_ships()
, 从而在玩家损失一艘飞船时更新飞船图像
见game_functions.py的
update_aliens()
和ship_hit()
和check_aliens_bottom()
最后, 更新alien_invasion.py的调用
- 如何创建用于开始新游戏的Play按钮
- 如何检测鼠标事件
- 在游戏处于活动状态时如何隐藏光标
- 如何随游戏进行调整节奏
- 如何实现积分系统
- 如何以文本和非文本方式显示信息
每当玩家关闭并重新开始游戏<外星人入侵>时, 最高分都将被重置. 请修复这个问题, 调用sys.exit()
前将最高分写入文件,
并将GameStats 中初始化最高分时从文件中读取它.
找出执行了多项任务的函数和方法啊, 对它们进行重构, 以让代码高效而有序.
例如, 对于check_bullet_alien_collisions()
, 将其中在外星人群被消灭干净时开始新登基的代码移到一个名为start_new_level()
的函数中; 又比如, 对于ScoreBoard的方法__init__()
, 将其中调用四个不同方法的代码移到一个名为prep_images()
的方法中, 以
缩短方法__init__()
. 如果你重构了check_play_button()
, 方法prep_images()
也可以为check_play_button()
或
start_game()
提供帮助.
重构项目前, 阅读Git使用方法, 了解如果重构时引入了bug, 如何将项目恢复到可正确运行的状态.
想想如何扩展游戏<外星人入侵>.如:
- 外星人也能够向飞船射击
- 添加盾牌, 让飞船躲到它后面, 使得只有从两边射来的子弹才能摧毁飞船
- 使用像
pygame.mixer
这样的模块来添加音效, 如爆炸声和射击声