Pygame学习笔记(四):chimp源码解析

学习资料:《Chimp Tutorial, Line by Line》

介绍

Pygame自带的示例程序中有一个叫“chimp”的小程序。它展示了很多 Pygame 的特性,比如:创建图形窗口,加载图像和声音文件,渲染TTF文本,基本事件(如鼠标事件)处理。

导入模块

这段代码导入了所需的模块,并且检查可选模块是否可用。

import os, sys
import pygame
from pygame.locals import 

if not pygame.font: print 'Warning, fonts disabled'
if not pygame.mixer: print 'Warning, sound disabled'

加载资源

我们分别用两个函数来加载图片和声音。下面逐个进行解析。

def load_image(name, colorkey=None):
    fullname = os.path.join('data', name)
    try:
        image = pygame.image.load(fullname)
    except pygame.error, message:
        print 'Cannot load image:', name
        raise SystemExit, message
    image = image.convert()
    if colorkey is not None:
        if colorkey is -1:
            colorkey = image.get_at((0,0))
        image.set_colorkey(colorkey, RLEACCEL)
    return image, image.get_rect()

我们把加载图片的语句封装在了 try/except 代码块中,这样一来,一旦出错,我们可以优雅地结束程序。

这段代码中,比较困扰我的是 colorkey 这个参数。经过查阅资料和动手实验,发现它是一个很重要的概念——色键,用于选定透明色。例如,给 colorkey 设定一个RGB值 (255, 255, 255),则加载后的图片中,原来像素值为 (255, 255, 255) 的点,都会变得透明。上面的代码中,如果给 colorkey 传的值为 -1,则会以原图最左上角的那个点的颜色值作为色键。

另外,参数 RLEACCEL 的作用:An RLEACCEL Surface will be slower to modify, but quicker to blit as a source.

def load_sound(name):
    class NoneSound:
        def play(self): pass
    if not pygame.mixer:
        return NoneSound()
    fullname = os.path.join('data', name)
    try:
        sound = pygame.mixer.Sound(fullname)
    except pygame.error, message:
        print 'Cannot load sound:', fullname
        raise SystemExit, message
    return sound

这段代码加载声音文件。我们首先检查 pygame.mixer 模块是否正确导入,如果没有,返回一个包含假的 play 方法的对象。在游戏中,它可以像正常的 Sound 对象那样进行调用,而不必做额外的错误检查。

游戏物体类

我们创建了两个类来表示游戏中的物体。

class Fist(pygame.sprite.Sprite):
    """moves a clenched fist on the screen, following the mouse"""
    def __init__(self):
        pygame.sprite.Sprite.__init__(self) #call Sprite initializer
        self.image, self.rect = load_image('fist.bmp', -1)
        self.punching = 0

    def update(self):
        "move the fist based on the mouse position"
        pos = pygame.mouse.get_pos()
        self.rect.midtop = pos
        if self.punching:
            self.rect.move_ip(5, 10)

    def punch(self, target):
        "returns true if the fist collides with the target"
        if not self.punching:
            self.punching = 1
            hitbox = self.rect.inflate(-5, -5)
            return hitbox.colliderect(target.rect)

    def unpunch(self):
        "called to pull the fist back"
        self.punching = 0

我们首先创建了玩家的拳头,它是从 pygame.sprite 模块中的 Sprite 类派生而来。

我的心得:

  1. 初始化时,先调用父类的 __init__ 方法。
  2. Rect 类的 move 和 move_ip 方法,作用相似,但有差别。move 方法会返回一个新的Rect(Returns a new rectangle that is moved by the given offset),move_ip 方法则是对当前 Rect 进行移动(Same as the Rect.move() method, but operates in place)。
  3. inflate 方法返回一个改变了尺寸的新矩形(Returns a new rectangle with the size changed by the given offset.)。
  4. Rect 类使用 colliderect 方法进行矩形的碰撞检测。
