返回顶部

python项目1--【外星人入侵游戏】之武装飞船

python项目1--【外星人入侵游戏】之武装飞船

我们来开发一个名为《外星人入侵》的游戏。为此将使用Pygame,这是一组功能强大而有趣的模块,可用于管理图形、动画乃至声音,让你能够更轻松地开发复杂的游戏,通过使用Pygame来处理在屏幕上绘制图像等任务,可将重点放在程序的高级逻辑上。

在本篇中,我们将安装Pygame,再创建一艘能够根据用户输入左右移动和射击的飞船。在接下来的两篇,你将创建一群作为射杀目标的外星人,并改进该游戏:限制可供玩家使用的飞船数,并且添加记分牌。

在开发这款游戏的过程中,你还讲学习如何管理包含多个文件的项目。你将重构很多代码并管理文件的内容,以确保项目组织有序以及提高效率。

开发游戏是趣学语言的理想方式。看别人玩你编写的游戏能获得满足感,而编写游戏有助于你明白专业级游戏是怎么编写出来的。在阅读本章的过程中,请手动输入并运行代码,已明白各个代码块对整个游戏所做的共享,并且尝试不同的值和设置,以对如何改进游戏的交互性有更升入的认识。

注意:游戏《外星人入侵》将包含很多不同的文件,因此请在系统中新建一个名为alien_invasion的文件夹,并将该项目的所有文件都存储到该文件夹中,这样相关的import语句才能正确工作。

另外,如果你熟悉版本控制,可能想将其用于这个项目;如果你没有使用过版本控制,请参考我的下一篇博客。

一、规划项目

开发大型项目时,制定好规划后再动手编写代码很重要。规划可确保你补偏离轨道,从而提高项目成功的可能性。下面来编写有关《外星人入侵》的描述,其中虽然没有涵盖足额款游戏的所有细节,但能让你清楚的指导该如何动手开发。

在游戏《外星人入侵》中,玩家控制一艘最初出现在屏幕底部中央的飞船。玩家可以使用方向键左右移动飞船,还可以使用空格键射击。游戏开始时,一群外星人出现在天空中,并向屏幕下方移动。玩家的任务是射杀这些外星人。玩家将所有外星人都消灭干净后,将出现一群新的外星人,其移动速度更快。只要有外星人撞到玩家的飞船或到达屏幕底部,玩家就损失一艘飞船。玩家损失三艘飞船后,游戏结束。

开发的第一个阶段将创建一艘飞船,它可左右移动,并且能在用户按空格键时开火。设置好这种行为后,就可以创建外星人并提高游戏的可玩性了。

二、安装Pygame

开始编码前,先来安装Pygame。可使用pip来帮助下载并安装 python包。要安装Pygame,在终端提示符下执行如下命令:

python -m pip install --user pygame

这个命令让Python运行pip模块,将pygame包添加到当前用户的Python安装中。如果你运行程序或启动终端会话时使用的命令不是python,而是python3,可执行如下命令来安装Pygame

python3 -m pip install --user pygame

注意:如果该命令在macOS系统中不管用,请尝试在不指定标志--user的情况下再次执行。

三、开始游戏项目

开始开发游戏《外星人入侵》吧。首先要创建一个空的Pygame窗口,供之后用来绘制游戏元素,如飞船和外星人。我们还将让这个游戏响应用户输入,设置背景色,以及加载飞船图像。

1. 创建Pygame窗口及响应用户输入

下面创建一个表示游戏的类,以创建空的Pygame窗口。为此,在文本编辑器中新建一个文件,将其保存为alien_invasion.py,再在其中输入如下代码:

首先,导入模块syspygame。模块pygame包含开发游戏所需的功能。玩家退出时,我们将使用模块sys中的工具来退出游戏。

