12.6 驾驶飞船
下面来让玩家能够左右移动飞船。我们将编写代码,在用户按左或右箭头键时做出相应。我们将首先专注于向右移动,再使用同样大院里来控制向左移动。

12.6.1 响应按键
每当用户按键时,需要检查按下的是否是出发行动的键。例如,如果玩家按下的是右箭头键,就增大飞船的rect.centerx值,将飞船向右移动:
    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

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

如果现在运行alien_invasion.py,则每按右箭头键一次,飞船都向右移动一个像素。这是一个开端,但并非控制飞船的高效方式。下面来改进控制方式,允许持续移动。
12.6.2 允许持续移动
玩家按住右箭头键不放时,我们希望飞船不断向右移动,直到玩家松开为止。我们将让游戏检测pygame.KEYUP事件,以便直到玩家合适松开右箭头键。然后,结合使用KEYDOWN和KEYUP事件以及一个名为moving_right的标志来实现持续移动。

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

飞船的属性由ship类控制,因此要给这个类添加一个名为moving_right的属性和一个名为upadte()的方法。方法updeate()检查标志moving_right的状态。如果该标志为True,就调整飞船的位置。我们将在while循环中调用这个方法,以调整飞船的位置。
class Ship:
    def __init__(self,ai_game):
        """初始化飞船并设置其初始位置"""
        self.screen = ai_game.screen
        self.screen_rect = ai_game.screen.get_rect()

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

        #对于每艘新飞船,都将其放在屏幕底部的中央。
        self.rect.midbottom = self.screen_rect.midbottom

        #移动标志
        self.moving_right = False

    def update(self):
        """根据移动标志来调整飞船的位置"""
        if self.moving_right:
            self.rect.x += 1

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

接下来,需要修改_check_events(),使其在玩家按下右箭头键时将moving_right设置为True,并在玩家松开时将moving_right设置为False:
    def _check_events(self):
        """ 响应键盘和鼠标事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()

            elif event.type == pygame.KEYDOWN:
                   elif event.type == pygame.KEYDOWN:
                if event.key == pygame.K_RIGHT:
                    self.ship.moving_right = True

            elif event.type == pygame.KEYUP:
                if event.key == pygame.K_RIGHT:
                    self.ship.moving_right = False

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

最后,需要修改run_game()中的 while循环,一遍每次执行循环时都调用飞船的方法update():
    def run_game(self):
        """开始游戏的主循环"""
        while True:
            self._check_events()
            self.ship.update()
            self._update_screen()

飞船的位置将在检测到键盘事件后更新。这样,玩家输入时,飞船的位置将更新,从而确保使用更新后的位置将飞船绘制到屏幕上。
12.6.3 左右移动
现在飞船能够持续向右移动了,添加想做移动的逻辑也很容易。我们将再次修改ship类和方法_check_events()。

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

        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.png')
        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_right:
            self.rect.x += 1

        if self.moving_left:
            self.rect.x -= 1

还需对_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
12.6.4 调整飞船的速度
每次执行while循环时,飞船最多移动1像素,但可在settings类中添加属性ship_seppd
,用于控制飞船的速度。
    def __init__(self):
        """初始化游戏的设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230,230,230)
        
        #飞船速度
        self.ship_speed = 1.5

通过速度设置指定为小数值,可在后面加快游戏节奏。然而rect的X等属性只能存储整数值,因此需要对ship类做些修改:
class Ship:
    def __init__(self,ai_game):
        """初始化飞船并设置其初始位置"""
        self.screen = ai_game.screen
        self.settings = ai_game.settings
        self.screen_rect = ai_game.screen.get_rect()

        #加载飞船图像并获取其外接矩形
        self.image = pygame.image.load('images/ship.png')
        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):
        """根据移动标志来调整飞船的位置"""
        #更新飞船而不是rect对象的X值
        if self.moving_right:
            self.x += self.settings.ship_speed

        if self.moving_left:
            self.x -= self.settings.ship_speed
        
        #根据self.x更新rect对象
        self.rect.x = self.x


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

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

