飞机大战项目(二)

  • 1.项目实战 —— 飞机大战
  • 2.英雄登场
  • 3.游戏背景
  • 4.游戏框架搭建

1.项目实战 —— 飞机大战

1.1 确认模块 —— pygame

  • pygame 就是一个 Python 模块,专为电子游戏设计
  • 官方网站:https://www.pygame.org/
    • 提示:要学习第三方模块,通常最好的参考资料就在官方网站

网站栏目

内容

GettingStarted

在各平台安装模块的说明

Docs

pygame 模块所有 子类 的参考手册

安装 pygame

$ sudo pip3 install pygame

验证安装

$ python3 -m pygame.examples.aliens

2 英雄登场

目标

  • 设计 英雄子弹
  • 使用 pygame.key.get_pressed() 移动英雄
  • 发射子弹

2.1 设计 英雄子弹

英雄需求

  1. 游戏启动后,英雄 出现在屏幕的 水平中间 位置,距离 屏幕底部 120 像素
  2. 英雄 每隔 0.5 秒发射一次子弹,每次 连发三枚子弹
  3. 英雄 默认不会移动,需要通过 左/右 方向键,控制 英雄 在水平方向移动

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_初始化方法

子弹需求

  1. 子弹英雄 的正上方发射 沿直线上方 飞行
  2. 飞出屏幕后,需要从 精灵组 中删除

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_初始化方法_02

Hero —— 英雄

  • 初始化方法
    • 指定 英雄图片
    • 初始速度 = 0 —— 英雄默认静止不动
    • 定义 bullets 子弹精灵组 保存子弹精灵
  • 重写 update() 方法
    • 英雄需要 水平移动
    • 并且需要保证不能 移出屏幕
  • 增加 bullets 属性,记录所有 子弹精灵
  • 增加 fire 方法,用于发射子弹

Bullet —— 子弹

  • 初始化方法
    • 指定 子弹图片
    • 初始速度 = -2 —— 子弹需要向上方飞行
  • 重写 update() 方法
    • 判断 是否飞出屏幕,如果是,从 精灵组 删除

2.2 创建英雄

2.2.1 准备英雄类

  • plane_sprites 新建 Hero
  • 重写 初始化方法,直接指定 图片名称,并且将初始速度设置为 0
  • 设置 英雄的初始位置

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_初始化方法_03

  • centerx = x + 0.5 * width
  • centery = y + 0.5 * height
  • bottom = y + height

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_sed_04

class Hero(GameSprite):
    """英雄精灵"""

    def __init__(self):

        super().__init__("./images/me1.png", 0)

        # 设置初始位置
        self.rect.centerx = SCREEN_RECT.centerx
        self.rect.bottom = SCREEN_RECT.bottom - 120

2.3 绘制英雄

  1. __create_sprites,添加 英雄精灵英雄精灵组
    • 后续要针对 英雄碰撞检测 以及 发射子弹
    • 所以 英雄 需要 单独定义成属性
  2. __update_sprites,让 英雄精灵组 调用 updatedraw 方法
代码实现
  • 修改 __create_sprites 方法如下:
# 英雄组
self.hero = Hero()
self.hero_group = pygame.sprite.Group(self.hero)
  • 修改 __update_sprites 方法如下:
self.hero_group.update()
self.hero_group.draw(self.screen)

2.4 移动英雄位置

pygame 中针对 键盘按键的捕获,有 两种 方式

  • 第一种方式 判断 event.type == pygame.KEYDOWN
  • 第二种方式
    1. 首先使用 pygame.key.get_pressed() 返回 所有按键元组
    2. 通过 键盘常量,判断元组中 某一个键是否被按下 —— 如果被按下,对应数值为 1

提问 这两种方式之间有什么区别呢?

  • 第一种方式
elif event.type == pygame.KEYDOWN and event.key == pygame.K_RIGHT:
    print("向右移动...")
  • 第二种方式
# 返回所有按键的元组,如果某个键被按下,对应的值会是1
keys_pressed = pygame.key.get_pressed()
# 判断是否按下了方向键
if keys_pressed[pygame.K_RIGHT]:
    print("向右移动...")

结论

  • 第一种方式 event.type 用户 必须要抬起按键 才算一次 按键事件操作灵活性会大打折扣
  • 第二种方式 用户可以按住方向键不放,就能够实现持续向某一个方向移动了,操作灵活性更好

2.4.1 移动英雄位置

