Python 外星人入侵游戏(一):武装飞船(下)

接着做接着做接着做

来源:《Python编程:从入门到实践》

5 重构:模块game_functions

  • 重构:旨在简化既有代码的结构,使其更容易扩展
  • 下面创建一个名为game_functions的新模块,它存储大量让《外星人入侵》运行的函数
  • 创建模块game_functions,可避免alien_invasion.py太长,逻辑也更易理解

5.1 函数check_events()

  • 管理事件的代码移到check_events()的函数中,以简化run_game()并隔离事件管理循环
  • 隔离事件循环,可将事件管理与游戏的其他方面(如更新屏幕)分离

game_functions.py

import sys
import pygame

def check_events():
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
  • 导入事件检查循环要使用的sys和pygame
  • 函数check_events()不需要形参,函数体复制alien_invasion.py的事件循环
  • 下面来修改alien_invasion.py,导入模块game_functions,再将事件循环替换为函数check_events():

alien_invasion.py

import pygame
import game_functions as gf

def run_game():
    --snip--
    # 开始游戏的主循环
    while True:
        
        gf.check_events()
        
        # 每次循环时都重绘屏幕
        --snip--
  • 主程序文件不再需要直接导入sys,因为模块game_functions中使用了它

5.2 函数update_screen()

  • 进一步简化run_game(),将更新屏幕的代码移到update_screen()的函数中,并将此函数放在模块game_functions中

game_functions.py

--snip--
def check_events():
    --snip--
def update_screen(ai_settings, screen, ship):
    """更新屏幕上的图像,并切换到新屏幕"""
    # 每次循环时都重绘屏幕
    screen.fill(ai_settings.bg_color)
    ship.blitme()
            
    # 让最近绘制的屏幕可见
    pygame.display.flip()
  • update_screen()包含三个形参:ai_settings、screen、ship
  • 现在将主程序的while循环更新屏幕的代码替换为函数update_screen()

alien_invasion.py

--snip--
def run_game():
    --snip--
    # 开始游戏的主循环
    while True:
        gf.check_events()
        gf.update_screen(ai_settings, screen, ship)
  • 这两个函数让while更简单,让后续开发更容易:在模块game_functions完成大部分工作而不是run_game()

练习12-1 蓝色天空:创建一个背景为蓝色的Pygame窗口

test12-1.py

import sys
import pygame
from settings import Settings

def run_game():
    pygame.init()
    
    settings = Settings()
    screen = pygame.display.set_mode(
        (settings.screen_width, settings.screen_height))
    pygame.display.set_caption("程浩宇的蓝色天空")
    
    # 开始游戏的主循环
    while True:
        
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                sys.exit()
            
        screen.fill(settings.bg_color)
        # 让最近绘制的屏幕可见
        pygame.display.flip()
run_game()

settings.py

class Settings():
    def __init__(self):
        self.screen_width = 1200
        self.screen_height = 800
        self.bg_color = (70, 130, 180)

运行主程序,结果如图:
在这里插入图片描述

练习12-2 游戏角色:找一幅你喜欢的游戏角色位图图像或将一副图像转换为位图。创建一个类,将该角色绘制到屏幕中央,并将该图像的背景色设置为屏幕背景色,或将屏幕背景色设置为该图像的背景色

test12-2.py
直接用现成的小飞船吧,背景色用上面的蓝色,再把小飞船的背景色改成屏幕背景色…
将角色绘制到屏幕中央,设置相应rect对象的属性centerx、centery即可
下面就把模块ship放上来吧,主程序和模块settings变动不大

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.centery = self.screen_rect.centery
        
    def blitme(self):
        """在指定位置绘制飞船"""
        self.screen.blit(self.image, self.rect)

运行主程序,发现小飞船成功绘制到屏幕中央了:
在这里插入图片描述

6 驾驶飞船

  • 用户按左或右箭头时作出响应
  • 首先专注于向右移动,再使用同样的道理来控制向左移动
  • 通过这样做,你将学会如何控制屏幕图像的移动

6.1 响应按键

  • 每当用户按键,都将在Pygame注册一个事件
  • 事件通过方法pygame.event.get()获取的,因此在函数check_events()中,每次按键都将被注册为一个KEYDOWN事件
  • 例如,如果按下的是右箭头键,我们就增大飞船的rect.centerx的值,将飞船右移