现在在update()中调整飞船的位置是,将self.x的值增减settings.ship_speed的值。更新self.x后,再根据它来更新控制飞船位置的self.rect.x。self.rect.x只存储self.x的整数部分,但对显示飞船而言,问题不大。
12.6.5 限制飞船的活动范围
当前,只要玩家按住箭头键的时间足够长,飞船将飞到屏幕之外。下面要让飞船到达屏幕边缘后停止移动。为此,要修改ship类的方法update():
    def update(self):
        """根据移动标志来调整飞船的位置"""
        #更新飞船而不是rect对象的X值
        if self.moving_right and self.rect.right < self.screen_rect.right:
            self.x += self.settings.ship_speed

        if self.moving_left and self.rect.left > 0:
            self.x -= self.settings.ship_speed

上述代码在修改self.x的值之前检查飞机的位置。self.rect.right返回飞船外接矩形右边缘的X坐标。如果这个值小于self.scree_rect.right的值,就说明飞船未触及屏幕右边缘。左边缘情况与此类似。这确保仅当飞船在屏幕内时,才调整self.x值。
12.6.6 重构_check_events()
随着游戏的开发,方法_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_evevts(event)

            elif event.type == pygame.KEYUP:
                self._check_keyup_evevts(event)

    def _check_keydown_evevts(self,event):
        """响应按键"""
        if event.key ==pygame.K_RIGHT:
            self.ship.moving_right = True
        elif event.key == pygame.K_LEFT:
            self.ship.moving_left = True

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

我们创建了两个新的辅助方法。它们都包含形参self和event。这两个方法是从_check_events()中复制而来的,因此将方法_check_events()中相应的代码替换成了对这两个新方法的调用。
12.6.8 在全屏模式下运行游戏
要在全屏模式下运行该游戏,可在__init__()中做如下修改:
 def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()

        self.settings = Settings()
        self.screen = pygame.display.set_mode((0,0),pygame.FULLSCREEN)
        self.settings.screen_width= self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height

        pygame.display.set_caption("Alien Invasion")

创建屏幕是,传入了尺寸(0,0)以及参数pygame.FULLSCREEN。这让pygame生成一个覆盖整个显示器的屏幕。由于无法预先知道屏幕的宽度和高度,要在创建屏幕后更新这些设置:使用屏幕的rect的属性width和height来更新对象settings。
12.7 简单回顾
目前一共有三个文件:alien_invasion.py,seyyings.py,ship.py。
12.8 射击
下面添加射击功能,我们将编写在玩家按空格键时发射子弹(用小矩形表示)的代码。子弹姜葱屏幕中向上飞行,抵达屏幕上边缘后消失。

12.8.1 添加子弹设置
首先,更新settings.py,在方法__init__()末尾存储新类Bullet所需的值:
    def __init__(self):
        """初始化游戏的设置"""
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (230,230,230)

        #子弹设置
        self.bullet_speed = 1.0
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60,60,60)

这些设置创建宽3像素、高15像素的深灰色子弹。子弹的速度比飞船稍低。
12.8.2 创建Bullet 类
下面来创建存储Bullet 类的文件bullet.py,其前半部分如下:
import pygame
from pygame.sprite import Sprite

class Bullet(Sprite):
    """管理飞船所发射子弹的类"""
    
    def __init__(self,ai_game):
        """在飞船当前位置创建一个子弹对象"""
        super().__init__()
        self.screen = ai_game.screen
        self.settings = ai_game.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_game.ship.rect.midtop
        
        #存储小数表示的子弹位置
        self.y= float(self.rect.y)

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

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

将子弹的erect.midtop设置为飞船的rect.midtop。这样子弹将从飞船顶部出发,看起来像是从飞船中社畜的。我们将子弹的Y坐标存储为小数值,一遍能够微调子弹的速度。

