6.3    走路动画

在上例中,马里奥走路只使用了一张图片,所以走路时马里奥是一直伸着胳膊滑动的。实际上马里奥走路是三张图片,如6‑7所示。

67马里奥走路的图片

马里奥在游戏中走路的画面如6‑8所示。

68马里奥走路的动画

从图可以看出,马里奥在移动时会不断切换图片。需要注意的是,这三张图片并不是连续的三帧,每个动作持续几帧才会切换到下一个动作的图片。只有当切换速度与移动速度相配合时,才能呈现出流畅连贯的走路效果。如果切换速度过快,会出现马里奥原地挥手的情况。如果切换速度过慢,则看起来像在慢动作滑行。

那么,究竟应该多久切换一次图片呢?答案是,通过实践来探索,运行程序并不断调整切换速度,直到达到满意的效果为止。

在我们的游戏中,每秒进行60帧的渲染。如果按照每帧显示不同的图片(如图1、图2、图3、图1循环),那将导致每秒图片切换60次,这样的速度太快了,需要限制一下切换速度。

一种控制切换速度的方法是计算帧数,比如设定每20帧切换一次图片。我们可以使用一个计数变量,每帧加1,当计数达到20时切换图片,然后将计数清零,如此循环。另一种方法是计算时间差,比如设定20毫秒切换一次图片。我们可以使用一个变量来保存上次切换图片的时间,如果当前时间距离上次切换时间超过了20毫秒,就切换图片并更新变量为当前时间,如此循环。

虽然计算帧数的方法比较简单,但存在一个问题,如果以后修改帧数为100,而切换频率仍然是每20帧一次,那么马里奥的动作切换速度就会变快。这就好比在播放查理·卓别林的黑白电影时,原本是以每秒16-18帧的速度拍摄的,如今以每秒24帧的速度播放,人物动作看起来就会显得快速。鉴于此,我们决定采用计算时间差的方法,无论帧数如何修改,都不影响动作切换的速度。

获取时间差有个简单方法,可以使用Clock对象的get_time()方法,它返回上两次调用tick()方法的时间差,单位是毫秒,如6‑9所示。从图可以看出,get_time()方法实际获取的就是上一帧的完整时间。

69获取时间差

假设马里奥每秒切换一次动作,那么一帧的时间肯定不够,因此需要等待多帧,累计时间差,直到累计值超过1秒才进行一次动作切换。如果上一帧由于电脑卡顿运行了10秒,时间差直接大于1秒,那本帧就直接切换动作。

除了动作切换,还需要考虑移动距离的计算。我们定义马里奥是匀速运动,需要根据时间差来计算移动距离。上一帧时间较短,本帧移动距离就较小,上一帧时间较长,本帧移动距离就较大。上一帧卡顿10秒钟这种情况,本帧需要对应移动10秒钟的距离,因为在游戏中的时间已经过去了10秒钟。

下面编写一个程序测试移动速度与切换速度的配合,代码如下所示:

06\03\Game.py

import pygame

from MarioImageCache import MarioImageCache

 

 