为开发游戏《外星人入侵》,我们创建了一个表示它的类,名为AlienInvasion。在这个类的方法__init__()中,调用函数pygame.init()来初始化背景设置,让Pygame能够正确地工作。然后,调用pygame.display.set_mode()来创建一个显示窗口,游戏的所有图形元素都将在其中绘制。实参(1200, 800)是一个元组,指定了游戏窗口的尺寸为宽1200像素、高800像素(你可以根据自己的显示器尺寸调整这些值)。将这个显示窗口赋给属性self.screen,让这个类中的所有方法都能够使用它。

赋给属性seld.screen的对象是一个surface。在Pygame中,surface是屏幕的一部分,用于显示游戏元素。在这个游戏中,每个元素(如外星人或飞船)都是一个surface。display.set_mode()返回的surface表示整个游戏窗口。激活游戏的动画循环后,每经过一次循环都将自动重绘这个surface,将用户输入触发的所有变化都反映出来。

这个游戏由方法run_game()控制。该方法包含一个不断运行的while循环,而这个循环包含一个事件循环以及管理屏幕更新的代码。事件是用户玩游戏时执行的操作,如按键或移动鼠标。为程序响应事件,可编写一个事件循环,以侦听事件并根据发生的时间类型执行合适的任务,后面的for循环就是一个事件循环。

为访问Pygame检测到的事件,我们使用了函数pygame.event.get()。这个函数返回一个列表,其中包含它在上一次被调用后发生的所有时间。所有键盘和鼠标事件都将导致这个for循环运行。在这个循环中,我们将编写一系列if语句来检测并响应特定的事件。所有键盘和鼠标时间都将导致这个for循环运行。在这个循环中,我们将编写一系列if语句来检测并响应特定的事件。例如,当玩家点击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出游戏。

最后,调用了pygame.display.flip(),命令Pygame让最近绘制的屏幕可见。在这里,它在每次执行while循环时都绘制一个空屏幕,并擦去旧屏幕,使得只有新屏幕可见。我们移动游戏元素时,pygame.display.flip()将不断更新屏幕,以显示元素的新位置,并且在原来的位置隐藏元素,从而营造平滑移动的效果。

在这个文件末尾,创建一个游戏实例并调用run_game()。这些代码放在一个if代码块中,仅当直接运行该文件时,它们才会执行。如果此时运行alien_invasion.py,将看到一个空的Pygame窗口。

2. 设置背景色

Pygame默认创建一个黑色屏幕,这太乏味了。下面来讲背景设置为另一种颜色,这是在方法__init__()末尾进行的:

在Pygame中,颜色是以RGB值指定的,这种颜色由红色、绿色和蓝色值组成,其中每个值的可取值范围都是0~255。颜色值(255, 0, 0)表示红色,(0, 255, 0)表示绿色,而(0, 0, 255)表示蓝色。通过组合不同的RGB值,可创建1600万种颜色。在颜色值(230, 230, 230)中,红色、绿色和蓝色的量相同,它生成一种浅灰色。我们将这种颜色赋给了self.bg_color

接着,调用方法fill()用这种背景色填充屏幕。方法fill()用于处理surface,只接受一个实参:一种颜色。

3. 创建设置类

每次给游戏添加新功能时,通常也将引入一些新设置。下面来编写一个名为settings的模块,在其中一个名为Settings的类,用于将所有设置都存储在一个地方,一面在代码中到处添加设置。这样,每当需要访问设置时,只需使用一个设置对象。另外,在项目增大时,这使得修改游戏的外观和行为更容易:要修改游戏,只需修改(接下来将创建的)settings.py中的一些值,而无需查找散布在项目中的各种设置。

在文件夹alien_invasion中,新建一个名为settings.py的文件,并在其中添加如下Settings类:

为在项目中创建Settings实例并用它来访问设置,需要将alien_invasion.py修改成下面这样:

在主程序文件中,导入Settings类,调用pygame.init(),再创建一个Settings实例并将其赋给self.settings。创建屏幕是,使用了self.settings的属性screen_widthscreen_height。接下来填充屏幕时,也使用了self.settings来访问背景色。

如果此时运行alien_invasion.py,结果不会有任何不同,因为我们只是将设置移到了不同的地方。现在可以在屏幕上添加新元素了。

