上例中马里奥只有走路一种状态,仅作为演示,下面尝试将走路动画集成到游戏中。
考虑到马里奥拥有多种状态,每种状态对应不同的图片,且朝向左右的图片也会有所不同,因此,我们需要单独创建一个类来存储不同状态对应的图片信息。
现在的时间差累计值,实际仅用于马里奥的走路状态,站立和跳跃状态并不需要该值,因为它们仅包含单张图片。但考虑到未来肯定会加入更多的状态,比如游泳、爬藤蔓等等,它们都包含多帧动画,并需要根据时间进行帧切换。因此,为每种多帧动画状态保存时间差累计值、帧切换间隔、帧下标等变量是必要的。
此外,当前状态切换过程较为复杂,按键处理需考虑多种状态,导致代码混乱不清晰。
综合以上几点,考虑编写一个马里奥状态的父类,封装动画帧处理的逻辑,并实现多个子类,分别对应不同状态,每个子类负责处理按键和状态切换等复杂逻辑,而Mario类的主要功能就是负责状态管理。
修改后Mario类的代码如下所示:
06\04\Mario.py
|
import pygame from pygame import Rect from utils.utils import * from game_globals.Globals import Globals from mario.state.StandState import *
# 马里奥 class Mario(object):
# 构造方法,记录初始坐标 def __init__(self, x, y, screen): self.screen = screen self.rect = Rect(x, y, MAP_BLOCK_SIZE, MAP_BLOCK_SIZE) self.walkStepDefault = 2 # 走路步伐,默认值 self.jumpStepDefault = 10 # 跳跃步伐,默认值 self.dropStepDefault = 4 # 掉落步伐,默认值 self.walkStep = 2 # 走路步伐,实际值 self.jumpStep = 10 # 跳跃步伐,实际值 self.dropStep = 4 # 掉落步伐,实际值
self.walkSpeed = 128 # 走路速度,每秒128像素 self.direction = DIRECTION_RIGHT # 运动方向,右是1,左 self.image = Globals.marioImageCache.marioStand # 站立图片 self.currentState = StandState(self) # 站立状态
# 更新 def update(self, keys, deltaTime):
# 步伐,先恢复默认值 self.walkStep = self.walkStepDefault self.jumpStep = self.jumpStepDefault self.dropStep = self.dropStepDefault
# 由当前状态类,处理按键,并返回新状态类(如果状态变化的话) newState = self.currentState.update(keys, deltaTime) print("state=" + newState.getStateName())
# 状态没变,则施加重力 # 状态变化,先不管重力,下一帧再施加重力。 # 否则站立变跳跃时,跳不起来(因为新状态是跳跃,这里直接检测到碰撞,又变成了站立)。 if newState == self.currentState: # ----重力影响---- # 下一步预判断 nextStepRect = self.rect.move(0, self.dropStep) pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray) if pipeIndex != -1: self.dropStep = mapToScreen(Globals.game.objectArray[pipeIndex]).y - self.rect.bottom
# 下边界碰撞,通知状态类 self.currentState = self.currentState.updateGravity(True) self.rect = self.rect.move(0, self.dropStep) else: self.currentState = newState
# 显示马里奥 self.screen.blit(self.image, self.rect)
# 滚动地图 if self.currentState.getScrollMap(): Globals.game.scrollMap(self.walkStep) |
代码中使用currentState变量保存当前状态对象,由当前状态的update方法进行主要逻辑处理。如果需要切换状态,则update()方法会返回新状态,保存到currentState变量即可。
多个状态类的文件结构如图6‑11所示。