演练步骤

  1. Hero 类中重写 update 方法
    • 速度 speed英雄 rect.x 进行叠加
    • 不需要调用父类方法 —— 父类方法只是实现了单纯的垂直运动
  2. __event_handler 方法中根据 左右方向键 设置英雄的 速度
    • 向右 => speed = 2
    • 向左 => speed = -2
    • 其他 => speed = 0

代码演练

  • Hero 类,重写 update() 方法,根据速度水平移动 英雄的飞机
def update(self):

    # 飞机水平移动
    self.rect.x += self.speed
  • 调整键盘按键代码
# 获取用户按键
keys_pressed = pygame.key.get_pressed()

if keys_pressed[pygame.K_RIGHT]:
    self.hero.speed = 2
elif keys_pressed[pygame.K_LEFT]:
    self.hero.speed = -2
else:
    self.hero.speed = 0

2.4.2 控制英雄运动边界

  • Hero 类的 update() 方法判断 英雄 是否超出 屏幕边界

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HZr89smI-1580112219177)(media/15025349250200/003_pygame.Rect.png)]

  • right = x + width 利用 right 属性可以非常容易的针对右侧设置精灵位置

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_sed_05

def update(self):

    # 飞机水平移动
    self.rect.x += self.speed
    
    # 判断屏幕边界
    if self.rect.left < 0:
        self.rect.left = 0
    if self.rect.right > SCREEN_RECT.right:
        self.rect.right = SCREEN_RECT.right

2.5 发射子弹

需求回顾 —— 英雄需求

  1. 游戏启动后,英雄 出现在屏幕的 水平中间 位置,距离 屏幕底部 120 像素
  2. 英雄 每隔 0.5 秒发射一次子弹,每次 连发三枚子弹
  3. 英雄 默认不会移动,需要通过 左/右 方向键,控制 英雄 在水平方向移动

2.5.1 添加发射子弹事件

pygame定时器 使用套路非常固定:

  1. 定义 定时器常量 —— eventid
  2. 初始化方法 中,调用 set_timer 方法 设置定时器事件
  3. 游戏循环 中,监听定时器事件

代码实现

  • Hero 中定义 fire 方法
def fire(self):
    print("发射子弹...")
  • plane_main.py 的顶部定义 发射子弹 事件常量
# 英雄发射子弹事件
HERO_FIRE_EVENT = pygame.USEREVENT + 1
  • __init__ 方法末尾中添加 发射子弹 事件
# 每隔 0.5 秒发射一次子弹
pygame.time.set_timer(HERO_FIRE_EVENT, 500)
  • __event_handler 方法中让英雄发射子弹
elif event.type == HERO_FIRE_EVENT:
    self.hero.fire()

2.5.2 定义子弹类

需求回顾 —— 子弹需求
  1. 子弹英雄 的正上方发射 沿直线上方 飞行
  2. 飞出屏幕后,需要从 精灵组 中删除
Bullet —— 子弹
  • 初始化方法
    • 指定 子弹图片
    • 初始速度 = -2 —— 子弹需要向上方飞行
  • 重写 update() 方法
    • 判断 是否飞出屏幕,如果是,从 精灵组 删除
定义子弹类
  • plane_sprites 新建 Bullet 继承自 GameSprite
  • 重写 初始化方法,直接指定 图片名称,并且设置 初始速度
  • 重写 update() 方法,判断子弹 飞出屏幕从精灵组删除
class Bullet(GameSprite):
    """子弹精灵"""
    
    def __init__(self):
        
        super().__init__("./images/bullet1.png", -2)
 
    def update(self):
    
        super().update()
        
        # 判断是否超出屏幕,如果是,从精灵组删除
        if self.rect.bottom < 0:
            self.kill()

2.6 发射子弹

演练步骤

  1. Hero初始化方法 中创建 子弹精灵组 属性
  2. 修改 plane_main.py__update_sprites 方法,让 子弹精灵组 调用 updatedraw 方法
  3. 实现 fire() 方法
    • 创建子弹精灵
    • 设置初始位置 —— 在 英雄的正上方
    • 子弹 添加到精灵组

代码实现

  • 初始化方法
# 创建子弹的精灵组
self.bullets = pygame.sprite.Group()
  • 修改 fire() 方法
def fire(self):

    # 1. 创建子弹精灵
    bullet = Bullet()
    
    # 2. 设置精灵的位置
    bullet.rect.bottom = self.rect.y - 20
    bullet.rect.centerx = self.rect.centerx
    
    # 3. 将精灵添加到精灵组
    self.bullets.add(bullet)
一次发射三枚子弹

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_sed_06

  • 修改 fire() 方法,一次发射三枚子弹