四、添加飞船图像

下面将飞船加入游戏中。为了在屏幕上绘制玩家的飞船,我们将加载一幅图像,再使用Pygame方法blit()绘制它。

为游戏选择素材时,务必要注意许可。最安全、最不费钱的方式是使用一些图片分享网站提供的免费图形,无需授权许可即可使用并修改。

在游戏汇总几乎可以使用任何类型的图像文件,但使用位图.bmp文件最为简单,因为Pygame默认加载位图。虽然可配置Pygame以使用其他文件类型,但有些文件类型要求你在计算机上安装相应的图像库,大多数图像为.jpg.png.gif格式,但可使用PhotoshopGIMPPaint等工具将其转换为位图。

选择图像时,要特别注意背景色。请尽可能选择背景为透明或纯色的图像,便于使用图像编辑器将其背景替换为任意颜色。图像的背景色与游戏的背景色匹配时,游戏看起来最美观。你也可以将游戏的背景色设置成图像的背景色。

就游戏《外星人入侵》而言,可使用文件ship.bmp(如下面所示)。这个文件的背景色与项目使用的设置相同。请在项目文件夹(alien_invasion)中新建一个名为images的文件夹,并将文件ship.bmp保存在其中。

1. 创建Ship类

选择用于表示飞船的图像后,需要将其显示到屏幕上。我们创建一个名为ship的模块,其中包含ship类,负责管理飞船的大部分行为。

Pygame之所以高效,是因为它让你能够像处理矩形(rect对象)一样处理所有的游戏元素,即便其形状并非矩形。像处理矩形一样处理游戏元素之所以高效,是因为矩形是简单的几何形状。例如,通过将游戏元素视为矩形,Pygame能够更快地判断出它们是否发生了碰撞。这种做法的效果通常很好,游戏玩家几乎注意不到我们处理的并不是游戏元素的实际形状。在这个类中,我们将把飞船和屏幕作为矩形进行处理。

定义这个类之前,导入了模块pygameShip的方法__init__()接受两个参数:引用self和指向当前AlienInvasion实例的引用。这让Ship能够访问AlienInvasion中定义的所有游戏资源。首先,将屏幕赋给了Ship的一个属性,以便在这个类的所有方法中轻松访问。接着,使用方法fer_rect()访问屏幕的属性rect,并将其赋给了self.screen_rect,这让我们能够将飞船放到屏幕的正确位置。

调用pygame.image.load()加载图像,并将飞船图像的位置传递给它。该函数返回一个表示飞船的surface赋给了self.image。加载图像后,使用get_rect()获取相应surface的属性rect,以便后面能够使用它来指定飞船的位置。

处理rect对象时,可使用矩形四角和中心的x坐标y坐标。可通过设置这些值来指定矩形的位置。要让游戏元素居中,可设置相应rect对象的属性centercenterxcentery;要让游戏元素与屏幕边缘对齐,可使用属性topbottomleftright。除此之外,还有一些组合属性,如midbottommidtopmidleftmidright。要调整游戏元素的水平或垂直位置,可使用属性xy,分别是相应矩形左上角的x坐标y坐标。这些属性让你无须做游戏开发人员原本需要手工完成的计算,因此会经常用到。

注意:在Pygame中,原点(0, 0)位于屏幕左上角,向右下方移动时,坐标值将增大。在1200 x 800的屏幕上,原点位于左上角,而右下角的坐标为(1200, 800)。这些坐标对应的是游戏窗口,而不是物理屏幕。

我们要将飞船放在屏幕底部的中央。为此,将self.rect.midbottom设置为表示屏幕的矩形的属性midbottom。Pygame使用这些rect属性来放置飞船图像,使其与屏幕下边缘对其并水平居中。

最后,定义了方法blitme(),它将图像绘制到self.rect指定的位置。

2. 在屏幕上绘制飞船

下面更新alien_invasion.py,创建一艘飞船并调用其方法blitme()

