pygame飞船射击游戏开发
游戏项目简介:
在游戏中,玩家控制一个最初出现在游戏界面底部中央的飞船。玩家可以通过左右键移动飞船,使用空格键发射子弹设计。游戏开始时,一群外星人出现在天空,并随时间向下移动。玩家的任务是射杀这些外星人。当玩家清理完屏幕上的外星人后,刷新一批新的外星人,其移动速度更快。当外星人撞到玩家的飞船,或者外星人到达屏幕底部,玩家损失一艘飞船。玩家损失一艘飞船。玩家损失三艘飞船后,游戏结束。
搭建项目环境
使用conda创建一个名为aline_game的python环境,本例使用的为python3.7环境,在该环境下使用pip模块安装pygame安装包。随后将创建好的conda环境配置到pycharm中,新建一个项目。注意该项目的绝对路径,在该项目的main函数下编译主程序。
代码如下:
1)Conda创建python3.7环境:
conda create -n aline_game python=3.7
- 激活该环境:conda activate aline_game
- 在该环境下安装pycharm包:
pip install pygame
等待conda在该环境下安装好,显示成功后,启动pycharm,新建项目并配置该conda环境。
此过程可移步毕老师b站《深度强化学习》配置过程,部分b站的conda教程会配置环境,开发包与项目的保存路径,按照毕老师教程直接使用默认路径比较方便。
开始游戏项目
2.1 创建一个运行游戏的类
开始开发该项目前首先应该创建一个管理游戏的类。当启动一次游戏时,就会创建该类下的实例,该类应该有如下功能:
- 该类负责管理该游戏项目的资源和行为(管理堆栈)
- 该类的实例为初始化游戏并创建游戏资源(类的实例化)
- 该类下定义运行游戏的主循环函数
编写规范:
1)定义类时做注释
- 类下的方法做字符注释
- 调用方法代码过长时在下方做注释
代码如下:
运行结果:
方法释义
该类下的属性screen的对象是一个surface。在pygame中,对象surface是屏幕的一部分,用于显示所有游戏的图形元素(后续的外星人,飞船,子弹等),方法set_mode()返回的surface表示整个游戏窗口,通过元组传参设置该窗口大小。
游戏的运行由该类下的方法run_game()控制。该方法是一个while循环,该循环包含一个events循环,这个循环下负责记录玩家的鼠标/键盘操作,并响应该操作。当事件为点击窗口的x摁(QUIT)键时,通过方法sys模块下的方法exit()退出游戏。
当访问pygame检测到的事件,使用函数pygame.event()返回一个列表,在列表里记录下上一次调用后发生的所有事件。
此方法使得主循环不断擦除旧画面,并显示新画面可见。
运行游戏的类下设置背景色
我们将RGB值为(230,230,230)的颜色赋值给该类下的self.bg_color属性。
通过方法fill()将该颜色填充至游戏窗口,需要注意,必须是在循环内的语句,否则会返回默认的黑色窗口。
创建设置类
每次给游戏添加新功能时,需要引入一些新的设置。我们创建一个名为setting.py的新模块,其中包含一个名为setting的类,需要修改游戏的某些setting时,只需要在该模块下setting类下的一些值,当项目变大时方便管理。注意将该模块保存在main目录下。
Settings模块下的代码:
Main模块中代码修改如下:(实际上就是用settings模块中,Setting类下的属性替换)
添加玩家操纵的飞船
在main所在目录下新建一个名为images的文件夹,将以下图片以ship.bmp为文件名,保存在images文件夹下。后续所有的图片都保存在此文件夹下,方便调用。
该项目目录如下:
创建ship类
创建ship类模块,该模块下有ship类,负责管理飞船大部分行为。
代码如下:
该类实例化中接收两个参数:
- self既该类本身
- ai_game 该参数可理解为一个指针,指针指向main函数中的实例ai,而实例ai使用了运行游戏的类,该类又导入了例如settings的类,所以ship类实际上接收了该游戏实例化后所有的引用,即ship在实例化后,既有自己的初始属性,同时也会接受实例化所有的类(玩家执行操作后,实例发生改变,发生改变后的实例又传给ship,使得ship接收玩家的操作)
设置ship的属性self.image ,通过方法pygame.image.load()加载图片,并赋值给该属性,该函数返回一个表示飞船的surface。
加载图形后,使用get_rect()获取对象surface的属性rect,以便后面能够使用该属性指定飞船的位置。
关于对象和属性,两者其实是在程序不同阶段时体现的性质做的划分。以本例为例,self .rect是该类实例时初始化的一个属性。
get_rect()返回一个surface的对象,所有self.rect的属性就是一个surface对象。
self.rect对象有midbottom属性,该属性使得这个rect对象处在屏幕中间。
在屏幕上绘制飞船
在主类下初始化属性ship,该类为Ship类,并接收主程序的各种属性。
在屏幕中循环刷新时显示该ship实例
可以通过更改image文件夹下的ship.bmp文件修改图像。
重构主类下的方法:_check_events()和_update_screen()
主循环下的这两个方法会随着项目的扩大变得很长,重构的目的是使得项目变得复杂化后更容易拓展。
在主循环中,因为添加了各种方法,当进行项目管理时,最好将同一功能的代码转移到同一代码块内。
主循环的方法可分为两类,一类是管理操作事件,一类是管理屏幕画面,依据其功能我们可以分别建立两种方法。
重构后代码
注意:该方法是定义在初始化函数同级别下的方法,注意缩进。
2.6驾驶飞船
此部分是为了让玩家能够左右移动飞船。此部分的代码是用户事件按照←或者→箭头做出响应,控制ship.image图像的移动。
2.6.1 响应摁键
在Ship类下:
已知通过以下代码得到该图形的外接矩形
另外
属性self.rect接收到的是对象surface
处理rect(surface)对象时,可使用矩形四角和中心的x坐标和y坐标。
要使得图像在屏幕居中,可通过设置对象rect的属性center。Centerx:
要让游戏元素与屏幕边缘对齐,可使用属性top、bottom、left、right。同时还可以通过属性rect.x,rect.y来改变图形的x,y坐标。
每当用户摁键时,都会在pygame中注册一个事件。事件都是通过方法pygame.event.get()获取的,因此需要在重构后的方法_check_events()中指定要检查哪种类型的事件。每次摁键都会被注册为一个KEYDOWN事件,需要筛选出有用的指令执行。
Pygame检测到KEYDOWN事件时,需要检查摁下是否触发行动的摁键。在本次设计的游戏中有效指令为←与→,当触发此类事件时,就会改变ship的属性rect.centerx值,飞船的图形会在主界面移动。
2.6.2 允许向右持续移动
单个摁键移动速度太慢,长摁持续移动会大大简化操作,这时可以让游戏检测pygam.KEYUP事件以及一个名为moving_right的标志来实现持续移动。
当该标志为False时,ship不移动。当玩家摁下指定摁键(←,→)时,将该标志检测为True,当玩家松开时该标志重新设置为False。
飞船的属性都由ship类控制,所以添加一个新的属性self.moving_flag,默认值为false。同时创建一个方法update(),检查该属性的状态,如果为True,就调整飞船的位置。这样可以在主函数的while下调用这个方法,以调整飞船的位置。
其实现步骤如下:
- 在ship类下添加moving_right属性控制飞船移动
- 在ship类下添加方法,通过读取moving_right属性属性执行动作(true时移动,false时不动)
- 修改main函数下的while循环,调用该方法
- 通过摁键动作生成的事件,来修改标志moving_right的值
2.6.3 允许左右持续运动
此部分与上述方法类似,其步骤如下:
- 在ship类下创建标志moving_lift属性,默认为False
- 在ship类下的update()方法下,当ship.moving_lift为True时运行左移指令在
- 在_check_events()下添加当摁键上抬时,修改属性ship.moving_lift为False,终止移动
2.6.4 调整飞船速度
摁键设置为坐标移动为1,如果想调整飞船速度,可以在setting类添加属性ship_speed,用于控制飞船速度。
控制飞船移动的ship属性ship.rect.x即对象rect的x轴位置,此x属性只接收int型,所以在调整移速时应该在ship下新设属性接收浮点数。如何接收setting模块下的ship speed属性?因为ship类传递了整个实例AlineInvasion,只需要将settings模块下实例化的类settings作为ship类下的属性,所以优先设置属性。
设置好属性以后,就可以使用self.settings.ship_speed里的值了。
因为rect只能接收整形,所以可以用一个强制类型转换,设置一个新变量作为速度属性
最后将update()方法稍微修改后即可
2.6.5 限制飞船的活动范围
因为飞船图像位置是由ship下的undate()方法处理,所以只需要在update方法下限制ship.x属性,使其不超过屏幕范围,即可实现此功能。
Rect对象是从左为0计数的。
2.6.6 重构_check_events()
_check_events 管理了所有鼠标事件,后续添加新的诸如射击开火后会变得臃肿。此处根据摁键的步骤,分别重构两个函数,一个处理KEYDOWN事件,一个处理KEYUP事件。
2.6.7 摁Q退出
除了pycham的显示界面的关闭摁键外,我们设置一个方便退出的快捷键。这个快捷键同样可以帮助我们在进入全屏模式下便于退出游戏。
在上一节的重构中,我们已经将_check_events()方法拆成两部分,一部分为_check_keydown_events(),一部分为_check_keyup_events()。只需_check_keydown_events()上调用方法sys.exit()即可。
2.6.8 全屏模式
许多游戏在启动时会自动进入全屏模式,pycharm同样支持全屏模式。只需要在主类实例化时设置一个默认值即可。
本例未作更改,更习惯于小窗工作。
2.7回顾开发过程
主文件main.py包含AlineInvasion类。该类是运行游戏的类。每启动一次游戏就是将该类进行一次实例化。
当主类实例化时,因为主类下会导入其他的类,所以都会实例化,类实例化后可以重新作为另一个类的属性,在该类再次实例化后在程序进程下不断体现和更新该属性。
该类实例化后的方法为游戏的进程,主要运行一个while循环。While循环内,重构后包含三个清晰的方法,检查摁键指令-》ship实例根据指令移动-》通过surface对象显示在pycham窗口中。
在_check_events()方法下,为了便于后续添加功能和方便维护,又将该方法重构为两部分:
根据检查摁键的状态,重构为_check_keyup_events()与_check_keydown_events()
2.8 飞船射击
2.8.1 在Settings类添加子弹设置
在settings模块下设置子弹的属性
这些属性方便在以后创建Bullet类时调用。
2.8.2 创建Bullet类
在此处需要导入两项:
注意第二项,从包里导入了Sprite类,以此类为父类创建子类Bullet
Super()函数使得子类Bullet可以使用父类Sprite里的所有函数,我们通过调方法__init__(),让Bullet类下的实例包含父类下的所有属性和方法。
对于bullet类型的实例,需要解释几个方法
- rect属性实例化后的是pygame下的Rect类,该类通过传递的参数会生成一个指定参数大小的矩形,该类的x,y坐标为(0,0)但
- 我们将bullet的rect对象(生成的矩形)至于ship.rect的顶部和中部
- 我们设置了一个可以接收浮点数的变量self.y,让他得到bullet.rect的位置(rect只能储存整形)
在完成以上属性后,我们可以设置Bullet类下特有的方法。
方法update()与ship.update没区别,实时更新bullet.rect的位置,只是子弹的x值不变
当需要再次生成一个新子弹时,可以使用方法draw_bullet()。
2.8.3 将子弹存储在编组内
我们已经定义了Bullet类和必要的设置以后,就可以开始在main下编写代码实现发射子弹的过程了。
我们可以用pygame.sprite.Grop创建一个实例,该实例可以管理程序生成的所有子弹,这个实例被称为编组(group),类似于列表。
主类下定义属性管理所有子弹
While循环调用方法更新子弹位置
2.8.4 开火
添加开火较为简单,只需更改_check_keydown_events()时生成一颗子弹,随后在_up_date_screen()上在调用flip()前重绘每颗子弹。
其步骤如下:
- 导入bullets模块
- 在主类下添加属性:一个管理所有子弹的编组
- 在_check_keydown_events()下添加新的摁键响应
- keydown()下调用方法
- 在主类下定义该方法
- 在画面中显示生成的子弹
方法bullets.sprites()返回一个列表,包含所有的sprite实例。Sprite类的实例包含检测外接矩形碰撞的方法,因为后续要检测物体的碰撞,所以不能单纯的返回一个rect对象。
2.8.5 删除消失的子弹(优化游戏)
对于主类初始化的属性bullets为一个空的编组,该编组会根据摁键空格的动作,不断将新生成的子弹储存至该编组内。在pygame窗口外这些子弹仍然存在,这点我们在移动飞船时已经知道。为了节约内存,减小cpu的负担,我们希望删除超出pygame窗口的子弹。
这点可以通过检测Pygame中的rect对象的属性bottom是否为0.(内置的flag)
只需要添加一个新的方法,随后在主循环上调用即可,其实现步骤如下:
在主循环下删除多余的子弹
2.8.6 限制子弹的数量
限制子弹的数量同样可以节约资源同时增加游戏难度。其实现步骤如下:
- 在settings模块下增加一个新的属性
- 在控制开火的函数下增加一条判断语句
Len()返回该编组的元素数,增加开火条件限制。
2.8.7 主循环下创建方法_update_bullets()
将管理子弹行为放到同一个重构函数下
在主循环下添加该方法
2.9 添加外星人
外星人同样可以使用sprite创建一个子类,与bullet类似。
首先先将飞船保存在该项目的image文件夹下。
2.9.1 创建Aline类
我们想要外星人完成自动生成,自动左右上下移动的功能。
2.9.2 创建Aline实例
其实现步骤如下:
- 在main下导入aline下的aline类
- 在主类下添加属性,一个新的空编组管理所有外星人实例。在初始化编组后,再实例化一个外星人存储至该编组内。
- 编写此方法,每次生成一个游戏实例,就会初始化此方法一次
- 更新_update_screen函数,将编组内的外星人显示在屏幕的指定位置上
完成后运行main,得到下图
2.9.3创建一群外星人
创建一群外星人,我们需要确定一行容纳多少外星人,绘制多少行。这需要确定每个外星人的间距,同时控制两侧边距,使两侧都有余量能够让外星人移动。
2.9.3.1 确定一行容纳多少外星人
本项需要三个参数:
- pygame的窗口大小
- 外星人的宽度
- 两边留下的余量
Pygame窗口宽度在setting.screen_width ,外星人的宽度我们还不知道,但可以用一个变量名表示aline_width。我们可以将两侧宽度留下2*aline_width。
Available_space_x = settings.screen_width - (2*alien_width)
确定以上后可以用以下公式确定显示数目
Numbe_alines_x = avilable_space_x
2.9.3.2 创建一行外星人
代码如下,通过for循环加入
实现结果:
2.9.3.3 重构_create_fleet()
2.9.3.4 添加行
在main下:
此处设置了一个嵌套的循环
增加一个形参记录行号,随后逐行添加外星人。
2.9.4 使外星人群运动
Alien群的移动与bullet的移动没有区别,此处略过。但需要添加一个边界检测,当外星人群移动到pygame边界时,会向反方向移动
2.9.4.1 检查碰撞
在alien下编写一个方法,检查alien是否已经碰撞到屏幕边缘。
2.9.4.2 碰撞后下移外星人群并改变移动方向
2.9.5射击外星人
在pygame中,击中实际上就是两个sprite对象检测碰撞。
2.9.5.1 检测子弹与外星人的碰撞
在pygame中,调用sprite.groupcollide()将两个编组的rect进行比较。在本例中,子弹和外星人进行比较,返回一个字典,字典的键是一颗子弹,值是关联的击中的外星人。
当后续两个参数设置为True,True时,表示碰撞后传递的键和值都消失。通过修改True和False值。
2.9.5.2 生成新的外星人
我们想要实现消灭完屏幕中的外星人后,刷新一群新的外星人。
只需要每次update画面后检测储存sprite类型的编组group是否为空即可。
当检测为空时,再次调用方法_create_fleet()添加新的外星人。
2.9.6 结束游戏
我们对游戏设置两种结束条件
- 当外星人与飞船相撞
- 当外星人到达屏幕的底部
2.9.6.1 检测外星人与飞船碰撞
我们通过pygame的内置函数spritecollideany()来检测碰撞。该函数接收两个实参,sprite对象和编组。当sprite与编组元素发生碰撞后返回发生碰撞的编组元素。如果没有发生碰撞,则返回None。
我们将这段代码编入方法_update_aline()中。
测试结果:
2.9.6.2 响应外星人和飞船的碰撞
我们在项目初期已经确定了,飞船允许被外星人击中三次,所以在更改实例时,我们需要先创建一个索引,跟踪游戏的统计信息。这里创建一个新类Gamestats。
我们首先设置一个响应碰撞以后的方法,方便调用
Ship类下的center_ship()保证生成的飞船位于底部中部
同时我们需要希望每触发一次该方法,即外星人与飞船发生一次碰撞,游戏的统计数据ship_left减1.
此时,每次发生碰撞游戏仍然会不断重置,因为ship_left没有设置flag检测负数,该值会一直减1不会终止。
2.9.6.3 检测外星人到达底部
我们在主类下添加一个新的方法,当外星人到达底部屏幕时,会触发飞船与外星人相撞同样的效果。
该方法用于检测是否有外星人到达底部屏幕,只要有一个就能跳出该循环并执行与外星人与飞船碰撞后相同的方法了。
我们在_update_alien()下调用该方法。
2.9.6.4 设置游戏结束flag终止游戏
在GameStats下设置一个检测游戏终止的flag,每生成一次游戏实例,该flag刷新一次。
将该判断条件放入方法_ship_hit()上
检查ship_left的数量,当小于零时,改变flag的值为False
随后将检测条件放入住循环内:
游戏终止时就会卡在此界面:
玩家需要手动退出。然后创建一个新的实例。
总结
Python的可读性会随着代码量的增大而变差。尽量在开始阶段设置好类的属性,否则在不断嵌套的过程中会非常难以重构变得庞大的主函数。
本次开发结束,添加计分和设置其他摁键交给其他感兴趣者了,需要源码的可以评论。