Python项目开发——外星人入侵(一)

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()方法接受一个参数游戏的帧率。这里使用的值为60Pygame将尽可能确保这个循环每秒恰好运行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)