图6‑11状态类的文件结构
其中,MarioState类是父类,其他三个子类分别对应站立、走路和跳跃三种状态。
MarioState类的代码如下所示:
06\04\mario\state\MarioState.py
|
import abc
# 马里奥状态,父类 class MarioState(abc.ABC): # 初始化 def __init__(self, mario, frameDuration): self.mario = mario # 马里奥 self.frames = [] # 动画图片 self.frameDuration = frameDuration # 每帧显示时间,单位为毫秒 self.frameIndex = 0 # 当前帧的下标 self.elapsedTime = 0 # 时间差累计 self.isScrollMap = False # 是否滚动地图
# 设置动画帧 self._setFrames(mario.direction)
# 设置动画帧(运行时动态切换方向) def _setFrames(self, direction): print("----setFrames:"+str(direction)) self.frames = self._getFrames(direction) self.frameIndex = 0 # 当前帧的下标 self.elapsedTime = 0 # 时间差累计
# 切换后,显示第一帧 self.mario.image = self.frames[0]
# 获取动画帧(子状态负责实现) @abc.abstractmethod def _getFrames(self, direction): pass
# 更新动画,处理按键 def update(self, keys, deltaTime):
# 只有一帧,无需累积时间差 if len(self.frames) == 1: self.mario.image = self.frames[0] else: # 时间差累计 self.elapsedTime += deltaTime
# 切换动画图片 if self.elapsedTime > self.frameDuration: self.elapsedTime -= self.frameDuration # 减去差值 self.frameIndex += 1 if self.frameIndex >= len(self.frames): self.frameIndex = 0 self.mario.image = self.frames[self.frameIndex]
# 处理按键 return self._updateKeys(self.mario, keys, deltaTime)
# 处理按键(子状态负责实现) @abc.abstractmethod def _updateKeys(self, mario, keys, deltaTime): pass
# 获取状态名称(子状态负责实现) @abc.abstractmethod def getStateName(self): pass
# 重力处理,下边界碰撞(子状态负责实现) @abc.abstractmethod def updateGravity(self, isHitBottom): pass
# 是否滚动地图 def getScrollMap(self): return self.isScrollMap |
父类封装了动画帧相关的变量和处理逻辑,并提供了一些方法,具体如下:
l _setFrames()方法:设置动画帧内容,用来切换动画帧,主要就是马里奥变更方向时调用。
l _getFrames()抽象方法:由每个子类具体实现,获取动画帧内容。
l update()方法:封装了动画帧切换的逻辑,最后会调用_updateKeys()方法处理按键。
l _updateKeys()抽象方法:由每个子类具体实现,处理按键。
l getStateName()抽象方法:由每个子类具体实现,返回状态名称。
l updateGravity()抽象方法:由每个子类具体实现,马里奥由于重力下边界碰撞时会调用该方法。
l getScrollMap()方法:获取是否滚动地图。
StandState类的代码如下所示:
06\04\mairo\state\StandState.py
|
import pygame from mario.state.JumpState import JumpState from mario.state.MarioState import MarioState from utils.utils import * from mario.state.WalkState import WalkState
# 站立状态 class StandState(MarioState): # 初始化 def __init__(self, mario): super().__init__(mario, 80)
# 设置动画帧 def _getFrames(self, direction): cache = Globals.marioImageCache if direction == DIRECTION_RIGHT: return [cache.marioStand] else: return [cache.marioLeftStand]
# 获取状态名称 def getStateName(self): return "stand"
# 处理按键(子类必须实现这个方法) def _updateKeys(self, mario, keys, deltaTime): # 按键状态 if keys[pygame.K_LEFT]: print("按了左方向键") mario.direction = DIRECTION_LEFT return WalkState(mario) elif keys[pygame.K_RIGHT]: print("按了右方向键") mario.direction = DIRECTION_RIGHT return WalkState(mario)
if keys[pygame.K_g]: # 原地跳跃,原来脸朝向哪边就是哪边 return JumpState(mario)
# 没按方向键和跳跃键,则保持站立状态 return self
# 重力处理,下边界碰撞 def updateGravity(self, isHitBottom): return self
|
代码比较简单,主要功能如下:
l 实现_getFrames()方法,根据方向设置不同的图片。
l 实现_updateKeys()方法,根据按键返回不同的状态子类。
l 实现updateGravity()方法,直接返回自身。因为站立状态时,下边界肯定是碰撞状态,继续保持站立状态即可,无需变更。
WalkState类代码如下所示:
06\04\mario\state\WalkState.py
|
import pygame from mario.state.MarioState import MarioState from mario.state.JumpState import JumpState
from utils.utils import *
# 走路状态 class WalkState(MarioState): # 初始化 def __init__(self, mario): super().__init__(mario, 80)
# 设置动画帧 def _getFrames(self, direction): cache = Globals.marioImageCache if direction == DIRECTION_RIGHT: return [cache.marioWalk1, cache.marioWalk2, cache.marioWalk3] else: return [cache.marioLeftWalk1, cache.marioLeftWalk2, cache.marioLeftWalk3]
# 获取状态名称 def getStateName(self): return "walk"
# 处理按键(子类必须实现这个方法) def _updateKeys(self, mario, keys, deltaTime):
# 是否滚动地图 self.isScrollMap = False
# 移动距离 mario.walkStep = mario.walkSpeed * deltaTime / 1000.0
# 按键状态 if keys[pygame.K_LEFT]: print("按了左方向键")
# 之前马里奥向右走,现在变成向左走,切换动画帧 if mario.direction == DIRECTION_RIGHT: mario.direction = DIRECTION_LEFT self._setFrames(DIRECTION_LEFT)
# 窗口边界判断 if mario.rect.x - mario.walkStep <= 0: mario.walkStep = mario.rect.x
# 下一步预判断 nextStepRect = mario.rect.move(-1 * mario.walkStep, 0) pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray) # 是否碰撞 if pipeIndex != -1: mario.walkStep = mario.rect.x - mapToScreen(Globals.game.objectArray[pipeIndex]).right mario.rect = mario.rect.move(-1 * mario.walkStep, 0) elif keys[pygame.K_RIGHT]: print("按了右方向键")
# 之前马里奥向左走,现在变成向右走,切换动画帧 if mario.direction == DIRECTION_LEFT: mario.direction = DIRECTION_RIGHT self._setFrames(DIRECTION_RIGHT)
# 超过屏幕中线才滚动地图,地图末尾右半屏也不滚动 if mario.rect.x >= SCREEN_WIDTH / 2 and screenToMap( mario.rect).x < Globals.game.mapViewFromMax + SCREEN_WIDTH / 2:
# 如果地图滚动后,有碰撞发生,则不能滚动 # 地图向左滚动,相当于马里奥向右移动。 # 这里不能修改mapViewFrom的值 nextStepRect = mario.rect.move(mario.walkStep, 0) pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray) if pipeIndex == -1: self.isScrollMap = True else: # 窗口边界判断 if mario.rect.right + mario.walkStep >= SCREEN_WIDTH: mario.walkStep = SCREEN_WIDTH - mario.rect.right
# 下一步预判断 nextStepRect = mario.rect.move(mario.walkStep, 0) pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray) if pipeIndex != -1: mario.walkStep = mapToScreen(Globals.game.objectArray[pipeIndex]).x - mario.rect.right mario.rect = mario.rect.move(mario.walkStep, 0)
# (1)如果跳跃,则返回JumpState # (2)只走路,不跳跃,返回WalkState # (3)没走路,没跳跃,返回StandState if keys[pygame.K_g]: print("按了跳跃键") return JumpState(mario) elif keys[pygame.K_LEFT] or keys[pygame.K_RIGHT]: return self else: # 没有走路,没有跳跃,则变成站立 from mario.state.StandState import StandState return StandState(mario)
# 重力处理,下边界碰撞 def updateGravity(self, isHitBottom): return self |
代码实现类似,只是将走路状态的处理进行了封装。当马里奥改变方向时,只需重新设置动画帧即可。如果没有走路也没有跳跃,则返回站立状态子类。
由于走路状态和站立状态二者之间存在交叉引用,因此将"import StandState"语句放在使用之前,而不是放在Python文件的顶部,避免交叉引用导致的错误。
JumpState类的代码如下所示:
06\04\mario\state\JumpState.py
|
import pygame from mario.state.MarioState import MarioState from utils.utils import *
# 跳跃状态 class JumpState(MarioState): # 初始化 def __init__(self, mario): super().__init__(mario, 80)
# 设置动画帧 def _getFrames(self, direction): cache = Globals.marioImageCache if direction == DIRECTION_RIGHT: return [cache.marioJump] else: return [cache.marioLeftJump]
# 获取状态名称 def getStateName(self): return "jump"
# 处理按键(子类必须实现这个方法) def _updateKeys(self, mario, keys, deltaTime):
# 是否滚动地图 self.isScrollMap = False
# 移动距离 mario.walkStep = mario.walkSpeed * deltaTime / 1000.0
# 按键状态 if keys[pygame.K_LEFT]: print("按了左方向键")
# 之前马里奥向右走,现在变成向左走,切换动画帧 if mario.direction == DIRECTION_RIGHT: mario.direction = DIRECTION_LEFT self._setFrames(DIRECTION_LEFT)
# 窗口边界判断 if mario.rect.x - mario.walkStep <= 0: mario.walkStep = mario.rect.x
# 下一步预判断 nextStepRect = mario.rect.move(-1 * mario.walkStep, 0) pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray) # 是否碰撞 if pipeIndex != -1: mario.walkStep = mario.rect.x - mapToScreen(Globals.game.objectArray[pipeIndex]).right mario.rect = mario.rect.move(-1 * mario.walkStep, 0) elif keys[pygame.K_RIGHT]: print("按了右方向键")
# 之前马里奥向左走,现在变成向右走,切换动画帧 if mario.direction == DIRECTION_LEFT: mario.direction = DIRECTION_RIGHT self._setFrames(DIRECTION_RIGHT)
# 超过屏幕中线才滚动地图,地图末尾右半屏也不滚动 if mario.rect.x >= SCREEN_WIDTH / 2 and screenToMap( mario.rect).x < Globals.game.mapViewFromMax + SCREEN_WIDTH / 2:
# 如果地图滚动后,有碰撞发生,则不能滚动 # 地图向左滚动,相当于马里奥向右移动。 # 这里不能修改mapViewFrom的值 nextStepRect = mario.rect.move(mario.walkStep, 0) pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray) if pipeIndex == -1: self.isScrollMap = True else: # 窗口边界判断 if mario.rect.right + mario.walkStep >= SCREEN_WIDTH: mario.walkStep = SCREEN_WIDTH - mario.rect.right
# 下一步预判断 nextStepRect = mario.rect.move(mario.walkStep, 0) pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray) if pipeIndex != -1: mario.walkStep = mapToScreen(Globals.game.objectArray[pipeIndex]).x - mario.rect.right mario.rect = mario.rect.move(mario.walkStep, 0)
if keys[pygame.K_g]: print("按了跳跃键")
# 窗口边界判断 if mario.rect.y - mario.jumpStep <= 0: mario.jumpStep = mario.rect.y
# 下一步预判断 nextStepRect = mario.rect.move(0, -1 * mario.jumpStep) pipeIndex = screenToMap(nextStepRect).collidelist(Globals.game.objectArray) if pipeIndex != -1: mario.jumpStep = (mapToScreen(Globals.game.objectArray[pipeIndex]).bottom - mario.rect.y) * -1 mario.rect = mario.rect.move(0, -1 * mario.jumpStep)
# 没有走路,没有跳跃,则是空中掉落状态,暂不处理 if not (keys[pygame.K_LEFT] or keys[pygame.K_RIGHT] or keys[pygame.K_g]): pass
return self
# 重力处理,下边界碰撞 def updateGravity(self, isHitBottom): # 掉落碰到物体,则转为站立状态 if isHitBottom: from mario.state.StandState import StandState return StandState(self.mario) return self |
代码与走路状态类似。目前,跳跃处理与原作仍有一些差异,比如空中可以进行二次跳跃、可以改变方向、松开按键后直线掉落等,有待后续进一步优化。尽管还存在一些不完美之处,但整体效果已相当不错。