下面是bullet.py的第二部分,包括方法update()和draw_bullet():
    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)

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

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

需要绘制子弹时,我们调用draw_bullet()。draw.rect()函数使用存储在self.color中的颜色填充表示子弹的rect占据的屏幕部分。
12.8.3 将子弹存储到编组中
我们将在AlienInvasion中创建一个编组froup,用于存储所有有效的子弹。这个编组是pygame.sprite类的一个实例。

首先,在__init__()中创建用于存储子弹的编组:
    def __init__(self):
        """初始化游戏并创建游戏资源"""
        pygame.init()

        self.settings = Settings()
        self.screen = pygame.display.set_mode((0,0),pygame .FULLSCREEN)
        self.settings.screen_width= self.screen.get_rect().width
        self.settings.screen_height = self.screen.get_rect().height
        self.bullets = pygame.sprite.Group()

然后在while循环中更新子弹的位置:
    def run_game(self):
        """开始游戏的主循环"""
        while True:
            self._check_events()
            self.ship.update()
            self._update_screen()
            self.bullets.update()

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

12.8.4 开火
在AlienInvasion中,需要修改_check_keydown_events(),以便在玩家按空格时发射一颗子弹。还需要修改_update_screen(),确保掉用flip()前在屏幕上重绘每颗子弹。

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

import sys

import pygame

from settings import Settings
from ship import Ship
from bullet import Bullet

    def _check_keydown_evevts(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)
        self.ship.blitme()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()

12.8.5 删除消失的子弹
子弹到达屏幕顶端后消失,但只是不显示,实际上依然存在。这回继续消耗内存和处理能力。需要将这些消失的子弹删除。为此,需要检测表示子弹的rect的bottom属性是否为0.如果是,则表明子弹已飞过屏幕顶端:
    def run_game(self):
        """开始游戏的主循环"""
        while True:
            self._check_events()
            self.ship.update()
            self._update_screen()
            for bullet in self.bullets.copy():
                if bullet.rect.bottom <= 0:
                    self.bullets.remove(bullet)
            print(len(self.bullets))

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

当确认子弹被城阙删除后,请将这个函数调用print()删除,如果不删除,游戏的速度将大大降低,因为将输出写入终端花费的时间比图形绘制到游戏窗口花费的时间还多。

12.8.6 限制子弹数量
很多设计游戏都对屏幕上的子弹数量进行限制,以鼓励玩家有目标的设计。下面我们来进行设置:

首先,在settings.py中存储最大子弹数:
        #子弹设置
        self.bullet_speed = 1.0
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = (60, 60, 60)
        self.bullets_allowed = 3

这将未消失的子弹数限制为三颗。在alieninvasion的_fire_bullet()中,在创建薪资单前检查微笑是的子弹数是否小于该设置:
    def _fire_bullet(self):
        """创建一颗子弹,并将其加入编组bullets中"""
        if len(self.bullets) < self.settings.bullets_allowed:
            new_bullet = Bullet(self)
            self.bullets.add(new_bullet)

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

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

    def _update_bullets(self):
        """更新子弹的位置并删除消失的子弹"""
        # 更新子弹的位置
        self.bullets.update()

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

    def _update_screen(self):
        """更新屏幕上的图像,并切换到新屏幕"""
        # 每次循环时都重绘屏幕
        self.screen.fill(self.settings.bg_color)
        self.ship.blitme()
        for bullet in self.bullets.sprites():
            bullet.draw_bullet()

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

run_game()中的while循环又变得简单了:
    def run_game(self):
        """开始游戏的主循环"""
        while True:
            self._check_events()
            self.ship.update()
            self._update_screen()
            self._update_bullets()

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

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

不得不说,学到这里的时候我已经蒙了,没错,蒙了。我决定去看看别的教程来补充一下知识,否则感觉就是在单纯的抄书而已。我先去也,拜拜。

胭惜雨

2021年02月16日

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据