def fire(self):

    for i in (1, 2, 3):
        # 1. 创建子弹精灵
        bullet = Bullet()
        
        # 2. 设置精灵的位置
        bullet.rect.bottom = self.rect.y - i * 20
        bullet.rect.centerx = self.rect.centerx
        
        # 3. 将精灵添加到精灵组
        self.bullets.add(bullet)

3.游戏背景

3.1 背景交替滚动的思路确定

运行 备课代码观察 背景图像的显示效果:

  • 游戏启动后,背景图像连续不断地 向下方 移动
  • 视觉上 产生英雄的飞机不断向上方飞行的 错觉 —— 在很多跑酷类游戏中常用的套路
    • 游戏的背景 不断变化
    • 游戏的主角 位置保持不变

3.1.1 实现思路分析

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_初始化方法_07

解决办法

  1. 创建两张背景图像精灵
    • 1完全和屏幕重合
    • 2 张在 屏幕的正上方
  2. 两张图像 一起向下方运动
    • self.rect.y += self.speed
  3. 任意背景精灵rect.y >= 屏幕的高度 说明已经 移动到屏幕下方
  4. 移动到屏幕下方的这张图像 设置到 屏幕的正上方
    • rect.y = -rect.height

3.1.2 设计背景类

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_python_08

  • 初始化方法
    • 直接指定 背景图片
    • is_alt 判断是否是另一张图像
      • False 表示 第一张图像,需要与屏幕重合
      • True 表示 另一张图像,在屏幕的正上方
  • update() 方法
    • 判断 是否移动出屏幕,如果是,将图像设置到 屏幕的正上方,从而实现 交替滚动

继承 如果父类提供的方法,不能满足子类的需求:

  • 派生一个子类
  • 在子类中针对特有的需求,重写父类方法,并且进行扩展

3.2 显示游戏背景

3.2.1 背景精灵的基本实现

  • plane_sprites 新建 Background 继承自 GameSprite
class Background(GameSprite):
    """游戏背景精灵"""

    def update(self):

        # 1. 调用父类的方法实现
        super().update()

        # 2. 判断是否移出屏幕,如果移出屏幕,将图像设置到屏幕的上方
        if self.rect.y >= SCREEN_RECT.height:
            self.rect.y = -self.rect.height

3.2.2 在 plane_main.py 中显示背景精灵

  1. __create_sprites 方法中创建 精灵精灵组
  2. __update_sprites 方法中,让 精灵组 调用 update()draw() 方法

__create_sprites 方法

def __create_sprites(self):

    # 创建背景精灵和精灵组
    bg1 = Background("./images/background.png")
    bg2 = Background("./images/background.png")
    bg2.rect.y = -bg2.rect.height
    
    self.back_group = pygame.sprite.Group(bg1, bg2)

__update_sprites 方法

def __update_sprites(self):

    self.back_group.update()
    self.back_group.draw(self.screen)

3.2.3 利用初始化方法,简化背景精灵创建

思考 —— 上一小结完成的代码存在什么样的问题?能否简化?

  • 在主程序中,创建的两个背景精灵传入了相同的图像文件路径
  • 创建 第二个 背景精灵 时,在主程序中,设置背景精灵的图像位置

思考 —— 精灵 初始位置 的设置,应该 由主程序负责?还是 由精灵自己负责

答案 —— 由精灵自己负责

  • 根据面向对象设计原则,应该将对象的职责,封装到类的代码内部
  • 尽量简化程序调用一方的代码调用

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_初始化方法_09

  • 初始化方法
    • 直接指定 背景图片
    • is_alt 判断是否是另一张图像
      • False 表示 第一张图像,需要与屏幕重合
      • True 表示 另一张图像,在屏幕的正上方

plane_sprites.py 中实现 Background初始化方法

def __init__(self, is_alt=False):

    image_name = "./images/background.png"
    super().__init__(image_name)
       
    # 判断是否交替图片,如果是,将图片设置到屏幕顶部
    if is_alt:
        self.rect.y = -self.rect.height
  • 修改 plane_main__create_sprites 方法
# 创建背景精灵和精灵组
bg1 = Background()
bg2 = Background(True)

self.back_group = pygame.sprite.Group(bg1, bg2)

4.游戏框架搭建

目标 —— 使用 面相对象 设计 飞机大战游戏类

  • 明确主程序职责
  • 实现主程序类
  • 准备游戏精灵组

4.1 明确主程序职责

  • 回顾 快速入门案例,一个游戏主程序的 职责 可以分为两个部分:
    • 游戏初始化
    • 游戏循环
  • 根据明确的职责,设计 PlaneGame 类如下:

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_sed_10

