Python项目开发——外星人入侵(一)
1、项目介绍
在本章中,将安装Pygame,然后创建一艘能够根据用户输入左右移动和射击的飞船。
1.1 项目设计
在该游戏中,玩家控制着一艘最初出现在屏幕底部中间的飞船。玩家可以使用方向键左右移动飞船,使用空格键进行射击。当游戏开始时,一个外星舰队出现在天空中,并向屏幕下方移动。玩家的任务就是消灭这些外星人。玩家将外星人消灭干净后,将出现一个新的飞船,移动速度更快。只要有外星人撞到玩家的飞船或到达屏幕下边缘,玩家就损失一艘飞船。玩家损失三艘飞船后,游戏结束。
在开发的第一个阶段,将创建一艘飞创,这艘飞船在用户按方向键时能够左右移动,并在用户按空格键是射击。设置这种行为后,就可以创建外星人以提高游戏的可玩性了。
1.2 开发游戏项目
在开发前,需要安装Pygame。安装命令自行搜索。首先创建一个空的Pygame窗口,稍后将在其中绘制游戏元素,如飞船和外星人。之后,还将让这个游戏影响用户输入、设置背景色、以及加载飞船图像。
2 开始游戏项目
2.1 创建Pygame窗口以及相应用户输入
下面创建一个表示游戏的类,以创建空的Pygame窗口。在文本编译器中新建一个文件,命名为alien_invasion.py。
import sys import pygame class AlienInvasion: #管理游戏资源和行为的类 def __init__(self): #初始化游戏并创建游戏资源 pygame.init() self.screen = pygame.display.set_mode((1200,800)) pygame.display.set_caption('Alien Invasion') def run_game(self): #开始游戏的主循环 while True: # 监听键盘和鼠标事件 for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() # 让最近绘制的屏幕可见 pygame.display.flip() if __name__ == '__main__': #创建游戏实例并运行游戏 ai = AlienInvasion() ai.run_game()代码解析:
一、导入模块sys和pygame。pygame模块包含开发游戏所需的功能。当玩家退出时,将使用sys模块中的工具来退出游戏。
二、创建一个名为 AlienInvasion的类。在这个类的__init__()方法中,调用pygame.init()函数来初始化背景,让Pygame能够正确的工作。
三、调用pygame.display.set_mode()创建一个显示窗口,这个游戏的所有图形元素都将在其中绘制。实参(1200,800)是一个元组,指定了游戏窗口的尺寸——宽1200像素、高800像素(可根据自己的显示器动态调整)。在将这个显示窗口赋值给self.screen,让这个类的所有都能使用它。赋给属性self.screen的对象是一个surface。在Pygame中,surface是屏幕的一部分,用于显示游戏元素。在这个游戏中,每个元素(如外星人或飞船)都是一个surface。display.set_mode()返回的surface表示整个游戏窗口,激活游戏的动画循环后,每经过一次循环都将自动重绘这个surface,将用户输入触发的所有变化都反映出来。
四、这个游戏由run_game方法控制。该方法包含一个不断运行的while循环,而这个循环包含一个事件循环以及管理屏幕更新的代码。事件是用户玩游戏时执行的操作,如按键或移动鼠标。为了让程序能够响应事件,可编写一个事件循环,以监听事件并根据放生的时事件类型执行适当的操作。嵌套在while循环中的for循环就是一个事件循环。
五、使用pyagme.event.get()函数来访问Pygame检测到的事件。这个函数返回一个列表,其中包含它在上一次调用后发生的所有事件。所有键盘和鼠标事件都将导致这个for循环运行。在这个循环中,将编写一系列if语句来检测并相应特定的事件。例如,当玩家单击游戏窗口的关闭按钮时,将检测到pygame.QUIT事件,进而调用sys.exit()来退出。
六、pygame.display.flip(),命令pygame让最近绘制的屏幕可见。这里,它在每次执行while循环时都会绘制一个空屏幕,并擦去旧屏幕,使得只有新的空屏幕可见。在移动游戏元素时,pygame.display.flip()将不断更新屏幕,以显示新位置上的元素并隐藏原来位置上的屏幕,从而营造平滑移动的效果。
七、在这个文件末尾,创建一个游戏实例并调用run_game(),这些代码被放在一个if代码块中,仅当直接运行该文件时,才会执行。如果此时运行这个文件,将会看到一个空的Pygame窗口。效果如下:
2.2 控制帧率
理想情况下,游戏在所有的系统中都应该以相同的帧率运行。对于可在多种系统中运行的游戏,控制帧率是个问题,Pygame提供了一种相对简单的方式来实现。创建一个时钟(clock),并确保它在主循环每次通过后都进行计时。当这个循环的通过速度超过定义的帧率时,Pygame会计算需要暂停多长时间,以便游戏的运行速度保持一致。下面在__init__()方法中定义这个时钟,然后在run_game()的while循环末尾让这个时钟进行计时。完整代码如下:
import sys import pygame class AlienInvasion: #管理游戏资源和行为的类 def __init__(self): #初始化游戏并创建游戏资源 pygame.init() #定义时钟 self.clock = pygame.time.Clock() self.screen = pygame.display.set_mode((1200,800)) pygame.display.set_caption('Alien Invasion') def run_game(self): #开始游戏的主循环 while True: # 监听键盘和鼠标事件 for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() # 让最近绘制的屏幕可见 pygame.display.flip() #时钟开始计时 self.clock.tick(60) if __name__ == '__main__': #创建游戏实例并运行游戏 ai = AlienInvasion() ai.run_game()tick()方法接受一个参数:游戏的帧率。这里使用的值为60,Pygame将尽可能确保这个循环每秒恰好运行60次。
2.3 设置背景色
Pygame默认创建一个黑色屏幕,可以修改设置。在__init__方法中设置颜色,在run_game方法绘制屏幕时添加颜色。新增代码如下:
#设置背景颜色 self.bg_color = (230,230,230)
#每次循环时都重新绘制 self.screen.fill(self.bg_color)
此时效果如下:
在Pygame中,颜色是以RGB值指定的,这里不做细致介绍,可自行了解。
2.4 创建settings类
每次给游戏添加新功能时,通常会引入一些新设置。下面将编写一个名为settings的模块,其中包含一个名为Settings的类,用于将所有设置都存储在一个位置,避免在代码中到处添加设置。这样,每当需要访问设置时,只需要使用一个settings对象。此外,在项目规模增大时,还可以让游戏的外观和行为修改起来更加容易,下面创建一个settings.py,并在此文件修改一些相关的值即可,无须查找分布在项目中的各种设置。
class Settings: def __init__(self): #初始化游戏设置 #屏幕大小设置 self.screen_width = 1200 self.screen_height = 800 #背景色设置 self.bg_color = (230,230,230)修改alien_invasion.py,导入Settings类
import sys import pygame from settings import Settings class AlienInvasion: #管理游戏资源和行为的类 def __init__(self): #初始化游戏并创建游戏资源 pygame.init() #定义时钟 self.clock = pygame.time.Clock() self.settings = Settings() self.screen = pygame.display.set_mode((self.settings.screen_width,self.settings.screen_height)) pygame.display.set_caption('Alien Invasion') def run_game(self): #开始游戏的主循环 while True: # 监听键盘和鼠标事件 for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() #每次循环时都重新绘制 self.screen.fill(self.settings.bg_color) # 让最近绘制的屏幕可见 pygame.display.flip() #时钟开始计时 self.clock.tick(60) if __name__ == '__main__': #创建游戏实例并运行游戏 ai = AlienInvasion() ai.run_game()3 添加飞船
在游戏中,可以使用几乎任意类型的图像文件,但使用位图(.bmp)文件最为简单,因为Pygame默认加载位图。在项目路径下创建images路径,存在相关图片。
3.1 创建Ship类
选择好飞船的图像后,需要将其显示到屏幕上。创建一个ship的模块,包含Ship的类,负责管理飞船的大部分行为:
import pygame class Ship: def __init__(self, ai): #初始化飞船并设置初始位置 self.screen = ai.screen self.screen_rect = ai.screen.get_rect() #加载飞船图像并获取其外接矩形 self.image = pygame.image.load('images/ship.bmp') self.rect = self.image.get_rect() #每艘新飞船都放在屏幕底部的中间位置 self.rect.midbottom = self.screen_rect.midbottom def blitme(self): #在指定位置绘制飞船 self.screen.blit(self.image, self.rect)代码说明:
一、Pygame之所以高效,是因为它把所有的游戏元素当做矩形(rect对象)来处理,即便它们的形状并非矩形也一样。而把游戏元素当作矩形来处理之所以高效,是因为矩形是最简单的几何图形。例如,通过将游戏元素视为矩形,Pygame能够更快地判断它们是否发生了碰撞。这种做法的效果通常很好。在这个类中,把飞船和屏幕作为矩形进行处理。
二、定义这个类之前,导入模块pygame。Ship的__init__方法接收两个参数:除了self,还有一个指向当前 AlienInvasion 实例的引用。这让Ship可以访问AlienInvasion中定义的所有游戏资源。
self.screen = ai.screen:表示将屏幕赋给ship的一个属性,这样可以在这个类中的所有方法都能访问它。
self.screen_rect = ai.screen.get_rect():使用get_rect()方法访问屏幕的rect属性,并将其赋给self.screen_rect,可以将飞船放到屏幕的正确位置上。
self.image = pygame.image.load('images/ship.bmp'):调用pygame.image.load()加载图像,这个函数返回一个表示飞船的surface,再把这个surface赋给self.image。self.rect = self.image.get_rect():调用get_rect()获取相应surface的属性rect,以便将来使用它来指定飞船的位置。
在处理rect对象时,可以使用矩形的四个角及中心的x、y坐标,通过设置这些值来指定矩形的位置。如果要将游戏元素居中,可以设置相应rect对象的属性center、centerx、centery;如果让游戏元素与屏幕边缘对其,可设置属性top、bottom、left、right。除此之外,还有一些组合属性,如midbottom、midtop、midleft、midright。要调整游戏元素的水平或垂直位置,可使用x、y属性,她们分别是矩形左上角x和y坐标。
self.rect.midbottom = self.screen_rect.midbottom:表示飞船位于屏幕底部的中间位置。
最后定义blitme()方法,将图像绘制到self.rect指定的位置。
3.2 在屏幕上绘制飞船
修改alien_invasion.py,创建一艘飞船并调用blitme()方法
def __init__(self) .... #创建飞船示例 self.ship = Ship(self)
def run_game(self) ... #把飞船绘制到屏幕上 self.ship.blitme()
运行后效果
4 重构_check_event()方法和_update_screen()方法
在大型项目中,经常需要在添加新代码前重构既有代码。重构旨在简化既有代码的结构,使其更容易扩展。本节把原来的run_game()方法拆分两个辅助方法。辅助方法一般只在类中调用,不会在类外调用。在Python中,辅助方法的名称以_开头。
4.1 _check_events()方法
下面把管理事件的代码移到一个名为_check_events方法中,简化run_game并隔离事件循环。通过隔离事件循环,可将事件管理与游戏的其他方法分离。下面是新增_check_events方法后的
AlienInvasion的类。
import sys import pygame from settings import Settings from ship import Ship class AlienInvasion: #管理游戏资源和行为的类 def __init__(self): #初始化游戏并创建游戏资源 pygame.init() #定义时钟 self.clock = pygame.time.Clock() self.settings = Settings() self.screen = pygame.display.set_mode((self.settings.screen_width,self.settings.screen_height)) pygame.display.set_caption('Alien Invasion') #创建飞船示例 self.ship = Ship(self) def run_game(self): #开始游戏的主循环 while True: #调用_check_events方法 self._check_events() #每次循环时都重新绘制 self.screen.fill(self.settings.bg_color) #把飞船绘制到屏幕上 self.ship.blitme() # 让最近绘制的屏幕可见 pygame.display.flip() #时钟开始计时 self.clock.tick(60) # 监听键盘和鼠标事件 def _check_events(self): for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() if __name__ == '__main__': #创建游戏实例并运行游戏 ai = AlienInvasion() ai.run_game()4.2 _update_screen方法
进一步简化run_game方法,把更新屏幕的代码移到_update_screen方法中.
#更新屏幕方法 def _update_screen(self): # 每次循环时都重新绘制 self.screen.fill(self.settings.bg_color) # 把飞船绘制到屏幕上 self.ship.blitme() # 让最近绘制的屏幕可见 pygame.display.flip()5 驾驶飞船
下面控制飞船左右移动。在用户按左右方向键时做出响应。
5.1 响应按键
在Pygame中,事件都是通过pygame.event.get()方法获取的,因此需要在_check_events方法中指定要检查的事件类型,每当用户按键时,都将在Pygame中产生一个KEYDOWN事件。在检测KEYDOWN事件时,需要检查按下键是否触发飞船的移动。如果用户按右方向键,就增大飞船的rect.x值,使飞船向右移动。同理,向左移动则减小。
def _check_events(self): for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() #监控用户按键 elif event.type == pygame.KEYDOWN: #如果是右方向键 if event.key ==pygame.K_RIGHT: self.ship.rect.x += 1 #如果是左方向键 elif event.key == pygame.K_LEFT: self.ship.rect.x -= 1效果如下:
每按一次方向键,飞船都会向按键的方向移动1像素,然而要飞船持续移动则需要用户不停的按键。下面来改进控制,允许飞船持续移动。
5.2 持续移动
让游戏检测pygame.KEYUP事件,以便知道用户何时释放方向键。然后,在结合使用KEYDOWN和KEYUP事件以及一个名为moving_right和moving_left的标志来实现持续移动。
当移动标志为False时,飞船不会移动。当用户按下方向键后,该标志改为True;当用户释放按键时,再改为False。
飞船的属性是Ship类控制,因此要给这个类添加一个名为moving_right和moving_left的标志和一个名为update()方法。该方法检查移动标志的状态。如果这个标志为True,就调整飞船的位置。每次通过while循环时调用一次这个方法,以更新飞船的位置。
import pygame class Ship: def __init__(self, ai): #初始化飞船并设置初始位置 self.screen = ai.screen self.screen_rect = ai.screen.get_rect() #加载飞船图像并获取其外接矩形 self.image = pygame.image.load('images/ship.bmp') self.rect = self.image.get_rect() #每艘新飞船都放在屏幕底部的中间位置 self.rect.midbottom = self.screen_rect.midbottom #向右移动标志 self.moving_right = False #向左移动标志 self.moving_left = False def update(self): if self.moving_left: self.rect.x -= 1 elif self.moving_right: self.rect.x += 1 def blitme(self): #在指定位置绘制飞船 self.screen.blit(self.image, self.rect)接下来修改_check_events方法
def _check_events(self): for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() #监控用户按键 elif event.type == pygame.KEYDOWN: #如果是右方向键 if event.key ==pygame.K_RIGHT: self.ship.moving_right = True #如果是左方向键 elif event.key == pygame.K_LEFT: self.ship.moving_left = True #监控用户释放按键 elif event.type == pygame.KEYUP: if event.key == pygame.K_RIGHT: self.ship.moving_right = False elif event.key == pygame.K_LEFT: self.ship.moving_left = False最后,修改run_game方法中的while循环,以便在每次执行循环时都调用飞船的update方法:
def run_game(self): #开始游戏的主循环 while True: #调用_check_events方法 self._check_events() #调用飞船的update方法 self.ship.update() #调用_update_screen方法 self._update_screen() #时钟开始计时 self.clock.tick(60)效果如下:
5.3 调整飞船的速度
当前,每次执行while循环时,飞船都移动1像素。可以在settings中添加属性用来控制飞船的速度。将根据这个属性决定飞船在每次循环时做多移动的距离。
#飞船默认速度 self.ship_speed_factor = 1.5
通过将速度设置为浮点数,可加快移动速度。但是,rect的x等属性只能存储整数,因此提前进行转换。
下面对Ship类进行修改,使用上面的属性控制飞船的速度。
import pygame class Ship: def __init__(self, ai): #初始化飞船并设置初始位置 self.screen = ai.screen #通过ai获取settings对象 self.settings = ai.settings self.screen_rect = ai.screen.get_rect() #加载飞船图像并获取其外接矩形 self.image = pygame.image.load('images/ship.bmp') self.rect = self.image.get_rect() #每艘新飞船都放在屏幕底部的中间位置 self.rect.midbottom = self.screen_rect.midbottom #在飞船的属性x中存储一个浮点数 self.x = float(self.rect.x) #向右移动标志 self.moving_right = False #向左移动标志 self.moving_left = False def update(self): if self.moving_left: self.x -= self.settings.ship_speed_factor elif self.moving_right: self.x += self.settings.ship_speed_factor #根据self.x更新rect对象 self.rect.x += self.x def blitme(self): #在指定位置绘制飞船 self.screen.blit(self.image, self.rect)5.4 重构_check_events方法
再次重构该方法,将其部分代码拆分为两个方法,一个是处理KEYDOWN事件,一个处理KEYUP事件。
# 监听键盘和鼠标事件 def _check_events(self): for event in pygame.event.get(): if event.type == pygame.QUIT: sys.exit() #监控用户按键 elif event.type == pygame.KEYDOWN: self._check_keydown_events(event) #监控用户释放按键 elif event.type == pygame.KEYUP: self._check_keyup_events(event) #监听keydown事件 def _check_keydown_events(self,event): # 如果是右方向键 if event.key == pygame.K_RIGHT: self.ship.moving_right = True # 如果是左方向键 elif event.key == pygame.K_LEFT: self.ship.moving_left = True #监听keyup事件 def _check_keyup_events(self,event): if event.key == pygame.K_RIGHT: self.ship.moving_right = False elif event.key == pygame.K_LEFT: self.ship.moving_left = False5.5 限制飞船活动范围
当前,如果用户按住方向键的时间足够长,飞船将移动屏幕之外。下面来修复这个问题,让飞船到底屏幕边缘后停止移动。
def update(self): #更新飞船而不是rect对象的x值 if self.moving_left and self.rect.left > 0: self.x -= self.settings.ship_speed_factor elif self.moving_right and self.rect.right < self.screen_rect.right: self.x += self.settings.ship_speed_factor #根据self.x更新rect对象 self.rect.x = self.x5.6 按Q键退出
在_check_keydown_events方法新增elif分支,判断。
elif event.key == pygame.K_q: sys.exit()
6 射击
下面添加射击功能,在用户按空间建时发射子弹(用小矩形)的代码,子弹将在屏幕中直线上升,并在抵达屏幕上边缘后消失。
6.1 创建Bullet类
创建Bullet(子弹)类。
import pygame from pygame.sprite import Sprite class Bullet(Sprite): def __init__(self,ai): #在飞船的当前位置创建一个子弹对象 super().__init__() self.screen = ai.screen self.settings = ai.settings #需要在settings中设置子弹颜色 self.color = self.settings.bullet_color #在(0,0)位置创建一个表示子弹的矩形,再设置正确的位置 self.rect = pygame.Rect(0,0,self.settings.bullet_width,self.settings.bullet_height) self.rect.midtop = ai.ship.rect.midtop #存储用浮点数表示子弹位置 self.y = float(self.rect.y) # 向上移动子弹 def update(self): #向上移动子弹,更新子弹的准确位置 self.y -= self.settings.bullet_speed #更新表示子弹的rect位置 self.rect.y = self.y #在屏幕上绘制子弹 def draw_bullet(self): pygame.draw.rect(self.screen, self.color, self.rect)代码说明:
一、Bullet类继承了从模块pygame.sprite导入的Sprite类,通过使用sprite,可将游戏中相关的元素编组,进而同时操作编组中的所有元素。为了创建子弹实例,__init__方法需要当前的AlienInvasion实例,因此调用super()来继承Sprite。另外,还定义了用于存储屏幕和设置对象以及表示子弹颜色属性。
二、
self.rect = pygame.Rect(0,0,self.settings.bullet_width,self.settings.bullet_height)
创建子弹的rect属性。子弹并非基于图像文件的,因此必须使用pygame.Rect()类从头开始创建一个矩形。在创建这个类的实例时,必须提供矩形左上角的x、y坐标,还有矩形的宽度和高度。在(0,0)处创建这个矩形,而下一行代码将其移动到正确的位置。因为子弹的初始位置取决于飞船当前的位置。
三、
self.rect.midtop = ai.ship.rect.midtop:讲子弹的rect.midtop设置为飞创的rect.midtop,这样子弹将出现在飞船顶部,看起来像飞船发射的。将子弹的y坐标存储为浮点数,方便微调子弹的速度。
6.2 添加子弹设置
更新settings.py,在__init__方法末尾存储新的类Bullet所需的值。
#子弹相关设置 self.bullet_speed = 2.0 self.bullet_width = 3 self.bullet_height = 15 self.bullet_color = (60,60,60)6.3 讲子弹存储到编组中
在定义Bullet类和必要设置后,便可编写代码在玩家每次按空格键时都发射一颗子弹。在
AlienInvasion中创建一个编组(group),用于存储所有有效的子弹,以便管理发射出去的所有子弹。这个编组是Group类,来自pygame.sprite模块的一个实例。Group类类似于列表,当提供了有助于开发游戏的额外功能。在主循环中,将使用这个编组在屏幕上绘制子弹,以及更新每颗子弹的位置。
__init__(self) self.bullets = pygame.sprite.Group()run_game(self) self.bullets.update()
6.4 发射子弹
在AlienInvasion中,修改_check_keydown_events方法,以便用户在按空格时发射一颗子弹。还需修改_update_screen,确保在调用flip前在屏幕上重绘每颗子弹。新增一个方法_fire_bullet来完成这项任务。
def _check_keydown_events(self,event): # 如果是右方向键 if event.key == pygame.K_RIGHT: self.ship.moving_right = True # 如果是左方向键 elif event.key == pygame.K_LEFT: self.ship.moving_left = True elif event.key == pygame.K_q: sys.exit() elif event.key ==pygame.K_SPACE: self._fire_bullet() def _fire_bullet(self): #创建一颗子弹,并将其加入编组bullets new_bullet = Bullet(self) self.bullets.add(new_bullet) #更新屏幕方法 def _update_screen(self): # 每次循环时都重新绘制 self.screen.fill(self.settings.bg_color) for bullet in self.bullets.sprites(): bullet.draw_bullet() # 把飞船绘制到屏幕上 self.ship.blitme() # 让最近绘制的屏幕可见 pygame.display.flip()在_fire_bullet方法中,创建一个Bullet实例并将其赋给new_bullet,在使用add()方法将其加入编组bullets。add()方法类似列表的append方法,不过是专门为pygame编组编写的。
6.5 删除以消失的子弹
当前,虽然子弹会在抵达屏幕上边缘后消失,但这仅仅是因为Pygame无法再屏幕外绘制它们。这些子弹实际依然存在,它们的y坐标作为负数越来越小。这个是问题,会消耗系统的内存和处理能力。需要经这些已经消失的子弹删除、为此,需要检测表示子弹的rect的bottom属性是否为0.如果是就表明子弹已飞过屏幕上边缘。在主循环中新增以下代码:
#判断子弹是否到达屏幕上边缘,如果是,则从sprite移除 for bullet in self.bullets.copy(): if bullet.rect.bottom <= 0: self.bullets.remove(bullet)
使用for循环遍历时,Python要求改列表的长度在整个循环中保持不变。这意味着不能从for循环遍历的列表或编组中删除元素,因此必须遍历编组的副本。使用copy()方法作为for循环的遍历对象,能否在for循环中修改原始编组bullets。
6.6 限制子弹数量
将允许同时出现的子弹数量存储到settings中,默认是3。
self.bullet_nums = 3
在AlienInvasion的_fire_bullet中,会在创建新子弹前检查未消失的子弹数量是否小于3.
def _fire_bullet(self): #创建一颗子弹,并将其加入编组bullets if len(self.bullets) < self.settings.bullet_nums: new_bullet = Bullet(self) self.bullets.add(new_bullet)
6.7 创建_update_bullets方法
编写并检查子弹管理的代码可放到_update_bullets中,以确保AlienInvasion的整洁。
def _update_bullets(self): self.bullets.update() # 判断子弹是否到达屏幕上边缘,如果是,则从sprite移除 for bullet in self.bullets.copy(): if bullet.rect.bottom <= 0: self.bullets.remove(bullet)