导入Ship类,并在创建屏幕后创建一个Ship实例。调用Ship()时,必须提供一个参数:一个AlienInvasion实例。在这里,self指向的是当前AlienInvasion实例。这个参数让Ship能够访问游戏资源,如对象screen。我们将这个Ship实例赋了self.ship

填充背景后,调用ship.blitme()将飞船绘制到屏幕上,确保它出现在背景前面。

现在如果运行alien_invasion.py,将看到飞船位于游戏屏幕底部的中央,如下图所示。

五、重构:方法_check_events()和_update_screen()

在大型项目中,经常需要在添加新代码前重构既有代码。重构旨在简化兼有代码的结构,使其更容易扩展。下面将把越来越长的方法run_game()拆分成两个辅助方法(helper method)。辅助方法在类中执行任务,但并非是通过实例调用的。在Python中,辅助方法的名称以单个下划线打头。

1. 方法_check_events()

我们将把管理实践的代码移到一个名为_check_events()的方法中,以简化run_game()并隔离事件管理循环,通过隔离事件循环,可将事件管理与游戏的其他方面(如更新屏幕)分离,

下面是新增方法_check_events()后的AlienInvasion类,只有run_game()的代码收到影响:

新增方法_check_events(),并检查玩家是否单击了关闭窗口按钮的代码移到该方法中。

要调用当前类的方法,可使用句点表示法,并指定变量名self和要调用的方法的名称。我们在run_game()的while循环中调用这个新增的方法。

2. 方法_update_screen()

为进一步简化run_game(),将更新屏幕的代码移到一个名为_update_screen()的方法中:

我们将回执背景和飞船以及切换屏幕的代码移到了方法_updata_screen()中。现在,run_game()中的主循环简单多了,很容易看出在每次循环中都检测了新发生的时间并更新了屏幕。

如果你开发过大量的游戏,可能在就开始像这样将代码放到不同的方法中了。不过如果你从未开发过这样的项目,可能不知道如何组织代码。这里采用的做法是,先编写可行的代码,等代码越来越复杂时再进行重构,以向你展示真正的开发过程:先编写尽可能简单的代码,等项目越来越复杂后对其进行重构。

对代码进行重构使其更容易扩展后,可以开始处理游戏的动态页面了!

六、驾驶飞船

下面来让玩家能够左右移动飞船。我们将编写代码,在用户按左或右箭头键时做出相应。我们将首先专注于向右移动,再使用同样的原理来控制向左移动。通过这样做,你将学会如何控制屏幕图像的移动。

1. 响应按键

每当用户按键时,都将在Pygame中注册一个事件。事件都是通过方法pygame.event.get()获取的,因此需要在方法_check_events()中指定要检查哪些类型的事件。每次按键都被注册为一个KEYDOWN事件。

Pygame检测到KEYDOWN事件时,需要检查按下的是否是触发行动的键。例如,如果玩家按下的是右箭头键,就增大飞船的rect.centerx值,将飞船向右移动:

在方法_check_events()中,为事件循环添加一个elif代码块,以便在Pygame检测到KEYDOWN事件时做出相应。我们检查按下键(event.key)是否是右箭头键(pygame.K_RIGHT)。如果是,就将self.ship.rect.centerx的值加1,从而将飞船向右移动。

如果现在运行alien_invasion.py,则每按右箭头一次,飞船都将向右移动1像素。这是一个开端,但并非控制飞船的高效方式。下面来改进控制方式,允许持续移动。

2. 允许持续移动

玩家按住右箭头不放时,我们希望飞船不断向右移动,直到玩家松开位置。我们将让游戏检测pygame.KEYUP事件,以便知道玩家何时松开右箭头键。然后,结合使用KEYDOWNKEYUP事件以及一个名为moving_right的标志来实现持续移动。

当标志moving_ringtFalse时,飞船不会移动。玩家按下右箭头键时,我们将该标志设置为True,在玩家松开时将该标志重新设置为False

飞船的属性都由Ship类控制,因此要给这个类添加一个名为moving_right的属性和一个名为update()的方法。方法update()检查标志moving_right的状态。如果该标志为True,就调整飞船的位置。我们将在while循环中调用这个方法,以调整飞船的位置。