提示 根据 职责 封装私有方法,可以避免某一个方法的代码写得太过冗长

  • 游戏初始化 —— __init__() 会调用以下方法:

方法

职责

__create_sprites(self)

创建所有精灵和精灵组

  • 游戏循环 —— start_game() 会调用以下方法:

方法

职责

__event_handler(self)

事件监听

__check_collide(self)

碰撞检测 —— 子弹销毁敌机、敌机撞毁英雄

__update_sprites(self)

精灵组更新和绘制

__game_over()

游戏结束

4.2 实现飞机大战主游戏类

4.2.1 明确文件职责

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_sed_11

  • plane_main
    1. 封装 主游戏类
    2. 创建 游戏对象
    3. 启动游戏
  • plane_sprites
    • 封装游戏中 所有 需要使用的 精灵子类
    • 提供游戏的 相关工具
代码实现
  • 新建 plane_main.py 文件,并且设置为可执行
  • 编写 基础代码
import pygame
from plane_sprites import *


class PlaneGame(object):
    """飞机大战主游戏"""

    def __init__(self):
        print("游戏初始化")

    def start_game(self):
        print("开始游戏...")


if __name__ == '__main__':
    # 创建游戏对象
    game = PlaneGame()

    # 开始游戏
    game.start_game()

4.2.2 游戏初始化部分

  • 完成 __init__() 代码如下:
def __init__(self):
    print("游戏初始化")
    
    # 1. 创建游戏的窗口
    self.screen = pygame.display.set_mode((480, 700))
    # 2. 创建游戏的时钟
    self.clock = pygame.time.Clock()
    # 3. 调用私有方法,精灵和精灵组的创建
    self.__create_sprites()

def __create_sprites(self):
    pass
使用 常量 代替固定的数值
  • 常量 —— 不变化的量
  • 变量 —— 可以变化的量

应用场景

  • 在开发时,可能会需要使用 固定的数值,例如 屏幕的高度700
  • 这个时候,建议 不要 直接使用固定数值,而应该使用 常量
  • 在开发时,为了保证代码的可维护性,尽量不要使用 魔法数字

常量的定义

  • 定义 常量 和 定义 变量 的语法完全一样,都是使用 赋值语句
  • 常量命名 应该 所有字母都使用大写单词与单词之间使用下划线连接

常量的好处

  • 阅读代码时,通过 常量名 见名之意,不需要猜测数字的含义
  • 如果需要 调整值,只需要 修改常量定义 就可以实现 统一修改

代码调整

  • plane_sprites.py 中增加常量定义
import pygame

# 游戏屏幕大小
SCREEN_RECT = pygame.Rect(0, 0, 480, 700)
  • 修改 plane_main.py 中的窗口大小
self.screen = pygame.display.set_mode(SCREEN_RECT.size)

4.2.3 游戏循环部分

  • 完成 start_game() 基础代码如下:
def start_game(self):
    """开始游戏"""
    
    print("开始游戏...")
       
    while True:
    
        # 1. 设置刷新帧率
        self.clock.tick(60)
        
        # 2. 事件监听
        self.__event_handler()
        
        # 3. 碰撞检测
        self.__check_collide()
        
        # 4. 更新精灵组
        self.__update_sprites()
        
        # 5. 更新屏幕显示
        pygame.display.update()

def __event_handler(self):
    """事件监听"""
    
    for event in pygame.event.get():
    
        if event.type == pygame.QUIT:
            PlaneGame.__game_over()

def __check_collide(self):
    """碰撞检测"""
    pass

def __update_sprites(self):
    """更新精灵组"""
    pass
    
@staticmethod
def __game_over():
   """游戏结束"""

   print("游戏结束")
   pygame.quit()
   exit()

4.3 准备游戏精灵组

4.3.1 确定精灵组

Python玩转飞机大战开题报告国内外研究综述 python飞机大战项目报告书_Group_12

4.3.2 代码实现

  • 创建精灵组方法
def __create_sprites(self):
    """创建精灵组"""
    
    # 背景组
    self.back_group = pygame.sprite.Group()
    # 敌机组
    self.enemy_group = pygame.sprite.Group()
    # 英雄组
    self.hero_group = pygame.sprite.Group()
  • 更新精灵组方法
def __update_sprites(self):
    """更新精灵组"""
    
    for group in [self.back_group, self.enemy_group, self.hero_group]:
    
        group.update()
        group.draw(self.screen)