第一个实践的小项目《外星人入侵》

外星人入侵(一)

前面安装好了需要的Pygame,并且也大概了解了Python的一些基础知识,接下来我们就一起,尝试着编写一个简单有趣的小游戏吧。
我们平时玩游戏都知道,打开游戏会弹出一个界面,就是一个新的窗口,那好我们就先创建一个窗口,然后后面就在这个窗口里面来绘制我们的游戏内容和元素,比如:飞船和外星人;飞船还需要相应玩家的输入;也可以设置游戏的背景颜色等等…

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

首先,创建一个空的Pygame窗口。使用Pygame编写的游戏的基本结构如下:
alien_invasion.py

import sys #这里将导入sys模块
import pygame  #这里将导入pygame模块

def run_game():
	#初始化游戏并创建一个屏幕对象
	pygame.init()
	screen = pygame.display.set_mode((800,600))  #这个设置的是窗口的宽高尺寸
	pygame.display.set_caption("Alien Invasion")  #这里是窗口的Title
	
	#开始游戏的主循环
	while True:
		
		#监视键盘和鼠标事件
		for event in pygame.event.get():
			if event.type == pygame.QUIT:
				sys.exit(0)  #如果鼠标点击窗口右上角的关闭button,就会退出。
		#注意exit是有参数的,传递0,但是有些code里面没有参数,这点需要注意,后续会详细介绍

		#让最近绘制的屏幕可见
		pygame.display.flip()

run_game()

看看执行结果:
在这里插入图片描述
模块pygame包含开发游戏所需的功能。玩家退出时,需要使用sys模块的exit()方法来退出游戏。
上面代码段的开始是run_game()。代码行pygame_init()初始化背景设置,让Pygame能够正确的工作。pygame.display.set_mode()是设置显示窗口,传递的实参(800,600)是一个元组(元组大家还是否记得呢?不记得的同学请自觉复习哦!),设置了一个像素为800*600像素的游戏窗口。
接下来要注意哦,对象screen是一个
surface
。在Pygame中,surface是屏幕的一部分,用于显示游戏元素。在《外星人入侵》游戏中,每个元素(如外星人和飞船)都是一个surface,display.set_mode()返回的surface表示整个游戏窗口。我们激活游戏的动画循环后,每经过一次循环都将自动重绘这个surface
在while循环中,包含一个事件循环事件是用户玩游戏时执行的操作,如按键和移动鼠标。为了让程序响应事件,编写了一个事件循环,来侦听事件,就是for循环,这里是侦听退出,方法pygame.event.get()来检测事件的发生。
pygame.display.flip()让最近绘制的屏幕可见,在每次执行循环时,都绘制一个空屏幕,并擦去旧屏幕,只显示新屏幕,如果是在游戏中,屏幕不断的刷新就可以看到元素的位置的变化,隐藏原来位置的元素,在元素的新位置显示,从而就看到了平滑移动的效果。

2.设置背景色

我们看到上面创建的空窗口是黑色的,感觉看着不爽,那能不能更换成喜欢的背景色呢?当然可以:
alien_invasion.py

import sys #这里将导入sys模块
import pygame  #这里将导入pygame模块

def run_game():
	#初始化游戏并创建一个屏幕对象
	pygame.init()
	screen = pygame.display.set_mode((800,600))  #这个设置的是窗口的宽高尺寸
	pygame.display.set_caption("Alien Invasion")  #这里是窗口的Title

	#设置背景色   R   G   B
	bg_color = (230,230,230)	
	
	#开始游戏的主循环
	while True:
		
		#监视键盘和鼠标事件
		for event in pygame.event.get():
			if event.type == pygame.QUIT:
				sys.exit(0)  #如果鼠标点击窗口右上角的关闭button,就会退出。
		#注意exit是有参数的,传递0,但是有些code里面没有参数,这点需要注意,后续会详细介绍
		
		#每次循环时都重绘屏幕
		screen.fill(bg_color)
		
		#让最近绘制的屏幕可见
		pygame.display.flip()

run_game()

看看效果如何:
在这里插入图片描述
嗯,这背景色看着舒服些了。
Pygame中,颜色是以RGB三原色(红色,绿色和蓝色)值指定的,其中每个值的可能取值范围都是0~255.颜色值(255,0,0)表示红色,(0,255,0)表示绿色,(0,0,255)表示蓝色。通过RGB的不同组合,可创建1600万种颜色,在我们的code中,RGB值是相同的,所以设置的背景色为浅灰色。
方法screen.fill(),用背景色填充屏幕;方法只接受一个实参:一种颜色。

3.创建设置类

因为在一个软件或者游戏中,会有很多设置,随着功能需求的增加,设置也在增加和改变,为了方便我们更改和修改,添加新的设置,我们编写一个设置类:Settings类,用于存储设置。
settings.py