下面是对Ship类所做的修改:

在方法__init__()中,添加属性self.moving_right,并将其初始值设置为False。接下来,添加方法update(),在前述标志为True时向右移动飞船。方法update()将通过Ship实例来调用,因此不是辅助方法。

接下来,需要修改_check_events(),使其在玩家按下右箭头键时将moving_right设置为True,并在玩家松开时将moving_right设置为False

首先,修改游戏在玩家按下右箭头键时响应的方式:不直接调整飞船的位置,而只是将moving_right设置为True。然后,添加一个新的elif代码块,用于响应KEYUP事件:玩家松开右箭头时(K_RIGHT)时,将moving_right设置为False

最后需要修改run_game()中的while循环,以便每次执行循环时都调用飞船的方法update()

飞船的位置将在检测到键盘事件后(但在更新屏幕前)更新,这样,玩家输入时,飞船的位置将更新,从而确保使用更新后的位置将飞船绘制到屏幕上。

如果现在运行alien_invasion.py并按住右箭头键,飞船将持续向右移动,直到松开为止。

3. 左右移动

现在飞船能够持续向右移动了,添加向左移动的逻辑也很容易。我们将再次修改ship类和方法_check_events()。下面显示了对Ship类的方法__init__()update()所做的相关修改:

在方法__init__()中,添加标志self.moving_left。在方法update()中,添加一个if代码块而不是elif代码块,这样如果玩家按下了左右箭头键,将先增加再减少飞船的rect.x值,即飞船的位置保持不变。如果使用一个elif代码块来处理向左移动的情况,右箭头键将始终处于优先地位。从向左移动切换到向右移动时,玩家可能同时按住左右箭头键,此时前面的做法让移动更准确。

还需对_check_events()做两方面的调整:

如果因玩家按下K_LEFT键而触发了KEYDOWN事件,就将moving_left设置为True。如果因玩家松开K_LEFT而触发了KEYUP事件,就将moving_left设置为False。这里之所以可以使用elif代码块,是因为每个事件都只与一个键相关联。如果玩家同时按下左右箭头键,将检测到两个不同的事件。

如果此时运行alien_invasion.py么就能够持续左右移动飞船。如果同时按下左右箭头键,飞船将纹丝不动。

下面来进一步优化飞船的移动方式:调整飞船的速度,以及限制飞船的移动距离,以免其消失在屏幕之外。

4. 调整飞船的速度

当前,每次执行while循环时,飞船最后移动1像素,但可在Settings类中添加属性ship_speed,用于控制飞船的速度。我们将根据这个属性决定飞船在每次循环时最多移动多远。下面演示了如何在settings.py中添加这个新属性:

ship_speed的初始值设置为1.5。现在需要移动飞船时,每次循环将移动1.5像素而不是1像素。

通过将速度设置指定为小数值,可在后面加快游戏节奏时更细致地控制飞船的速度。然而,rectx等属性智能存储整数值,因此需要对Ship类做些修改:

首先,给Ship类添加属性settings,以便能够在update()中使用它。鉴于现在调整飞船的位置时,将增减一个单位为像素的小数值,因此需要将位置赋给一个能够存储飞船的位置,定义一个可存储小数值的新属性self.x。使用函数float()self.rect.x的值转换为小数,并将结果赋给self.x

现在在update()中调整飞船的位置时,将self.x的值增减settings.ship_speed的值。更新self.x后,再根据它来更新控制飞船位置的self.rect.xself.rect.x只存储self.x的整数部分,但对显示飞船而言,这问题不大。

现在可以修改ship_speed的值了。只要它的值大于1,飞船的移动速度就会比以前更快。这有助于让飞船的反应速度足够快,以便射杀外星人,还让我们能够随着游戏的进行加快游戏的节奏。

注意:如果你使用的是macOS,可能发现即便ship_speed的值很大,飞船的移动速度还是很慢。要修复这个问题,可在全屏模式下运行游戏,我们稍后就将实现这种功能。