game_functions.py

def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            sys.exit()
                
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                # 右移飞船
                ship.rect.centerx += 1
  • 现在函数check_events()中包含形参ship
  • 在函数check_events()内部,在事件循环中添加了一个elif代码块,以便在Pygame检查到KEYDOWN事件时做出响应
  • 读取属性event.key,检查按下的是否是右箭头键(pygame.K_RIGHT),若是,ship.rect.centerx的值加1,飞船右移
  • 在alien_invasion.py中,更新check_events() 代码:

alien_invasion.py

    # 开始游戏的主循环
    while True:
        gf.check_events(ship)
        gf.update_screen(ai_settings, screen, ship)
  • 不过现在运行程序的话,你会发现每按一次右箭头键,飞船向右移动1像素
  • 但并非控制飞船的高效方式。下面改进控制方式,允许持续移动

6.2 允许不断移动

  • 让游戏检测pygame.KEYUP事件,以便玩家松开右箭头键时我们知道这一点
  • 结合使用KEYDOWN和KEYUP事件,以及一个moving_right的标志来实现持续移动
    • 飞船不动时,moving_right为False
    • 玩家按下→时,moving_right为True
    • 玩家松开时,moving_right为False
  • 飞船的属性由Ship类控制,下面添加一个方法update()和一个属性moving_right

ship.py

class Ship():
    def __init__(self, screen):
        --snip--
        # 移动标志
        self.moving_right = False
        
    def update(self):
        """根据移动标志调整飞船的位置"""
        if self.moving_right:
            self.rect_centerx += 1
        
    def blitme(self):
        --snip--
  • 下面来修改check_events(),玩家按下→时将moving_right设置为True,松开时将moving_right设置为False:

game_functions.py

def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        --snip--
        elif event.type == pygame.KEYDOWN:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = True
        
        elif event.type == pygame.KEYUP:
            if event.key == pygame.K_RIGHT:
                ship.moving_right = False
  • 上面修改了按下→时响应的方式:不直接调整飞船的位置,而只是将moving_right设置为True
  • 接着添加了一个elif代码块,用于响应KEYUP事件:玩家松开→时,将moving_right设置为False
  • 最后,我们需要修改alien_invasion.py的while循环,以便每次执行循环时都调用飞船的方法update()

alien_invasion.py

    # 开始游戏的主循环
    while True:
        gf.check_events(ship)
        ship.update()
        gf.update_screen(ai_settings, screen, ship)
  • 现在运行alien_invasion.py,按住→,飞船能够不断右移,直到你松开为止

6.3 左右移动

  • 飞船能够不断右移后,添加左移的逻辑很容易
  • 下面再次修改Ship类函数check_events()

ship.py

    def __init__(self, screen):
        --snip--
        # 移动标志
        self.moving_right = False
        self.moving_left = False
        
    def update(self):
        """根据移动标志调整飞船的位置"""
        if self.moving_right:
            self.rect.centerx += 1
        if self.moving_left:
            self.rect.centerx -= 1
  • 方法__init__()中,添加了标志moving_left
  • 方法update()中,添加了一个if代码块而不是elif代码块:当玩家同时按下左右箭头键,将先增大self.rect.centerx,再降低这个值,即飞船的位置保持不变
  • 下面来修改check_events()

game_functions.py

def check_events(ship):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        --snip--
        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
  • 这里之所以使用elif代码块,是因为每个事件都只与一个键相关联
  • 如果同时按下了←→,将检测到两个不同事件
  • 此时运行主程序,发现能够不断地左右移动飞船;同时按←→键,飞船纹丝不动

6.4 调整飞船的速度

  • 当前,每次执行while循环时,飞船最多移动1像素
  • 现在在Settings类中添加属性ship_speed_factor用于控制飞船的速度

settings.py

class Settings():
    def __init__(self):
        --snip--
        # 飞船的设置
        self.ship_speed_factor = 1.5
  • 通过将ship_speed_factor速度设置指定为小数值,可在后面加快游戏的节奏时更细致地控制飞船的速度
  • 然而,rect的centerx等属性只能存储整数值,因此需要对Ship类做些修改:

ship.py