class Settings():
	"""存储《外星人入侵》的所有设置的类"""

	def __init__(self):
		"""初始化游戏的设置"""
		#屏幕设置
		self.screen_width = 800
		self.screen_height = 600
		self.bg_color = (230,230,230)

alien_invasion.py

import sys #这里将导入sys模块
import pygame  #这里将导入pygame模块
from settings import Settings  #导入Settings类
def run_game():
	#初始化pygame、设置和屏幕对象
	pygame.init()
	ai_settings = Settings()
	screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
	pygame.display.set_caption("Alien Invasion")  #这里是窗口的Title

	#设置背景色   R   G   B
	bg_color = (230,230,230)	
	
	#开始游戏的主循环
	while True:
		
		#监视键盘和鼠标事件
		for event in pygame.event.get():
			if event.type == pygame.QUIT:
				sys.exit(0)  #如果鼠标点击窗口右上角的关闭button,就会退出。
		#注意exit是有参数的,传递0,但是有些code里面没有参数,这点需要注意,后续会详细介绍
		
		#每次循环时都重绘屏幕
		screen.fill(ai_settings.bg_color)
		
		#让最近绘制的屏幕可见
		pygame.display.flip()

run_game()

看看效果,其实效果和上面是一样的:
在这里插入图片描述
我们从settings.py中导入了Settings类,然后创建了一个Settings实例,并将其存储在变量ai_settings中,然后在创建屏幕时,使用了ai_settings的属性screen_width和screen_height;然后使用ai_settings来填充颜色就可以了。

4.添加飞船图像

我们已经了解了基本的画面绘制,是不是很期待怎么把飞船给整到画面里面呢?哈哈~,不要着急,我们接下来,就在画面中添加一个飞船;绘制飞船需要加载一幅图像,然后使用pygame中的blit()方法绘制它。
在游戏中几乎可以使用任何类型的图像文件,但使用
位图(.bmp)文件最为简单,因为pygame
默认加载位图。想要加载其他的文件类型,需要配置配置Pygame让其可以加载其他文件类型,但有些文件可能需要我们安装相应的图像库。大多数图像都为.jpg.png.gif格式,我们可以使用工具转换一下嘛,比如使用Photoshop、GIMP、Paint等工具将其转换为位图。
我们接下来加载的如下的图片:
在这里插入图片描述
我们在主项目文件夹中新建一个文件夹,将其命名为images,然后将让面图片保存在文件夹中。
在这里插入图片描述

4.1创建Ship类

我们创建一个ship的模块,其中包含Ship类,负责管理飞船的大部分行为:
ship.py

import pygame  #导入模块Pygame

class Ship():
	
	def __init__(self,screen):
		"""初始化飞船并设置其初始位置"""
		self.screen = screen

		#加载飞船图像并获取其外接矩形
		self.image = pygame.image.load('images/ship') #加载图片
		self.rect = self.image.get_rect()
		self.screen_rect = screen.get_rect()

		#将每艘新飞船放在屏幕底部中央
		self.rect.centerx = self.screen_rect.centerx
		self.rect.bottom = self.screen_rect.bottom

	def blitme(self):
		"""在指定位置绘制飞船"""
		self.screen.blit(self.image,self.rect)

Ship的方法__init__()接受两个参数:引用self和screen,screen指定了要将飞船绘制到什么地方。pygame.image.load()函数加载图像,它返回一个表示飞船的surface,然后将surface存储到self.image中。
图像加载后,使用get_rect()获取相应surface属性rectPygame的效率之所以如此之高,一个原因是它让你能够处理矩形(rect对象)一样处理游戏元素,即便它们的形状并非矩形。那为什么处理矩形的效率就很高呢?是因为矩形是简单的几何形状。
要将游戏元素居中,可设置相应
rect
对象的属性center,centerx或centery。要让游戏元素与屏幕边缘对齐,可使用top、bottom、left和right;要调整游戏元素的水平或垂直位置,可使用x和y,分别对应矩形左上角的x和y坐标。

注意:在Pygame中,原点(0,0)位于屏幕左上角,向右下方移动时,坐标值将增大,在800600的屏幕上,原点位于左上角,而右下角的坐标为800600.就像在复印机在打印图像一样,原点也是图像居中在定影鼓的左上角,是先居中在定影鼓上面,然后才是左上角,然后按照行输出的模式,把图像排列出来。

方法blitme(),它根据self.rect指定的位置将图像绘制到屏幕上。

4.2在屏幕上绘制飞船

接下来我们就创建一个飞船。
alien_invasion.py