class Chimp(pygame.sprite.Sprite):
    """moves a monkey critter across the screen. it can spin the monkey when it is punched."""
    def __init__(self):
        pygame.sprite.Sprite.__init__(self) #call Sprite intializer
        self.image, self.rect = load_image('chimp.bmp', -1)
        screen = pygame.display.get_surface()
        self.area = screen.get_rect()
        self.rect.topleft = 10, 10
        self.move = 9
        self.dizzy = 0

    def update(self):
        "walk or spin, depending on the monkeys state"
        if self.dizzy:
            self._spin()
        else:
            self._walk()

    def _walk(self):
        """move the monkey across the screen, and turn at the ends"""
        newpos = self.rect.move((self.move, 0))
	if self.rect.left < self.area.left or \
                self.rect.right > self.area.right:
            self.move = -self.move
            newpos = self.rect.move((self.move, 0))
            self.image = pygame.transform.flip(self.image, 1, 0)
        self.rect = newpos

    def _spin(self):
        "spin the monkey image"
        center = self.rect.center
        self.dizzy += 12
        if self.dizzy >= 360:
            self.dizzy = 0
            self.image = self.original
        else:
            rotate = pygame.transform.rotate
            self.image = rotate(self.original, self.dizzy)
        self.rect = self.image.get_rect(center=center)

    def punched(self):
        "this will cause the monkey to start spinning"
        if not self.dizzy:
            self.dizzy = 1
            self.original = self.image

Chimp 这个类让猩猩在屏幕上来回移动,当它被拳头击中时,会兴奋地转圈圈。同样是 Sprite 的子类,它的初始化与 Fist 类似。在初始化时,它还把整个屏幕的尺寸信息保存在了 self.area 中。

update 函数,判断猩猩是否处于“晕眩”状态,进而决定调用 _walk 还是 _spin 方法。注意这两个方法的命名方式,在 Python 的规范中,加上“_”的前缀,表示该方法仅供类的内部使用。更正式一点,可以加上两个下划线“__”,这时 Python 会认为它是类的私有方法,在类的外部不可见。

_walk 函数把猩猩移动到一个新的位置,并做了边界检查,如果到了左右边界,就改变方向,并翻转图片。pygame.transform.flip 函数的原型是 flip(Surface, xbool, ybool),返回一个 Surface 对象。

当猩猩处于“晕眩”状态时,会调用 _spin 函数。dizzy 属性存储图像旋转的度数,达到 360 时,将猩猩图像重置为原始状态。代码中对 transform.rotate 函数做了一个局部引用“rotate”,主要是为了尽量缩短后面的代码,并非必须。注意,在 rotate 函数中,我们总是对原始图像(original)进行旋转。因为旋转操作会对图片质量产生轻微的影响,如果对某一幅图像反复进行旋转,会让图片质量变得越来越差。另外,旋转时,图像的尺寸也会发生变化,图像的四个角跑到了原来的矩形区域之外,导致图像尺寸变大。我们要确保新图像的中心点始终与原图像的中心点重合,这样图像在旋转时就不会到处乱动了。

初始化

在 main 函数中,用以下代码对 Pygame 进行初始化,并创建一个图形窗口:

pygame.init()
screen = pygame.display.set_mode((468, 60))
pygame.display.set_caption('Monkey Fever')
pygame.mouse.set_visible(0)

pygame.display 模块控制着所有和显示有关的设置。

我们设置了窗口的标题,并让鼠标在经过窗口时不可见。

创建背景

我们的程序将在背景上显示文字信息,最好创建一个 surface 对象来表示背景,这样可以反复使用它。

background = pygame.Surface(screen.get_size())
background = background.convert()
background.fill((250, 250, 250))

我们创建了一个与 display 窗口同样大小的 surface,并调用了 convert 函数。不带任何参数的 convert 可以确保我们的背景与 display 的格式相同,这让程序运行得更快。另外,还用了一个RGB值作为 fill 函数的参数,把背景填充为白色。

在背景上写字,并居中显示

下面,开始渲染字体。

if pygame.font:
    font = pygame.font.Font(None, 36)
    text = font.render("Pummel The Chimp, And Win $$$", 1, (10, 10, 10))
    textpos = text.get_rect(centerx=background.get_width()/2)
    background.blit(text, textpos)

