项目描述:基于面向对象思想,使用 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 并没有提供 imagerect 两个属性,需要程序员从 pygame.sprite.Sprite 派生子类,并在子类的 初始化方法 中,设置 imagerect 属性。

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

调用 精灵组 对象的 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()