import sys #这里将导入sys模块
import pygame  #这里将导入pygame模块
from settings import Settings  #导入Settings类
from ship import Ship  #导入ship类
def run_game():
	#初始化pygame、设置和屏幕对象
	pygame.init()
	ai_settings = Settings()
	screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
	pygame.display.set_caption("Alien Invasion")  #这里是窗口的Title

	#设置背景色   R   G   B
	bg_color = (230,230,230)	
	
	#创建一艘飞船
	ship = Ship(screen)
	
	#开始游戏的主循环
	while True:
		
		#监视键盘和鼠标事件
		for event in pygame.event.get():
			if event.type == pygame.QUIT:
				sys.exit(0)  #如果鼠标点击窗口右上角的关闭button,就会退出。
		#注意exit是有参数的,传递0,但是有些code里面没有参数,这点需要注意,后续会详细介绍
		
		#每次循环时都重绘屏幕
		screen.fill(ai_settings.bg_color)
		ship.blitme()  #根据指定的位置将图像绘制到屏幕上。
		
		#让最近绘制的屏幕可见
		pygame.display.flip()

run_game()

导入Ship类后,创建一个名为Ship的实例。必须在主while循环前创建该实例,以免每次循环时都创建一艘飞船。填充背景后,调用ship.blitme()将飞船绘制到屏幕上。
在这里插入图片描述

5.重构:模块game_functions

在实际的大型项目中,有时需要在添加新code前重构既有代码。重构旨在简化既有代码的结构,使其更容易扩展。那接下来呢,我们就创建一个game_functions的新模块,该模块将存储大量让游戏《外星人入侵》运行的函数。通过创建该模块,可避免alien_invasion.py太长,并且逻辑也可以更简洁。

5.1函数check_events()

首先定义一个check_events()函数,见名知意,是管理事件的,用来简化run_game()并隔离事件管理循环。通过隔离事件循环,可将事件管理与游戏的其他方面(如更新屏幕)分离。
将check_events()放在一个名为game_functions的模块中:
game_functions.py

import sys
import pygame

def check_events():
	"""响应按键和鼠标事件"""
	for event in pygame.event.get():
		if event.type == pygame.QUIT:
			sys.exit(0)

模块中导入了事件检查循环要使用的sys和pygame。当前,函数check_event()不需要任何形参。下面再修改alien_invasion.py:

import pygame  #导入模块pygame
from settings import Settings  
from ship import Ship
import game_functions as gf #导入模块game_functions,并重命名为gf

def run_game():
    #初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")

    #设置背景色
    bg_color = (230, 230, 230)

    #创建一艘非飞船
    ship = Ship(screen)

    #开始游戏的主循环
    while True:

        #监视键盘和鼠标事件
        gf.check_events()
        ship.update()
        #每次循环时都重绘屏幕
        #让最近绘制的屏幕可见
        gf.update_screen(ai_settings,screen,ship)

run_game()

5.2函数update_screen()

为了进一步简化run_game(),我们下面将更新屏幕的代码移到一个名为**update_screen()**函数中,并将这个函数放在模块game_functions.py中:
game_functions.py

import sys
import pygame

def check_events():
	"""响应按键和鼠标事件"""
	for event in pygame.event.get():
		if event.type == pygame.QUIT:
			sys.exit(0)

def update_screen(ai_settings,screen,ship):
	"""更新屏幕上的图像,并切换到新屏幕"""
	#每次循环时都重绘屏幕
	screen.fill(ai_settings.bg_color)
	ship.blitme()

	#让最近绘制的屏幕可见
	pygame.display.flip()

新函数update_screen()包含三个形参:ai_settings、screen和ship。现在需要将alien_invasion.py的while循环中更新屏幕的代码替换为对函数update_screen()的调用:
alien_invasion.py

import pygame  #导入模块pygame
from settings import Settings  
from ship import Ship
import game_functions as gf #导入模块game_functions,并重命名为gf

def run_game():
    #初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")

    #设置背景色
    bg_color = (230, 230, 230)

    #创建一艘非飞船
    ship = Ship(screen)

    #开始游戏的主循环
    while True:

        #监视键盘和鼠标事件
        gf.check_events()
        #每次循环时都重绘屏幕
        #让最近绘制的屏幕可见
        gf.update_screen(ai_settings,screen,ship)

run_game()

刚开始我们把所有的代码逻辑都堆积在run_game()中,现在我们导入了模块game_functions把大部分工作可以移到该模块中完成,这样while循环就更简洁了。
在大型项目设计中,就需要这样对复杂的逻辑进行重构拆分。

6.驾驶飞船

我们接下来就绘制动态的画面,实现飞船的左右移动。

6.1响应按键

通过响应用户的按键来实现飞船的左右移动。每当用户按键时,都将在pygame中注册一个事件。事件都是通过方法pygame.event.get()获取的,因此在函数check_events()中,我们需要指定要检查那些类型的事件。每次按键都被注册为一个KEYDOWN事件事件驱动),检测到KEYDOWN事件时,我们需要检查按下的是否是特定的键。例如,如果按下的是右箭头键,就增大飞船的rect.centerx的值,飞船将向右移动;反之,按下左箭头,就减小rect.centerx的值,飞船将向左移动。
game_functions.py