5. 限制飞船的活动范围

当前,如果玩家按住箭头键的时间足够长,飞船将飞到屏幕之外,消失得无影无踪。下面来修复这个问题,让飞船到达屏幕边缘后停止移动。为此,将修改Ship类的方法update()

上述代码在修改self.x的值之前检查飞船的位置。self.rect.right返回飞船外接矩形右边缘的x坐标。如果这个值小于self.screen_rect.right的值,就说明飞船未触及屏幕右边缘。左边缘的情况与此类似:如果rect左边缘的x坐标大于零,就说明飞船未触及屏幕左边缘。这确保仅当飞船在屏幕内时,才调整self.x的值。

如果此时运行alien_invasion.py,飞船将在触及屏幕左边缘或右边缘后停止移动。真是太神奇了!只在if语句中添加一个条件测试,就让飞船在到达屏幕左右边缘时向被墙挡住了一样。

6. 重构_check_events()

随着游戏的开发,方法_check_events()将越来越长。因此将其部分代码放在两个方法中,其中一个处理KEYDOWN事件,另一个处理KEYUP事件:

我们创建了两个新的辅助方法:_check_keydown_events()_check_keyup_events()。我们都包含形参selfevent。这两个方法的代码是从_check_events()中复制而来的,因此将方法_check_events()中相应的代码替换成了对这两个新方法的调用。现在,方法_check_events()更简单,代码结构也更清晰,在其中响应玩家输入时将更容易。

7. 按Q键退出

能够高效地响应按键后,我们来添加另一种退出游戏的方式。当前,每次测试新功能时,都需要单击游戏窗口顶部的“×”按钮来结束游戏,实在是太麻烦了。因此,我们来添加一个结束游戏的键盘快捷键——Q键:

_check_key_down_events()中,添加一个代码块,用于在玩家按Q键时结束游戏。现在测试该游戏时,你可按Q键来结束游戏,而无须使用鼠标将窗口关闭。

8. 在全屏模式下运行游戏

Pygame支持全屏模式,你可能会更喜欢在这种模式下而非常规窗口中运行游戏。有些游戏在全屏模式下看起来更舒服,而在macOS系统中用全屏模式运行会提升性能。

要在全屏模式下运行该游戏,可在__init__()中做如下修改:

创建屏幕时,传入了尺寸(0, 0)以及参数pygame.FULLSCREEN。这让Pygame生成一个覆盖整个显示器的屏幕。由于无法预先知道屏幕的宽度和高度,要在创建屏幕后更新这些设置:使用屏幕的rect的属性widthheight来更新对象settings

如果你喜欢这款游戏在全屏模式下的外观和行为,请保留这些设置。如果你更喜欢这款游戏在独立窗口中运行,可恢复到原来采用的方法——将屏幕尺寸设置为特定的值。

注意: 在全屏模式下运行这款游戏之前,请确认能够按Q键退出,因为Pygame默认不提供在全屏模式下退出游戏的方式。

七、结构梳理

下面将添加射击功能,为此需要新增一个名为bullet.py的文件,并修改一些既有文件。当前有三个文件,其中包含很多类和方法。添加其他功能之前,先来回顾一下这些文件,让你清楚这个项目的组织结构。

1. alien_invasion.py

主文件alien_invasion.py包含AlienInvasion类。这个类创建一些列贯穿整个游戏都要用到的属性:赋给self.settings的设置,赋给screen中的主显示surface,以及一个飞船实例。这个模块还包含游戏的主循环,即一个调用_check_events()ship.update()_update_screen()的while循环。

方法_check_events()检测相关的事件(如按下和松开键盘),并通过调用方法_check_keydown_events()_check_keyup_events()处理这些事件。当前,这些方法负责管理飞船的移动。AlienInvasion类还包含方法_update_screen(),该方法负责管理飞船的移动。AlienInvasion类还包含方法_update_screen(),该方法在每次主循环中重绘屏幕。

