Pygame 外星人入侵(4)飞船移动
目录
引言
直到上一篇博文为止,我们实现了:
1、游戏屏幕的绘制
2、飞船的初始化和绘制
3、现有代码的封装和重构
实现效果:
在这篇博文中,我们将要实现通过键盘方向键来控制飞船左右移动。
一、移动飞船
根据需求来分析应该如何实现。
1、首先,我们应该在 事件响应 中增加对于玩家按下方向键这样的行为的响应,也就是 响应玩家按方向键。
2、其次,在玩家按下方向键后,改变飞船的坐标。
3、最后,绘制飞船到屏幕上,只是此时的飞船,可能已经是坐标改变后的飞船了。
1、响应玩家按键
在我们之前封装在 game_functions 模块中的 check_events() 函数中增加响应玩家按键的逻辑。
另外,应该响应的是方向键,而不是其他不管的按键,这里我们仅以右方向键为例:
def chekc_events(ship):
for event in pygame.event.get():
if event.type == QUIT:
sys.exit()
# 当检测到玩家按下键盘上某个按键时
elif event.type == pygame.KEYDOWN:
# 当玩家按下的键是 右方向键
if event.key == K_RIGHT:
print('玩家按下了右方向键')
这样我们在游戏运行时如果按下了右方向键,控制台就会打印出相应的提示语句。
即实现了响应玩家的操作。
2、修改飞船的坐标
现在,我们需要在响应玩家操作后,相应地修改飞船的坐标,使飞船在之后绘制屏幕的时候,显示在不一样的位置。
只需要修改响应后的业务就可以了:
def chekc_events(ship):
for event in pygame.event.get():
if event.type == QUIT:
sys.exit()
# 当检测到玩家按下键盘上某个按键时
elif event.type == pygame.KEYDOWN:
# 当玩家按下的键是 右方向键
if event.key == K_RIGHT:
# 修改飞船的横坐标,使其 +1
ship.rect.centerx += 1
此时,在主模块中运行游戏,就可以在按下右方向键后,看到飞船向右移动一个像素的位置了。
(不知道博文里能不能发动图。。。)
二、优化1:连续移动
虽然实现了飞船移动,但是也发现了相应的问题,其实也不能说是问题,而是可以优化的点:
我们的程序只有在按下一次右方向键时才会让飞船动一下,如果按住右键不动,飞船只会动一下,不会一直移动直到我们松手。
在很多游戏中,这都是不合适的,我们会经常需要一次性移动一大段距离,现在我们写的程序,如果想移动一大段距离,需要连续多次敲击按键,这是我们需要优化的地方。
于是想到,我们不仅要响应玩家按下按键的事件,还要响应玩家松开按键的事件,且在玩家按下和松开按键之间,都要移动飞船。
基于此,可以通过一个状态值来标识飞船是否移动。
简单总结一下,我们需要做如下修改:
1、在飞船类中增加属性,表示飞船移动的状态,初始值为False
2、响应玩家按下按键后,将状态值变为True
3、响应玩家松开按键后,将状态值变为False
4、在飞船类中定义一个方法,可以通过状态值来移动飞船,当状态值为True时,移动飞船,否则不移动。
1、设置飞船移动的状态属性
class Ship():
# 其他方法省略
...
self.moving_right = False
2、响应事件以修改状态
def chekc_events(ship):
for event in pygame.event.get():
if event.type == QUIT:
sys.exit()
# 当检测到玩家按下键盘上某个按键时
elif event.type == pygame.KEYDOWN:
# 当玩家按下的键是 右方向键
if event.key == K_RIGHT:
# 修改飞船移动状态,使其移动
ship.moving_right = True
# 当检测到玩家松开键盘上某个按键时
elif event.type == pygame.KEYUP:
# 当玩家松开的键是 右方向键
if event.key == K_RIGHT:
# 修改飞船移动状态,使其不再移动
ship.moving_right = False
3、update() 修改飞船位置
此时,我们在按下和松开右方向键时,分别修改了飞船的状态属性值。
现在,就可以定义方法,通过状态值来移动飞船了
class Ship():
# 其他方法和属性都省略
...
def update(self):
# 当状态值为真时,移动飞船
if moving_right == True:
self.rect.centerx += 1
# 当状态值为假时,什么都不用做,因此也不用写else语句了
4、修改主模块代码
修改飞船类的代码后,我们重新捋一下主模块中游戏循环代码的逻辑:
1、进入死循环
2、检查事件(这里根据事件已经修改了飞船的状态值,但飞船的坐标还没有修改)
3、改变飞船的坐标,即调用 update() 函数
4、绘制背景
5、绘制飞船
6、显示屏幕
while True:
# 事件检测,现在我们已经将事件循环的代码封装到函数中了,直接运行即可
check_events(my_ship, my_screen, my_settings)
# 每次执行完事件检测循环后,都更新飞船的位置
my_ship.update()
# 绘制游戏画面,包括屏幕背景、飞船,并将内容显示到游戏屏幕上
update_screen(my_screen, my_settings, my_ship)
此时运行游戏,可以在玩家按住右方向键不放时,一直移动飞船,直到玩家松开按键。
三、左右移动
至此我们实现了飞船的右移,现在按照差不多的逻辑实现飞船的左移。
1、响应玩家按下和松开左方向键的事件
2、响应后移动飞船的业务
# 事件响应
def chekc_events(ship):
for event in pygame.event.get():
if event.type == QUIT:
sys.exit()
# 当检测到玩家按下键盘上某个按键时
elif event.type == pygame.KEYDOWN:
# 当玩家按下的键是 右方向键
if event.key == K_RIGHT:
# 修改飞船移动状态,使其移动
ship.moving_right = True
# 当玩家按下的键是 左方向键
elif event.key == K_LEFT:
# 修改飞船移动状态,使其移动
ship.moving_left = True
# 当检测到玩家松开键盘上某个按键时
elif event.type == pygame.KEYUP:
# 当玩家松开的键是 右方向键
if event.key == K_RIGHT:
# 修改飞船移动状态,使其不再移动
ship.moving_right = False
# 当玩家松开的键是 左方向键
if event.key == K_LEFT:
# 修改飞船移动状态,使其不再移动
ship.moving_left = False
# 响应后的业务
class Ship():
# 其他方法和属性都省略
...
def update(self):
# 当状态值为真时,移动飞船
if moving_right == True:
self.rect.centerx += 1
if moving_left == True:
self.rect.centerx -= 1
值得注意的是,这里我们使用的是两个 if 语句 ,而不是一个 if elif 的语句,这是因为,当玩家同时按住左右键时,需要飞船保持不动。
如果用 if elif 语句的话,这里右移就会占“优势地位”,因为 if 条件满足后,就不会检查后面的 elif 条件了,所以就会一直右移。这显然是不合适的。
用两个 if 语句的话,保证了左右键都会被检查到,飞船在这种情况下就会保持不动。
四、优化2:调整飞船的速度
1、设置速度参数
之前的程序中,我们都设置飞船以 1 像素的距离来移动,如果我们想设置飞船的移动速度,那么就可以将这个参数放到设置类中去,比如我们此时想让飞船以每次 1.5 像素的距离移动:
1、在设置类中添加飞船速度的属性
2、飞船初始化时,传入设置参数
3、修改 update() 方法
class Settings():
...
self.ship_speed = 1.5
def __init__(self, screen, settings):
...
# 为了能在飞船对象中使用设置中的参数
self.settings = settings
def update(self):
# 当状态值为真时,移动飞船
if moving_right == True:
self.rect.centerx += self.settings.ship_speed
if moving_left == True:
self.rect.centerx -= self.settings.ship_speed
2、完善飞船调速(有点啰嗦)
至此,好像是解决了飞船调速的问题。
但是,这种处理其实是有问题的,问题出在 pygame 中 ,Surface 对象的rect 属性的值,是 int 型的。
也就是说,坐标只能存储整数,当我们每次都以浮点数进行加减时,实际上加减的距离是去掉小数部分的。因此,当我们把速度调整为1.5后,每一次移动飞船,仍然是以 1 的速度来移动的。
所以像我们刚才这样处理调速的逻辑,只能接受整数的速度值,浮点数的速度值,会自动忽略小数点部分。
而这显然不符合我们的预期。但是可以通过飞船的一个临时变量来保存飞船的坐标,这个临时变量是浮点型的,当我们移动飞船时,坐标的加减,先作用于这个临时变量上,这样是不会造成小数点被忽略的。而当玩家松开按键后,将这个临时变量的最终值,传递回飞船真正的坐标属性上。
这一段我描述的可能有些不清楚,我还是会在下面写上代码,如果还是不太清楚,可以在评论区留言。(有营销号内味儿了)
def update(self):
# 当状态值为真时,移动飞船
if moving_right == True:
# self.x 用来作为飞船坐标的临时值,它是浮点型的
self.x += self.settings.ship_speed
if moving_left == True:
self.x -= self.settings.ship_speed
# 被浮点数的速度值加减后,最终的结果再返回给坐标
self.centerx = self.x
在这之前,我们需要做如下准备工作:
1、在飞船初始化时,创建这个临时变量
2、这个临时变量的值,和飞船的坐标值相等
3、这个临时变量必须确保是浮点型的
class Ship():
...
def __init__(self,screen,settings):
...
# 一行代码,搞定上述的3点要求
self.x = float(self.rect.centerx)
至此,就实现了比较完善的飞船调速功能,以后需要调速,只需要在设置类中修改相应参数即可,且可以接受浮点数的速度值。
这里之所以说“比较完善”,是因为其实还是有一些缺陷的,因为即使我们通过临时变量获得了准确的坐标偏移量,但最后赋值给 centerx 时,由于坐标变量只接受整型数的特性,仍然会丢失偏移量的小数部分。只不过,这个误差比起优化前要小上不少,算是可以接受吧。
举个例子,比如我们分别以 1 和 1.5 的速度移动飞船 3 次。
优化前:
速度为 1 时的偏移量:int(1) + int(1) + int(1) = 3;误差为0
速度为 1.5 时的偏移量:int(1.5) + int(1.5) + int(1.5) = 3,误差为1.5
优化后:
速度为 1 时的偏移量:int(1 + 1 + 1) = 3 ;误差为0
速度为 1.5 时的偏移量:int(1.5+1.5+1.5) = 4;误差为0.5
还是之前说的,这部分我描述得可能不太好,也有点啰嗦。。。有好的解释方法也可以评论留言。
五、优化3:限制飞船活动范围
我们在移动飞船时,会发现一直往一个方向移动后,飞船会飞出屏幕。其实就是因为飞船的坐标超过了游戏屏幕的大小(显示范围)。因此需要修改一下飞船移动方法的逻辑:
1、在响应玩家按键事件后,移动飞船前,判断飞船的位置
2、只有飞船在屏幕右边界内时,才允许继续右移
3、只有飞船在屏幕左边界内时,才允许继续左移
def update(self):
# 当飞船正在向右移动时,飞船的横坐标递增
# 而且需要防止飞船飞出屏幕外
if self.moving_right and self.rect.right < self.screen_rect.right:
self.x += self.speed
# 当飞船正在向左移动时,飞船的横坐标递减
# 而且需要防止飞船飞出屏幕外
if self.moving_left and self.rect.left > 0:
self.x -= self.speed
# 将根据小数递增的坐标值给到飞船的坐标变量上
self.rect.centerx = self.x
六、封装代码
还是书上的老规矩,每次实现一些功能后,先不着急继续往后实现新功能,而是审视已有的代码,看能不能重构或者封装一部分代码。
这里我们在事件检测中对按键按下和按键松开两“种”事件进行了检测,随着之后可能会有越来越多的按键被我们使用,因此,将“按键检测“ 和 ”松键检测“这两部分分别封装是很有必要的。
1、封装”按键检测“
2、封装”松键检测“
3、修改”事件检测“方法的代码
# 封装事件循环中,所有 按下 键盘按键的事件
def check_keydown_events(event, ship, screen, settings):
# 当玩家在键盘上按下的是 右方向键时,飞船开始向右移动
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
# 当玩家在键盘上松开的是 左方向键时,飞船停止向左移动
elif event.key == pygame.K_LEFT:
ship.moving_left = False
def check_events(ship, screen, settings, bullets):
# 循环监视事件的发生
for event in pygame.event.get():
# 当玩家点击关闭按钮时,退出游戏
if event.type == pygame.QUIT:
sys.exit()
# 当玩家 按下键盘 发出事件时
elif event.type == pygame.KEYDOWN:
check_keydown_events(event, ship, screen, settings)
# 当玩家 松开键盘 发出事件时
elif event.type == pygame.KEYUP:
check_keyup_events(event, ship)
七、小结
在这篇博文中,我们实现了飞船的左右移动,且完善了这一功能:
1、长按长移,直到松手。
2、调速移动,接收小数。
3、限制范围,防止出界。
在学习开发这款游戏的过程中,我遇到一些不能从书上马上理解的东西,这个时候,我会停下来,先看一看自己已经掌握了哪些知识。
对于不能理解的内容,我的做法是先去 Pygame 官方文档中查看一些关键术语的解释,我每次会看一个板块,一个板块包括 Pygame 的一个模块或一个类。我会阅读自己已经见到过和使用过的模块和类。这是我的阅读顺序:
1、display 模块。因为最开始创建游戏屏幕时使用的模块就是这个。
2、Surface 类。因为 display 模块中提到,我们创建的屏幕本质上就是一个 Surface 对象,因此需要知道 Surface 到底是什么,能做什么。
3、image 模块。因为 Surface 类是用于控制图片的类,而导入图片是通过image模块来实现的。
4、Rect 类。Surface类的一个子类,每个 Surface 类都有一个 Rect 对象属性。
5、Color 类。这个比较简单,一笔带过。
值得一提的是,最初看书时,优化调速部分的叙述我不太能理解,不知道通过临时变量“绕一圈”的意义何在;但是我姑且记住了这种处理方法。而当我阅读了 Rect 类的官方文档后,发现了 坐标值 只接收整数的特性,这个时候,马上就联想到了之前不能理解的部分,也就迅速地返回去重新看一遍书上的叙述,发现此时的自己已经能看懂了。所以说,遇到不会的东西,先放一放,给自己充充电,说不定什么时候就会了。
另外,一个飞船移动的功能,自己实现的话,应该也能实现出来。但问题是怎样才能更好地实现这个功能。这篇博文通篇其实只是实现了一个功能,但绝大部分内容都是对功能地优化。所以,在自己写代码时,也要时刻注意这些细节,旨在写出高水平地代码。