import sys
import pygame

def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit(0)  #在这里的exit()中需要传递参数,他么的,竟然这么坑吗?
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                #向右移动飞船
                ship.rect.centerx += 3
            elif event.key == pygame.K_LEFT:
                #向左移动飞船
                ship.rect.centerx -= 3
                
def update_screen(ai_settings,screen,ship):
    """更新屏幕上的图像,并切换到新屏幕"""

    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()

    #让最近绘制的屏幕可见
    pygame.display.flip()

在函数check_events()中包含了形参ship,因为玩家按右箭头键时,需要将飞船向右移动;反之,向左移动;在函数check_events()内部,在事件循环中添加了一个elif代码块,以便在Pygame检测到KEYDOWN事件时做出响应,通过读取属性event.key,来检查按下的是否是左右箭头。
在alien_invasion.py中,需要更新调用check_events()代码,将ship作为实参传递进去:

import pygame
from settings import Settings
from ship import Ship
import game_functions as gf

def run_game():
    #初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")


    #设置背景色
    bg_color = (230, 230, 230)

    #创建一艘非飞船
    ship = Ship(screen)
    #开始游戏的主循环
    while True:

        #监视键盘和鼠标事件
        gf.check_events(ship)
        #每次循环时都重绘屏幕
        #让最近绘制的屏幕可见
        gf.update_screen(ai_settings,screen,ship)

run_game()

我们来看看效果如何:
在这里插入图片描述
还可以,像点样子了。但是有个问题就是:如果按住左或者右箭头不松开的话,飞船是不会移动的,所以接下来我们要解决按住按键不松开让飞船可以不断的移动问题。

6.2允许不断移动

我们在坦克大战时知道,如果按住按键之响应一次,那我们很快就会输掉游戏,游戏也没有什么意思了,因为敌方是连续不断的出现,而我们只有不断的松开和按下按键才可以消灭对方,这样太累了,不好玩,现在这个游戏也一样,那我们需要解决这个问题,可以左右不断移动的问题。
我们将让游戏检测Pygame.KEYUP事件,以便玩家松开右箭头键时能够知道;然后结合使用KEYDOWN和KWYUP事件,以及一个名为moving_right/moving_left的标志来实现持续移动。
初始化moving_right/moving_left为False,按下按键为True,松开按键为False。
Ship控制飞船的属性,我们就把两个属性moving_right/moving_left和一个名为update()的方法。方法update()检查标志moving_right/moving_left的状态,True就移动飞船。
ship.py

import pygame

class Ship():

    def __init__(self,screen):
        """初始化飞船并设置其初始化位置"""
        self.screen = screen

        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()

        #将每艘新飞船放在屏幕底部中央
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom

        #移动标志
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """根据移动标志调整飞船的位置"""
        if self.moving_right:
            self.rect.centerx +=3
        elif self.moving_left:
            self.rect.centerx -= 3

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image,self.rect)

在方法__init__中,我们添加了属性self.moving_right/self.moving_left,并添加了方法update()。
下面来修改check_events(),使其在玩家按下右箭头键时将moving_right/moving_left设置为True,松开为False。
game_functions.py

import sys
import pygame

def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit(0)  #在这里的exit()中需要传递参数,他么的,竟然这么坑吗?
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                #向右移动飞船
                ship.moving_right = True
            elif event.key == pygame.K_LEFT:
                #向左移动飞船
                ship.moving_left = True
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                #向右移动飞船
                ship.moving_right = False
            elif event.key == pygame.K_LEFT:
                #向左移动飞船
                ship.moving_left = False
                
def update_screen(ai_settings,screen,ship):
    """更新屏幕上的图像,并切换到新屏幕"""

    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()

    #让最近绘制的屏幕可见
    pygame.display.flip()

然后我们在修改alien_invasion.py中的while循环,以便每次执行循环时都调用飞船的方法update():

import pygame
from settings import Settings
from ship import Ship
import game_functions as gf

def run_game():
    #初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")


    #设置背景色
    bg_color = (230, 230, 230)

    #创建一艘非飞船
    ship = Ship(screen)

    #开始游戏的主循环
    while True:

        #监视键盘和鼠标事件
        gf.check_events(ship)
        ship.update()
        #每次循环时都重绘屏幕
        #让最近绘制的屏幕可见
        gf.update_screen(ai_settings,screen,ship)

run_game()