要玩游戏《外星人入侵》,只需运行文件alien_invasion.py,其他文件(settings.pyship.py)包含的代码会被导入这个文件中。

2. settings.py

文件settings.py包含Settings类,这个类只包含方法__init__(),用于初始化控制游戏外观和飞船速度的属性。

3. ship.py

文件ship.py包含Ship类,这个类包含方法__init__()、管理飞船位置的方法update()和在屏幕上绘制飞船的方法blitme()。表示飞船的图像存储在文件夹images下的文件ship.bmp中。

八、射击

下面来添加射击功能。我们将编写在玩家按空格键时发射子弹(用小矩形表示)的代码。子弹将在屏幕中向上飞行,抵达屏幕上边缘后消失。

1. 添加子弹设置

首先,更新setting.py,在方法__init__()末尾存储新类Bullet所需的值:

这些设置创建宽3像素、高15像素的深灰色子弹。子弹的速度比飞船稍低。

2. 创建Bullet类

下面来创建存储Bullet类的文件bullet.py,其前半部分如下:

Bullet类集成了从模块pygame.sprite导入的Sprite类。通过使用精灵(sprite),可将游戏中相关的元素编组,进而同时操作编组中的所有元素。为创建子弹实例,__init__()需要当前的AlienInvasion实例,我们还调用了super()来继承Sprite。另外,我们还定义了用于存储屏幕以及设置对象和子弹颜色的属性。

首先,创建子弹的属性rect。子弹并非基于图像,因此必须使用pygame.Rect()类从头开始创建一个矩形。创建这个类的实例时,必须提供矩形左上角的x坐标和y坐标,以及矩形的宽度和高度。我们在(0, 0)处创建这个矩形,但下一行代码将其移到了正确的位置,因为子弹的初始位置取决于飞船当前的位置。子弹的宽度和高度是从self.settings中获取的。

然后,将子弹的rect.midtop设置为飞船的rect.midtop。这样子弹将从飞船顶部出发,看起来像是从飞船中射出的。我们将子弹的y坐标存储为小数值,以便能够微调子弹的速度。

下面是bullet.py的第二部分,包括方法update()draw_bullet()

方法update()管理子弹的位置。发射出去后,子弹向上移动,意味着其y坐标将不断减小。为更新子弹的位置,从self.y中减去settings.bullet_speed的值。接下来,将self.rect.y设置为self.y的值。

属性bullet.speed让我们能够随着游戏的进行或根据需要提高子弹的速度,以调整游戏的行为。子弹发射后,其x坐标始终不变,因此子弹将沿直线垂直向上飞行。

需要绘制子弹时,我们调用draw_bullet()draw.rect()函数使用存储在self.color中的颜色填充表示子弹的rect占据的屏幕部分。

3. 将子弹存储到编组中

定义Bullet类和必要的设置后,便可编写代码在玩家每次按空格键时都射出一发子弹了。我们将在AlienInvasion中创建一个编组(group),用于存储所有有效的子弹,以便管理发射出去的所有子弹。这个编组是pygame.sprite.Group类的一个实例。pygame.sprite.Group类似于列表,但提供了有助于开发游戏的额外功能。在主循环中,将使用这个编组在屏幕上绘制子弹以及更新每颗子弹的位置。

首先,在__init__()中创建用于存储子弹的编组:

然后在while循环中更新子弹的位置:

对编组调用update()时,编组自动对其中的每个精灵调用update()。因此代码行bullets.update()将为编组bullets中的每颗子弹调用bullet.update()

4. 开火

AlienInvasion中,需要修改_check_keydown_events(),以便在玩家按空格键时发射一颗子弹。无需修改_check_keyup_events(),因为玩家松开空格键时什么都不会发生。还需要修改_update_screen(),确保在调用flip()前在屏幕上重绘每颗子弹。

为发射子弹,需要做的工作不少,因此编写一个新方法_fire_bullet()来完成这项任务:

首先导入Bullet类,再在玩家按空格键时调用_fire_bullet()。在_fire_bullet()中,创建一个Bullet实例并将其赋给new_bullet,再使用方法add()将其加入编组bullets中。方法add()类似于append(),不过是专门为Pygame编组编写的。