def __init__(self, ai_settings, screen):
        """初始化飞船并设置其初始位置"""
        self.screen = screen 
        self.ai_settings = ai_settings
        --snip--
        
        # 将每艘新飞船放在屏幕底部中央
        --snip--
        
        #在飞船的属性center中存储小数值
        self.center = float(self.rect.centerx)
        
        # 移动标志
        self.moving_right = False
        self.moving_left = False
        
    def update(self):
        """根据移动标志调整飞船的位置"""
        # 更新飞船的center值,而不是rect
        if self.moving_right:
            self.center += self.ai_settings.ship_speed_factor
        if self.moving_left:
            self.center -= self.ai_settings.ship_speed_factor
        
        # 根据self.center更新rect对象
        self.rect.centerx = self.center
  • __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

def run_game():
    --snip--
    # 创建一艘飞船
    ship = Ship(ai_settings, screen)
    --snip--

6.5 限制飞船的活动范围

  • 运行程序,你会发现,按住箭头键的时间过长,飞船就跑到屏幕外面去了……
  • 下面修复这个问题,让飞船到达屏幕边缘后停止移动
  • 修改Ship类的方法update():

ship.py

    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
        if self.moving_left and self.rect.left > 0:
            self.center -= self.ai_settings.ship_speed_factor
        
        # 根据self.center更新rect对象
        self.rect.centerx = self.center
  • self.rect.right返回飞船外接矩形右边缘的x坐标,如果这个值小于self.screen_rect.right的值,就说明飞船未触及屏幕右边缘
  • 左边缘情况与之类似:self.rect.left即rect的左边缘的x坐标,它大于零,就说明未触及屏幕左边缘
  • 这就确保了飞船只有在屏幕内时,才能调整self.center的值
  • 此时运行alien_invasion.py,飞船将在触及左右边缘不能移动

6.6 重构check_events()

  • 随着游戏开发的进行,函数check_events()愈来愈长,现在将其部分代码放在两个函数中:一个处理KEYDOWN事件,另一个处理KEYUP事件:

game_functions.py

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
    elif 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()
                
        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, ship)
        
        elif event.type == pygame.KEYUP:
            check_keyup_events(event, ship)

7 射击

  • 下面来添加射击功能:玩家按下空格键时发射子弹
  • 子弹将在屏幕中向上穿行,抵达屏幕上边缘后消失

7.1 添加子弹设置

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

settings.py

    def __init__(self):
        --snip--
        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
  • 创建宽3像素、高15像素的深灰色子弹
  • 子弹速度比飞船稍低

7.2 创建Bullet类

  • Bullet类,前半部分:

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

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

  • 下面是bullet.py的第二部分——方法update() 和 draw_bullet()

bullet.py

	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)
  • 方法update()管理子弹的位置
  • 需要绘制子弹时,调用draw_bullet()
  • 函数draw.rect()使用存储在self.color中的颜色填充表示子弹的rect占据的屏幕部分

8.3 将子弹存储在编组中

  • 完成上面的准备后,下面要做的是:在玩家每次按空格键时都射出一发子弹
  • 首先,在alien_invasion.py中创建一个编组(group),存储所有有效的子弹,以便能够管理发射出去的所有子弹
  • 这个group将是pygame.sprite.Group类的一个实例
  • pygame.sprite.Group类 类似于列表,但提供有助于开发游戏的额外功能
  • 在主循环中,使用这个group在屏幕上绘制子弹,以及更新每颗子弹的位置:

alien_invasion.py

from pygame.sprite import Group
--snip--

def run_game():
    --snip--
    # 创建一艘飞船
    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)
--snip--
  • 创建了一个Group实例,将其命名为bullets
  • 这个编组是在while循环外创建的 ,这样就无需每次运行该循环时都创建一个新的子弹编组,导致游戏慢得像蜗牛。如果游戏停滞不前,请仔细查看主while循环中发生的情况

注意:如果在循环内部创建这样的编组,游戏运行时将创建数千个子弹编组

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

8.4 开火

  • game_functions.py中,需要修改check_keydown_events(),以便在玩家按空格键时发射一颗子弹
  • 还需修改update_screen(),确保在调用flip()前在屏幕上重绘每颗子弹

game_functions.py

def check_keydown_events(event, ai_settings, screen, ship, bullets):
    --snip--
    elif event.key == pygame.K_SPACE:
        # 创建一颗子弹,并将其加入到编组bullets中
        new_bullet = Bullet(ai_settings, screen, ship)
        bullets.add(new_bullet)