在代码中使用了elif代码块,因为每个事件都只与一个键相关联;如果同时按下左右键,将检测到两个不同的事件。
飞船的位置将在检测到键盘事件后(在更新屏幕之前)更新。这样,玩家输入时,飞船的位置将更新,从而确保使用更新后的位置将飞船绘制到屏幕上。
来见证我们伟大成果的时刻~ - ~
在这里插入图片描述
但在这里有个问题就是飞船会飞到屏幕外面,并且移动的速度也不够精细;那我们接下来就想想办法,看看能不能解决这个问题。

6.4调整飞船的速度

我们当前飞船的移动速度时3像素,我们呢,在Settings类中新添加一个属性ship_speed_factor类,用于控制飞船的速度。我们将根据该属性来决定飞船在每次循环时最多移动多少距离。
settings.py

class Settings():
    """存储<<外星人入侵>>的所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        #屏幕设置
        self.screen_width = 800
        self.screen_height = 600
        self.bg_color = (230,230,230)

        #飞船的设置
        self.ship_speed_factor = 1.5

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

import pygame

class Ship():

    def __init__(self,settings,screen):
        """初始化飞船并设置其初始化位置"""
        self.screen = screen
        self.ai_settings = ai_settings

        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()

        #将每艘新飞船放在屏幕底部中央
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom

        #在飞船的属性center中存储小数值
        self.center = float(self.rect.centerx)

        #移动标志
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """根据移动标志调整飞船的位置"""
        if self.moving_right:
            self.center += self.ai_settings.ship_speed_factor
        elif self.moving_left:
            self.center -= self.ai_settings.ship_speed_factor

        #根据self.center更新rect对象
        self.rect.centerx = self.center

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image,self.rect)

在__init__()的形参列表中添加了ai_settings,让飞船能够获取其速度设置。然后将形参ai_settings的值存储在一个属性中,以便能够在update()中使用它,因为在调整的速度值为小数值,因此需要将位置存储在一个能够存储小数的变量中。可以使用小数来设置rect的属性,但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将只存储self.center的整数部分,但对显示飞船而言,问题不大。
在alien_invasion.py中创建Ship实例时,需要传入实参ai_settings:

import sys
import pygame
from settings import Settings
from ship import Ship
import game_functions as gf

def run_game():
    #初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")


    #设置背景色
    bg_color = (230, 230, 230)

    #创建一艘非飞船
    ship = Ship(ai_settings,screen)

    #开始游戏的主循环
    while True:

        #监视键盘和鼠标事件
        gf.check_events(ship)
        ship.update()
        #每次循环时都重绘屏幕
        #让最近绘制的屏幕可见
        gf.update_screen(ai_settings,screen,ship)

run_game()

6.5限制飞船的活动范围

好了解决了飞船移动速度的问题,接下来该解决飞船移动范围的问题。
我们将修改Ship类的方法update():

import pygame

class Ship():

    def __init__(self,ai_settings,screen):
        """初始化飞船并设置其初始化位置"""
        self.screen = screen
        self.ai_settings = ai_settings

        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.bmp')
        self.rect = self.image.get_rect()
        self.screen_rect = screen.get_rect()

        #将每艘新飞船放在屏幕底部中央
        self.rect.centerx = self.screen_rect.centerx
        self.rect.bottom = self.screen_rect.bottom

        #在飞船的属性center中存储小数值
        self.center = float(self.rect.centerx)

        #移动标志
        self.moving_right = False
        self.moving_left = False

    def update(self):
        """根据移动标志调整飞船的位置"""
        #更新飞船的center值,而不是rect
        if self.moving_right and self.rect.right < self.screen_rect.right:
            self.center += self.ai_settings.ship_speed_factor
        elif self.moving_left and self.rect.left > 0:
            self.center -= self.ai_settings.ship_speed_factor

        #根据self.center更新rect对象
        self.rect.centerx = self.center

    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image,self.rect)

在修改self.center的值之前检查飞船的位置。self.rect.right返回飞船外接矩形的右边缘的x坐标,如果这个值小于self.screen_rect.right的值,就说明飞船未触及屏幕右边缘。左边缘类似:如果rect的左边缘的x坐标大于零,就说明飞船未触及屏幕左边缘。这确保仅当飞船在屏幕内时,才调整self.center的值;这样飞船将在触及屏幕左边缘或右边缘后停止移动。
来看看运行效果:
在这里插入图片描述

6.6重构check_events()

随着游戏的完善,函数check_events()将越来越长,接下来我们就将其部分代码放在两个函数中;一个函数处理KEYDOWN事件,另一个处理KEYUP事件。
game_functions.py

import sys
import pygame

def check_keydown_events(event,ship):
    """响应按键"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