方法bullets.sprites()返回一个列表,其中包含编组bullets中的所有精灵。为在屏幕上绘制发射的所有子弹,遍历编组bullets中的精灵,并对每个精灵调用drwa_bullet()

如果此时运行alien_invasion.py,将能够左右移动飞船,并发射任意数量的子弹。子弹在屏幕上向上飞行,抵达屏幕顶部后消失得无影无踪,如下图所示。你在可settings.py中修改子弹的尺寸、颜色和速度。

5. 删除消失的子弹

当前,子弹在抵达屏幕顶端后消失,但这仅仅是因为Pygame无法在屏幕外面绘制它们。这些子弹实际上依然存在,其y坐标为负数且越来越小。这是个问题,因为它们将继续消耗内存和处理能力。

需要将这些消失的子弹删除,否则游戏所做的无谓工作将越来越多,进而变得越来越慢。为此,需要检测表示子弹的rectbottom属性是否为零。如果是,则表明子弹已飞过屏幕顶端:

使用for循环遍历列表(或Pygame编组)时,Python要求该列表的长度在整个循环中保持不变。因为不能从for循环遍历的列表或编组中删除元素,所以必须遍历编组的副本。我们使用方法copy()来设置for循环,从而能够在循环种修改bullets。我们检查每颗子弹,看看它是否从屏幕顶端消失。如果是,就将其从bullets中删除。最后,使用函数调用print()显示当前还有多少颗子弹,以核实确实删除了消失的子弹。

如果这些代码没有问题,我们发射子弹后查看终端窗口时,将发现随着子弹一颗颗地在屏幕顶端消失,子弹数将逐渐降为零。运行该游戏并确认子弹被正确删除后,请将这个函数调用print()删除。如果不删除,游戏的速度将大大降低,因为将输出写入终端花费的时间比将图形绘制到游戏窗口花费的时间还要多。

6. 限制子弹数量

很多射击游戏对可同时出现在屏幕上的子弹数量进行了限制,以鼓励玩家有目标地射击。下面在游戏《外星人入侵》中做这样的限制。

首先,在settings.py中存储最大子弹数:

这将未消失的子弹数限制为三颗。在AlienInvasion_fire_bullet()中,在创建新子弹前检查未消失的子弹数是否小于该设置:

玩家按空格键时,我们检查bullets的长度。如果len(bullets)小于3,就创建一颗新子弹;但如果有三颗未消失的子弹,则玩家按空格键时什么都不会发生。如果现在运行这个游戏,屏幕上最多只能有三颗子弹。

7. 创建方法_update_bullets()

编写并检查子弹管理代码后,可将其移到一个独立的方法中,确保AlienInvasion类租住有序。为此,创建一个名为_update_bullets()的新方法,并将其放在_update_screen()前面:

_update_bullets()的代码是从run_game()剪切并粘贴而来的,这里只是让注释更清晰了。

run_game()中的while循环又变得简单了:

我们让主循环包含尽可能少的代码,这样只要看方法名就能迅速知道游戏中发生的情况。主循环检查玩家的输入,并更新飞船的位置和所有未消失子弹的位置。然后,使用更新后的位置来绘制新屏幕。

请再次运行alien_invasion.py,确认发射子弹时没有错误。

九、小结

在本篇学习了:游戏开发计划的指定,以及使用Pygame编写的游戏的基本结构;如何设置背景色,以及如何将设置存储在独立的类中,以便轻松调整;如何在屏幕上绘制图像,以及如何让玩家控制游戏元素和移动;创建自动移动的元素,如在屏幕中向上飞行的子弹,以及删除不再需要的对象;如何定期重构项目的代码,为后续开发提供便利。

在下一篇中,我们将在游戏《外星人入侵》中添加外星人,并将能够击落外星人。

posted @ 2022-08-22 16:40  丨君丶陌  阅读(1841)  评论(0编辑  收藏  举报