项目描述:基于面向对象思想,使用 pygame 模块开发一个飞机大战小游戏。

最终目标:


使用 pygame 创建图形窗口

pygame 的初始化和退出

import pygame

pygame.init()

# 游戏代码
pass

pygame.quit()

游戏中的坐标系

类似于 web 开发中的坐标系,原点在左上角,向右是 x 轴正方向,向下是 y 轴正方向。

在游戏中,所有元素都是以 矩形区域 来描述位置的,共有四个要素:(x, y) (width, height),分别表示矩形左上角坐标、矩形宽高。

pygame 专门提供了一个类 pygame.Rect,用此类创建出矩形对象,就可以很方便地描述矩形区域。

hero_rect = pygame.Rect(99, 499, 119, 124)

print("坐标原点 %d %d" % (hero_rect.x, hero_rect.y))
print("英雄大小 %d %d" % (hero_rect.width, hero_rect.height))

print("英雄大小 %d %d" % hero_rect.size)

创建游戏主窗口

pygame.display 用于创建、管理游戏窗口。

  • pygame.display.set_mode() :初始化游戏显示窗口
    set_mode(resolution=(0,0), flags=0, depth=0)
    = 表示缺省值。
    • resolution 指定屏幕的 宽 和 高,默认创建的窗口大小和屏幕大小一致
    • flags 参数指定屏幕的附加选项,例如是否全屏等等,默认不需要传递
    • depth 参数表示颜色的位数,默认自动匹配
      # 创建游戏主窗口
      screen = pygame.display.set_mode((480, 700))
  • pygame.display.update():刷新屏幕内容显示,稍后使用

游戏循环

游戏窗口创建完后,就可以基于这个窗口变量进行游戏主循环,设计一些游戏对象、函数了。

screen = pygame.display.set_mode((480, 700))

# 游戏循环
while True:
    pass

运行之后,会出现一个没有内容的游戏窗口:


图像

图像素材

飞机大战游戏就是通过对这些图片的组合处理实现的。

背景图像

图像文件初始是保存在磁盘上的,如果想在游戏内调用,需要加载到内存。

bg = pygame.image.load("./images/background.png")

screen.blit(bg, (0, 0))  # (0, 0) 表示坐标

pygame.display.update()  

英雄图像

hero = pygame.image.load("./images/me1.png")

screen.blit(hero, (189, 500))

pygame.display.update()

可以在 screen 对象完成所有 blit 方法之后,统一调用一次 display.update 方法,不必每绘制一张图像更新一次。

.png 格式的图片支持透明,所以显示出的飞机不会呈正方形,飞机图片的背景不会遮挡游戏背景。


游戏循环和游戏时钟

游戏循环

现在英雄飞机已经绘制到屏幕上了,接下来怎么能让飞机移动呢?

原理很简单:将多张图片快速地在屏幕上绘制,以产生连贯的视觉效果。
每次绘制的画面也称为 帧。每秒绘制画面的次数称为 帧数,亦即 fps。
帧数在 60 左右就足够流畅了。

游戏循环即游戏的主体。

游戏初始化:

  • 设置游戏窗口
  • 绘制图像初始位置
  • 设置游戏时钟

游戏循环:

  • 设置刷新频率
    • 按 60 fps 来,即每秒刷新 60 次
  • 检测用户交互
    • 键盘、鼠标等
  • 更新所有图像位置
  • 更新屏幕显示
    • pygame.display.update()

游戏时钟

类 pygame.time.Clock 可以方便地设置屏幕刷新帧率,即游戏时钟。

clock = pygame.time.Clock()
i = 0

while True:

    clock.tick(60)

    print(i)
    i += 1

英雄的简单动画实现

hero_rect = pygame.Rect(150, 500, 102, 126)

while True:

    clock.tick(60)

    hero_rect.y -= 1

    if hero_rect.y + hero_rect.height <= 0:
        hero_rect.y = 700

    screen.blit(bg, (0, 0))
    screen.blit(hero, hero_rect)

    pygame.display.update()

注意这里的背景图像也需要更新,即 screen.blit(bg, (0, 0)),否则只更新英雄的话会出现下面这种情况:

在游戏循环中监听事件

事件 event 就是游戏启动后,用户所做的操作。如:点击关闭按钮、点击鼠标、按下键盘 等。

监听 就是在游戏循环中,捕获用户的具体操作,针对性地做出响应。

pygame.event.get() 可以获得用户当前所做动作的事件 列表

while True:

    clock.tick(60)

    for event in pygame.event.get():

        if event.type == pygame.QUIT:
            print("退出游戏...")
            pygame.quit()
            exit()

精灵和精灵组

介绍

到此为止,图像加载、位置变化、绘制图像 都需要程序员分别编写代码进行处理,这是很麻烦的事情。
为了简化开发步骤,pygame 提供了两个 类,即 精灵 和 精灵组:

  • pygame.sprite.Sprite
    • 第一个 spirte 是模块的名称,第二个 Spirte 是类的名称
  • pygame.sprite.Group

在游戏开发中,通常把 显示图像的对象 叫做 精灵 Sprite。它有两个重要的属性:

  • image:要显示的图像
  • rect:图像要显示在屏幕的位置

默认的 update() 方法什么事情也没做,子类可以重写此方法,在每次刷新屏幕时,更新精灵位置。

pygame.sprite.Sprite 并没有提供 image 和 rect 两个属性,需要程序员从 pygame.sprite.Sprite 派生子类,并在子类的 初始化方法 中,设置 image 和 rect 属性。

一个 精灵组 可以包含多个 精灵 对象。

调用 精灵组 对象的 update() 方法可以自动调用组内每一个精灵的 update() 方法。

调用 精灵组 对象的 draw(屏幕对象) 方法,可以将组内每一个精灵的 image 绘制在 rect 位置。

注意:仍然需要调用 pygame.display.update() 才能在屏幕看到最终结果

派生精灵子类

  1. 新建 plane_sprites.py 文件
  2. 定义 GameSprite 继承自 pygame.sprite.Sprite

如果一个类的 父类 不是 object,在重写 初始化方法 时,一定要先使用 super() 调用一下父类的 __init__ 方法,保证父类中实现的初始化代码能够被正常执行。

plane_sprites.py 文件:

import pygame


class GameSprite(pygame.sprite.Sprite):
    """游戏精灵基类"""
    
    def __init__(self, image_name, speed=1):
        
        super().__init__()
        
        self.image = pygame.image.load(image_name)
        self.rect = self.image.get_rect()
        self.speed = speed

    def update(self, *args):
        
        self.rect.y += self.speed

创建敌机

初始化部分:

from plane_sprites import *

enemy1 = GameSprite("./images/enemy1.png")
enemy2 = GameSprite("./images/enemy1.png", 10)
enemy2.rect.x = 200
enemy_group = pygame.sprite.Group(enemy1, enemy2)

游戏循环内:

enemy_group.update()
enemy_group.draw(screen)

pygame.display.update()