def check_keyup_events(event,ship):
    """响应松开"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    if event.key == pygame.K_LEFT:
        ship.moving_left = False

def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit(0)  #在这里的exit()中需要传递参数,他么的,竟然这么坑吗?
        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event,ship)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event,ship)
                
def update_screen(ai_settings,screen,ship):
    """更新屏幕上的图像,并切换到新屏幕"""

    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()

    #让最近绘制的屏幕可见
    pygame.display.flip()

添加了两个新函数:check_keydown_eventscheck_keyup_events,都接受两个形参event和ship,这样进一步替换,原来的event检测,就会更加的简洁,明了。

7.简单回顾

当前我们完成了游戏的部分基础部分,已经有四个文件,其中包含很多类、函数和方法。我们就先回顾一下这些文件,也好方便接下来添加新的文件和功能。

7.1 alien_invasion.py

主文件alien_invasion.py创建一系列整个游戏都要用到的对象:存储在ai_settings中的设置、存储在screen中的主显示surface以及一个飞船实例。文件alien_invasion.py还包含游戏的主循环,这是一个调用check_events()、ship.update()和update_screen()的while循环
要运行游戏就直接运行alien_invasion.py文件就可以了。

7.2 settings.py

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

7.3 game_functions.py

文件game_functions.py包含一系列函数,游戏的大部分工作都是在该文件中完成的。函数check_events()检测相关的事件,如按键和松开,函数check_keydown_events()和函数check_keyup_events()辅助来处理按键事件。模块中还包含函数update_screen(),它用于在每次执行主循环时都重绘屏幕。

7.4 ship.py

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

8 射击

前面我们已经创建好了飞船,接下来我们就为飞船添加些技能,射击功能。接下来编写按空格键时发射子弹(小矩形)的代码。子弹将在屏幕中向上穿行,抵达屏幕上边缘消失。

8.1 添加子弹设置

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

class Settings():
    """存储<<外星人入侵>>的所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        #屏幕设置
        self.screen_width = 800
        self.screen_height = 600
        self.bg_color = (230,230,230)

        #飞船的设置
        self.ship_speed_factor = 1.5

        #子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60 , 60 , 60

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

8.2 创建Bullet类

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

import pygame
from pygame.sprite import Sprite

class Bullet(Sprite):
    """一个对飞船发射的子弹进行管理的类"""

    def __init__(self,ai_settings,screen,ship):
        """在飞船所处的位置创建一个子弹的对象"""
        super(Bullet,self).__init__()
        self.screen = screen

        #在(0,0)处创建一个表示子弹的矩形,在设置正确的位置
        self.rect = pygame.Rect(0,0,ai_settings.bullet_width,ai_settings.bullet_height)
        self.rect.centerx = ship.rect.centerx
        self.rect.top = ship.rect.top

        #存储用小数表示的子弹位置
        self.y = float(self.rect.y)

        self.color = ai_settings.bullet_color
        self.speed_factor = ai_settings.bullet_speed_factor

    def update(self):
        """向上移动子弹"""
        #更新表示子弹位置的小数值
        self.y -= self.speed_factor
        #更新表示子弹的rect的位置
        self.rect.y = self.y

    def draw_bullet(self):
        """在屏幕上绘制子弹"""
        pygame.draw.rect(self.screen,self.color,self.rect)

Bullet类继承了我们从模块pygame.sprite中导入的Sprite类。通过使用精灵,可将游戏中相关的元素编组,进而同时操作编组中的所有元素。为创建子弹实例,需要向__init__()传递ai_settings、screen和ship实例,还调用了super()来继承Sprite

注意:代码super(Bullet, self).init()使用了python2.7语法。该语法也适用于Python3、但你也可以将这行代码简写为super().init()。

我们创建了子弹的属性rect。子弹并非基于图像的,因此我们必须使用pygame.Rect()类从空白开始创建一个矩形。创建这个类的实例时,必须提供矩形左上角的x坐标和y坐标,还有矩形的宽度和高度。我们在(0,0)处创建这个矩形,但接下来的两行代码将其移到了正确的位置,因为子弹的初始位置取决于飞船当前的位置。子弹的宽度和高度是从ai_settings中获取的。
我们将子弹的centerx设置为飞船的rect.centerx.子弹应从飞船顶部弹射出,因此我们将表示子弹的rect的top属性设置为飞船的rect的top属性,让子弹看起来像是从飞船中射出的。
我们将子弹的y坐标存储为小数值,以便能够满足微调子弹的速度,然后我们将子弹的颜色和速度设置分别存储到self.colorself.speed.factor中。
方法update()管理子弹的位置。发射出去后,子弹在屏幕中向上移动,这意味着y坐标将不断减小,因此为更新子弹的位置,我们从self.y中减去self.speed_factor的值。接下来,我们经self.rect.y设置为self.y的值。属性speed_factor让我们能够随着游戏的进行或根据需要提高子弹的速度、已调整游戏的行为。子弹发射后,其x坐标始终不变,因此子弹将沿直线垂直地往上穿行。
需要绘制子弹时,就调用draw_bullet()。函数draw.rect()使用存储在self.color中的颜色填充表示子弹的rect占据的屏幕部分。