我们创建了一个 font 对象,用它渲染出一个新的 surface,然后,把它粘贴(blit)到背景上,并水平居中。

Font 函数的第一个参数用于指定要用的字体文件,例如 font = pygame.font.Font('data/hellofont.ttf', 36),如果传入None,则使用默认字体。第二个参数指定字体大小。

render 函数根据指定的文本渲染出一个大小合适的 surface 对象,它的函数原型是render(text, antialias, color, background=None)。我们将 antialias 设为真(这样可以让文字看起来更平滑),然后用 (10, 10, 10) 把字体设为深灰色。background 参数用于指定文字的背景色,如果不设置或设为 None,则使用透明色。

Surface.get_rect 函数返回一个尺寸刚好能覆盖该 Surface 对象的矩形,并且默认情况下左上角的坐标总是 (0, 0)。我们可以传入键值对来指定此矩形的位置。

最后,我们把渲染好的字体 blit 到了已经居中的矩形区域上。


可以显示中文字符,但必须设置中文字体。我下载了一个“天真无邪体”,用法如下:

font = pygame.font.Font(os.path.join('data', '天真无邪体.ttf'), 36)
text = font.render(u"打猩猩,赢美刀", 1, (10, 10, 10))

显示已设定好的背景

目前为止,我们只能看到一个黑色的窗口。用下面的代码,将背景显示出来。

screen.blit(background, (0, 0))
pygame.display.flip()

把整个背景 blit 到 display 窗口上,然后调用 flip 函数。

在 Pygame 中,对 display 的修改不是立即可见的。通常,display 必须先更新有变化的区域,然后才能对用户可见。由于 display 有两个缓存,为了使修改可见,必须交换(swap or flip)缓存。这就是 flip 函数的作用。

准备游戏对象

下面来创建游戏中用到的所有对象。

whiff_sound = load_sound('whiff.wav')
punch_sound = load_sound('punch.wav')
chimp = Chimp()
fist = Fist()
allsprites = pygame.sprite.RenderPlain((fist, chimp))
clock = pygame.time.Clock()

我们把 chimp 和 fist 加入到了一个名为 RenderPlain 的特殊精灵组中,这个精灵组会把它包含的所有精灵绘制到屏幕上。pygame.sprite.RenderPlain 与 pygame.sprite.Group 没有任何区别,详见 Pygame 文档

clock 对象用来控制游戏的帧率,我们将会在游戏的主循环中使用它,以确保游戏不会运行得太快。

主循环

就是一个无限循环。

while 1:
    clock.tick(60)

所有的游戏都运行在某个循环中,通常的执行顺序是:检查计算机的状态和用户输入,移动所有对象并更新它们的状态,然后,把它们在屏幕上画出来。我们这里的例子也是如此。

我们也调用了 clock 对象的方法,这可以确保我们的游戏运行速度不超过每秒 60 帧。

处理输入事件

以下是处理事件队列(event queue)的一个简单示例:

for event in pygame.event.get():
    if event.type == QUIT:
        return
    elif event.type == KEYDOWN and event.key == K_ESCAPE:
        return
    elif event.type == MOUSEBUTTONDOWN:
        if fist.punch(chimp):
            punch_sound.play() #punch
            chimp.punched()
        else:
            whiff_sound.play() #miss
    elif event.type == MOUSEBUTTONUP:
        fist.unpunch()

首先,我们获取了 Python 中所有可能的事件,并对它们进行迭代。

更新精灵

allsprites.update()

精灵组有一个 update 方法,它会调用精灵组内所有精灵的 update 方法。

绘制整个场景

screen.blit(background, (0, 0))
allsprites.draw(screen)
pygame.display.flip()

第一行,把背景贴(blit)到整个屏幕上,这个步骤会擦除上一帧的所有内容(这样做,效率会轻微下降,但对目前这个游戏来说已经足够好了)。

第二行绘制出精灵组中所有的精灵。第三行将缓存中的内容显示到屏幕上。

游戏结束

用户退出游戏时,需要做清理工作。这在 Pygame 中很容易。事实上,由于所有变量都是自动析构(destructed)的,我们根本不需要做什么事情!