--snip--

def check_events(ai_settings, screen, ship, bullets):
    """响应按键和鼠标事件"""
    for event in pygame.event.get():
        --snip--
        elif event.type == pygame.KEYDOWN:
            check_keydown_events(event, ai_settings, screen, ship, bullets)
        --snip--
            
def update_screen(ai_settings, screen, ship, bullets):
    --snip--
    # 在飞船和外星人后面重绘所有子弹
    for bullet in bullets.sprites():
        bullet.draw_bullet()
    
    ship.blitme()
  • 编组bullets传递给了check_keydown_events()
  • 玩家按空格键时,创建一颗新子弹(一个名为new_bullet的Bullet实例),并使用方法add()将其加入到编组bullets中
  • 方法bullets.sprites()返回一个列表,其中包含编组bullets的所有精灵
  • 为绘制发射的所有子弹,我们遍历编组bullets中的精灵,并对每个精灵都调用draw_bullet()
  • 现在运行alien_invasion.py,能够左右移动飞船,并发射任意数量的子弹

在这里插入图片描述

8.5 删除已消失的子弹

  • 现在,子弹到达屏幕顶端后消失,但这些子弹实际上依然存在,y坐标为负数,且越来越小
  • 这个问题将导致它们继续消耗内存和处理能力
  • 我们需要将这些已消失的子弹删除:

alien_invasion.py

    # 开始游戏的主循环
    while True:
        --snip--
        bullets.update()
        
        # 删除已消失的子弹
        for bullet in bullets.copy():
            if bullet.rect.bottom <= 0:
                bullets.remove(bullet)
        print(len(bullets))
        --snip--
  • 在for中,不应从列表或编组中删除条目,因此必须遍历编组的副本
  • 使用方法copy()来设置for循环,这能够在循环中修改bullets
  • print语句显示还有多少颗子弹,从而核实已消失的子弹确实删除了
  • 如果这些代码没有问题,将print语句删除。如果留下这个语句,游戏的速度将大大降低

8.6 限制子弹数量

  • 很多设计游戏都对同时出现在屏幕上的子弹数量进行限制,以鼓励玩家有目标地设计
  • 首先,在settings中存储所允许的最大子弹数:

settings.py

        # 子弹设置
        self.bullet_speed_factor = 1
        self.bullet_width = 3
        self.bullet_height = 15
        self.bullet_color = 60, 60, 60
        self.bullets_allowed = 3
  • 接着在game_functions.py的check_keydown_events()中,在创建新子弹前检查未消失的子弹数是否小于该设置

game_functions.py

def check_keydown_events(event, ai_settings, screen, ship, bullets):
    --snip--
        # 创建一颗子弹,并将其加入到编组bullets中
        if len(bullets) < ai_settings.bullets_allowed:
            new_bullet = Bullet(ai_settings, screen, ship)
            bullets.add(new_bullet)
  • 现在运行这个游戏,屏幕上最多只能有3颗子弹

8.7 创建函数update_bullets()

  • 编写并检查子弹管理代码后,可将其移到模块game_functions中,以让主程序alien_invasion.py尽可能简单
  • 创建一个名为update_bullets()的函数,将其添加到game_functions.py的末尾:

game_functions.py

def update_bullets(bullets):
    """更新子弹的位置,并删除已消失的子弹"""
    # 更新子弹的位置
    bullets.update()
        
    # 删除已消失的子弹
    for bullet in bullets.copy():
    	if bullet.rect.bottom <= 0:
    		bullets.remove(bullet)

alien_invasion.py

    while True:
        gf.check_events(ai_settings, screen, ship, bullets)
        ship.update()
        gf.update_bullets(bullets)
        gf.update_screen(ai_settings, screen, ship, bullets)
  • 让主循环包含尽可能少的代码,这样只看函数名就能迅速知道游戏发生的情况

8.8 创建函数fire_bullet()

  • 下面将发射子弹的代码移到一个独立的函数中:

game_functions.py

def check_keydown_events(event, ai_settings, screen, ship, bullets):
    --snip--
    elif event.key == pygame.K_SPACE:
        fire_bullet(ai_settings, screen, ship, 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)

目前,运行游戏,确认发射子弹时依然没有错误
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值