8.3 将子弹存储到编组中

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

import pygame
from settings import Settings
from ship import Ship
import game_functions as gf
from pygame.sprite import Group

def run_game():
    #初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")


    #创建一艘非飞船
    ship = Ship(ai_settings,screen)
    #创建一个用于存储子弹的编组
    bullets = Group()

    #开始游戏的主循环
    while True:

        #监视键盘和鼠标事件
        gf.check_events(ai_settings,screen,ship,bullets)
        ship.update()
        bullets.update()
        gf.update_screen(ai_settings,screen,ship,bullets)

run_game()

导入pygame.sprite中的Group类,然后创建了一个Group实例,命名为bullets。编组时创建在while循环外面,这样无需每次运行该循环时都创建一个新的子弹编组。

如果在循环内部创建这样的编组,游戏运行时将创建数千个子弹编组,导致游戏慢的像蜗牛。如果发生了停滞现象,就需要排查while了。

8.4 开火

需要修改game_functions.py来实现飞船开火的效果:
game_functions.py

import sys
import pygame
from bullet import Bullet

def check_keydown_events(event,ai_settings,screen, ship,bullets):
    """响应按键"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        #创建一颗子弹,并将其加入到编组bullets中
        new_bullet = Bullet(ai_settings,screen,ship)
        bullets.add(new_bullet)

def check_keyup_events(event,ship):
    """响应松开"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    if event.key == pygame.K_LEFT:
        ship.moving_left = False

def check_events(ai_settings,screen, ship, bullets):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit(0)  #在这里的exit()中需要传递参数,他么的,竟然这么坑吗?
        elif event.type == pygame.KEYDOWN:
             check_keydown_events(event, ai_settings,screen,ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event,ship)
           
                
def update_screen(ai_settings,screen,ship,bullets):
    """更新屏幕上的图像,并切换到新屏幕"""

    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
     #在飞船和外星人后面重绘所有子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()

    #让最近绘制的屏幕可见
    pygame.display.flip()

   

好了,到这里我们的飞船已经可以发射子弹,并且可以在移动中发射子弹了。
在这里插入图片描述

8.5 删除已消失的子弹

上面的子弹抵达屏幕顶端后其实没有消失,只不过是跑到了屏幕的外面,接下来我们就删除抵达哦屏幕外面的子弹,来减小内存的消耗和处理能力的消耗。我们需要检查表示子弹的rect的bottom是否为0,为0表明子弹已经穿过了屏幕顶端。
alien_invasion.py

import pygame
from settings import Settings
from ship import Ship
import game_functions as gf
from pygame.sprite import Group

def run_game():
    #初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")


    #创建一艘非飞船
    ship = Ship(ai_settings,screen)
    #创建一个用于存储子弹的编组
    bullets = Group()

    #开始游戏的主循环
    while True:

        #监视键盘和鼠标事件
        gf.check_events(ai_settings,screen,ship,bullets)
        ship.update()
        bullets.update()

        #删除已销失的子弹
        for bullet in bullets.copy():
            if bullet.rect.bottom <= 0:
                bullets.remove(bullet)
        if len(bullets) != 0:
            print(len(bullets))  #在这里我们把子弹的存在屏幕内的个数做一个打印,只有子弹多于一个时才打印

        gf.update_screen(ai_settings,screen,ship,bullets)

run_game()

8.6 限制子弹的数量

在settings.py中存储所允许的最大子弹数:
settings.py

class Settings():
    """存储<<外星人入侵>>的所有设置的类"""

    def __init__(self):
        """初始化游戏的设置"""
        #屏幕设置
        self.screen_width = 800
        self.screen_height = 600
        self.bg_color = (230,230,230)

        #飞船的设置
        self.ship_speed_factor = 1.5

        #子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60 , 60 , 60)
        self.bullets_allowed = 3   #将在屏幕内可以存在的子弹限制为3颗

game_functions.py中我们需要检查当前屏幕中存储在的子弹个数:

import sys
import pygame
from bullet import Bullet

def check_keydown_events(event,ai_settings,screen, ship,bullets):
    """响应按键"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        #创建一颗子弹,并将其加入到编组bullets中
        #检查屏幕中存储在的子弹个数
        if len(bullets) < ai_settings.bullets_allowed:
            new_bullet = Bullet(ai_settings,screen,ship)
            bullets.add(new_bullet)

def check_keyup_events(event,ship):
    """响应松开"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    if event.key == pygame.K_LEFT:
        ship.moving_left = False