class Game:

    def __init__(self):

        # 初始化 Pygame

        pygame.init()

 

        # 窗口

        SCREEN_WIDTH = 256

        SCREEN_HEIGHT = 224

 

        # 先画到一个画布上

        self.screen = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT))

 

        # 真正的屏幕

        self.window = pygame.display.set_mode((SCREEN_WIDTH * 2, SCREEN_HEIGHT * 2), pygame.RESIZABLE)

        self.windowSize = self.window.get_rect().size

 

        # 设置窗口标题

        pygame.display.set_caption('super mario bros')

 

        # 避免输入法影响按键

        pygame.key.stop_text_input()

 

        # 创建时钟对象

        self.clock = pygame.time.Clock()

 

        # 初始化图片缓存

        self.marioImageCache = MarioImageCache()

 

        # 马里奥

        self.marioRect = pygame.Rect(10, 160, 16, 16)

        self.walkSpeed = 128  # 走路速度,每秒128像素

 

        # 走路动画图片

        self.frames = [self.marioImageCache.marioWalk1, self.marioImageCache.marioWalk2,

                       self.marioImageCache.marioWalk3]

        self.frameDuration = 100  # 切换一次帧的时间间隔,单位为毫秒

        self.frameIndex = 0  # 当前帧的下标

        self.image = self.marioImageCache.marioWalk1

 

        # 时间差累计

        self.elapsedTime = 0

 

        # 字体

        self.font = pygame.font.SysFont("Arial", 24)

    def run(self):

 

        # 主循环

        isRunning = True

        while isRunning:

            # 获取当前时间差(毫秒)

            deltaTime = self.clock.get_time()

            # 时间差累计

            self.elapsedTime += deltaTime

 

            for event in pygame.event.get():

                if event.type == pygame.QUIT:

                    isRunning = False

                elif event.type == pygame.KEYDOWN:

                    if event.key == pygame.K_KP_PLUS:

                        self.walkSpeed += 1

                    elif event.key == pygame.K_KP_MINUS:

                        self.walkSpeed -= 1

                        if self.walkSpeed <= 0:

                            self.walkSpeed = 1

                    elif event.key == pygame.K_KP_MULTIPLY:

                        self.frameDuration += 10

                    elif event.key == pygame.K_KP_DIVIDE:

                        self.frameDuration -= 10

                        if self.frameDuration <= 0:

                            self.frameDuration = 10

 

            # 全屏擦除(注意背景色改成了天蓝色)

            self.screen.fill((135, 206, 255))

 

            # 移动距离

            distance = self.walkSpeed * deltaTime / 1000.0

 

            # 获取按键状态

            keys = pygame.key.get_pressed()

            if keys[pygame.K_RIGHT]:

                self.marioRect = self.marioRect.move(distance, 0)

            elif keys[pygame.K_LEFT]:

                self.marioRect = self.marioRect.move(distance * -1, 0)

 

            # 切换动画图片

            if self.elapsedTime > self.frameDuration:

                self.elapsedTime -= self.frameDuration

                self.frameIndex += 1

                if self.frameIndex >= len(self.frames):

                    self.frameIndex = 0

                self.image = self.frames[self.frameIndex]

 

            # 显示马里奥

            self.screen.blit(self.image, self.marioRect)

 

            # 显示当前步伐、帧切换时间

            txtSurface = self.font.render("walkSpeed=" + str(self.walkSpeed), True, (255, 255, 255))

            self.screen.blit(txtSurface, (40, 40))

            txtSurface = self.font.render("deltaTime=" + str(deltaTime), True, (255, 255, 255))

            self.screen.blit(txtSurface, (40, 60))

            txtSurface = self.font.render("distance=" + str(distance), True, (255, 255, 255))

            self.screen.blit(txtSurface, (40, 80))

            txtSurface = self.font.render("frameDuration=" + str(self.frameDuration), True, (255, 255, 255))

            self.screen.blit(txtSurface, (40, 100))

 

            # 显示FPS

            fps = int(self.clock.get_fps())

            txtSurface = self.font.render("FPS=" + str(fps), True, (255, 255, 255))

            self.screen.blit(txtSurface, (0, 0))

 

            # 放大画面,显示在屏幕中间

            newSurface = pygame.transform.scale(self.screen, self.windowSize)

            showPosX = (self.window.get_rect().size[0] - self.windowSize[0]) / 2

            showPosY = (self.window.get_rect().size[1] - self.windowSize[1]) / 2

            self.window.blit(newSurface, (showPosX, showPosY))

 

            # 刷新显示

            pygame.display.flip()

 

            # 每秒60

            self.clock.tick(60)

 

        # 退出 Pygame

        pygame.quit()

 

 

if __name__ == '__main__':

    game = Game()

    game.run()

小键盘的加号和减号可以增减马里奥的移动速度,乘号和除号可以增减动画帧的切换间隔,控制马里奥向右移动,挑选效果最好的组合即可。

画面中输出了当前主要变量的值,使用了Font对象的render()方法,将文字转换为Surface对象,并显示在屏幕上。render方法的格式如下:

render(text, antialias, color, background=None)

l  text参数:表示文字内容。

l  antialias参数:表示文字边缘是否抗锯齿。

l  color参数:表示文字颜色。

l  background参数:表示文字背景色,可选。

画面左上角还输出了游戏的FPS值,使用clock对象的get_fps()方法即可。运行程序,画面如6‑10所示。

610移动速度和切换速度的配合

经过多次尝试后,我们发现walkSpeed=130frameDuration=80的组合效果比较理想