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

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

图6‑8马里奥走路的动画
从图可以看出,马里奥在移动时会不断切换图片。需要注意的是,这三张图片并不是连续的三帧,每个动作持续几帧才会切换到下一个动作的图片。只有当切换速度与移动速度相配合时,才能呈现出流畅连贯的走路效果。如果切换速度过快,会出现马里奥原地挥手的情况。如果切换速度过慢,则看起来像在慢动作滑行。
那么,究竟应该多久切换一次图片呢?答案是,通过实践来探索,运行程序并不断调整切换速度,直到达到满意的效果为止。
在我们的游戏中,每秒进行60帧的渲染。如果按照每帧显示不同的图片(如图1、图2、图3、图1循环),那将导致每秒图片切换60次,这样的速度太快了,需要限制一下切换速度。
一种控制切换速度的方法是计算帧数,比如设定每20帧切换一次图片。我们可以使用一个计数变量,每帧加1,当计数达到20时切换图片,然后将计数清零,如此循环。另一种方法是计算时间差,比如设定20毫秒切换一次图片。我们可以使用一个变量来保存上次切换图片的时间,如果当前时间距离上次切换时间超过了20毫秒,就切换图片并更新变量为当前时间,如此循环。
虽然计算帧数的方法比较简单,但存在一个问题,如果以后修改帧数为100,而切换频率仍然是每20帧一次,那么马里奥的动作切换速度就会变快。这就好比在播放查理·卓别林的黑白电影时,原本是以每秒16-18帧的速度拍摄的,如今以每秒24帧的速度播放,人物动作看起来就会显得快速。鉴于此,我们决定采用计算时间差的方法,无论帧数如何修改,都不影响动作切换的速度。
获取时间差有个简单方法,可以使用Clock对象的get_time()方法,它返回上两次调用tick()方法的时间差,单位是毫秒,如图6‑9所示。从图可以看出,get_time()方法实际获取的就是上一帧的完整时间。

图6‑9获取时间差
假设马里奥每秒切换一次动作,那么一帧的时间肯定不够,因此需要等待多帧,累计时间差,直到累计值超过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所示。

图6‑10移动速度和切换速度的配合
经过多次尝试后,我们发现walkSpeed=130和frameDuration=80的组合效果比较理想