def check_events(ai_settings,screen, ship, bullets):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit(0)  #在这里的exit()中需要传递参数,他么的,竟然这么坑吗?
        elif event.type == pygame.KEYDOWN:
             check_keydown_events(event, ai_settings,screen,ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event,ship)
           
                
def update_screen(ai_settings,screen,ship,bullets):
    """更新屏幕上的图像,并切换到新屏幕"""

    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
     #在飞船和外星人后面重绘所有子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()

    #让最近绘制的屏幕可见
    pygame.display.flip()

   

当然,你也可以把子弹的数目改为多个,这个自己随意就好,怎么开心怎么来啊
在这里插入图片描述

8.7 创建函数update_bullets()

我们进一步的简化alien_functions.py中的程序,把管理子弹的代码移到模块game_functions.py中;创建一个函数update_bullets()在game_functions.py中:
game_functions.py

import sys
import pygame
from bullet import Bullet

def check_keydown_events(event,ai_settings,screen, ship,bullets):
    """响应按键"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
        #创建一颗子弹,并将其加入到编组bullets中
        if len(bullets) < ai_settings.bullets_allowed:
            new_bullet = Bullet(ai_settings,screen,ship)
            bullets.add(new_bullet)

def check_keyup_events(event,ship):
    """响应松开"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    if event.key == pygame.K_LEFT:
        ship.moving_left = False

def check_events(ai_settings,screen, ship, bullets):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit(0)  #在这里的exit()中需要传递参数,他么的,竟然这么坑吗?
        elif event.type == pygame.KEYDOWN:
             check_keydown_events(event, ai_settings,screen,ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event,ship)
           
                
def update_screen(ai_settings,screen,ship,bullets):
    """更新屏幕上的图像,并切换到新屏幕"""

    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
     #在飞船和外星人后面重绘所有子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()

    #让最近绘制的屏幕可见
    pygame.display.flip()

def update_bullets(bullets):
    """更新子弹的位置,并删除已销失的子弹"""

    #更新子弹的位置
    bullets.update()

    #删除已销失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

    if len(bullets) != 0:
        print(len(bullets))

   

alien_functions.py

import pygame
from settings import Settings
from ship import Ship
import game_functions as gf
from pygame.sprite import Group

def run_game():
    #初始化游戏并创建一个屏幕对象
    pygame.init()
    ai_settings = Settings()
    screen = pygame.display.set_mode((ai_settings.screen_width,ai_settings.screen_height))
    pygame.display.set_caption("Alien Invasion")


    #创建一艘非飞船
    ship = Ship(ai_settings,screen)
    #创建一个用于存储子弹的编组
    bullets = Group()

    #开始游戏的主循环
    while True:

        #监视键盘和鼠标事件
        gf.check_events(ai_settings,screen,ship,bullets)
        ship.update()
        gf.update_bullets(bullets)

        gf.update_screen(ai_settings,screen,ship,bullets)

run_game()

8.8 创建函数fire_bullet()

下面继续进一步简化,把发射子弹的代码移到独立的函数中。
game_functions.py

import sys
import pygame
from bullet import Bullet

def check_keydown_events(event,ai_settings,screen, ship,bullets):
    """响应按键"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = True
    elif event.key == pygame.K_LEFT:
        ship.moving_left = True
    elif event.key == pygame.K_SPACE:
       fire_bullet(ai_settings,screen,ship,bullets)

def check_keyup_events(event,ship):
    """响应松开"""
    if event.key == pygame.K_RIGHT:
        ship.moving_right = False
    if event.key == pygame.K_LEFT:
        ship.moving_left = False

def check_events(ai_settings,screen, ship, bullets):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit(0)  #在这里的exit()中需要传递参数,他么的,竟然这么坑吗?
        elif event.type == pygame.KEYDOWN:
             check_keydown_events(event, ai_settings,screen,ship, bullets)
        elif event.type == pygame.KEYUP:
            check_keyup_events(event,ship)
           
                
def update_screen(ai_settings,screen,ship,bullets):
    """更新屏幕上的图像,并切换到新屏幕"""

    #每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
     #在飞船和外星人后面重绘所有子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    ship.blitme()

    #让最近绘制的屏幕可见
    pygame.display.flip()

def update_bullets(bullets):
    """更新子弹的位置,并删除已销失的子弹"""

    #更新子弹的位置
    bullets.update()

    #删除已销失的子弹
    for bullet in bullets.copy():
        if bullet.rect.bottom <= 0:
            bullets.remove(bullet)

    if len(bullets) != 0:
        print(len(bullets))

def fire_bullet(ai_settings,screen,ship,bullets):
    """如果还没有到达限制,就发射一颗子弹"""

    #创建新子弹,并将其加入到编组bullets中
    if len(bullets) < ai_settings.bullets_allowed:
        new_bullet = Bullet(ai_settings,screen,ship)
        bullets.add(new_bullet)
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值