【Pygame】第9章 动画系统与帧动画
摘要动画为游戏注入了生命力是游戏体验不可或缺的组成部分。本章将介绍 Pygame 中动画的基本实现方式包括帧动画、精灵表、补间动画和状态机管理。我们将学习如何创建流畅、高效的角色动画系统掌握动画的播放控制和过渡技巧。通过本章的学习读者将能够为游戏角色和物体创建生动有趣的动画效果。9.1 动画基础动画本质上是通过快速连续显示一系列静态图像制造运动错觉。9.1.1 动画原理动画的核心概念包括帧率每秒显示的帧数常见为 12 到 30 FPS帧时间每帧持续的时间帧率越高帧时间越短关键帧动画中的重要姿态中间帧可以由程序插值生成9.1.2 动画类型类型特点适用场景帧动画由多张图片组成角色动作、特效补间动画由程序计算中间状态UI 动画、移动效果骨骼动画基于骨骼驱动复杂角色动画粒子动画由大量粒子组成火焰、烟雾、爆炸9.2 帧动画实现帧动画是最常见、最容易理解的动画方式。它的基本思路是准备多张连续帧图片然后按固定时间间隔切换显示。9.2.1 基础帧动画下面示例使用不同颜色的圆形来模拟动画帧importpygameimportsys pygame.init()screenpygame.display.set_mode((800,600))clockpygame.time.Clock()classAnimatedSprite(pygame.sprite.Sprite):def__init__(self,x,y):super().__init__()self.frames[]colors[(255,0,0),(255,128,0),(255,255,0),(0,255,0),(0,255,255),(0,0,255),(255,0,255)]forcolorincolors:framepygame.Surface((64,64),pygame.SRCALPHA)pygame.draw.circle(frame,color,(32,32),30)self.frames.append(frame)self.current_frame0self.animation_speed100self.last_updatepygame.time.get_ticks()self.imageself.frames[0]self.rectself.image.get_rect(center(x,y))defupdate(self):nowpygame.time.get_ticks()ifnow-self.last_updateself.animation_speed:self.last_updatenow self.current_frame(self.current_frame1)%len(self.frames)self.imageself.frames[self.current_frame]all_spritespygame.sprite.Group()spriteAnimatedSprite(400,300)all_sprites.add(sprite)runningTruewhilerunning:foreventinpygame.event.get():ifevent.typepygame.QUIT:runningFalseall_sprites.update()screen.fill((50,50,50))all_sprites.draw(screen)pygame.display.flip()clock.tick(60)pygame.quit()sys.exit()说明frames保存所有帧current_frame表示当前帧索引animation_speed控制切换速度单位是毫秒pygame.time.get_ticks()返回程序启动后的毫秒数9.2.2 帧动画的核心逻辑帧动画的关键点就是“定时切换”。nowpygame.time.get_ticks()ifnow-last_updateanimation_speed:last_updatenow current_frame(current_frame1)%len(frames)解释当当前时间减去上次更新时间达到设定间隔时切换到下一帧使用取模%可以实现循环播放9.3 从文件加载帧动画在实际项目中动画帧通常来自图片文件而不是程序绘制。9.3.1 文件帧加载思路通常会把一组图片按固定命名规则保存比如frame_00.pngframe_01.pngframe_02.png然后按顺序读取。importpygameimportosdefload_frames(folder,count):frames[]foriinrange(count):filepathf{folder}/frame_{i:02d}.pngifos.path.exists(filepath):framepygame.image.load(filepath).convert_alpha()frames.append(frame)returnframes9.3.2 动画播放控制classAnimationPlayer:def__init__(self,animations):self.animationsanimations self.current_animationidleself.current_frame0self.animation_speed100self.last_updatepygame.time.get_ticks()self.loopTrueself.playingTruedefplay(self,name,loopTrue):ifnameinself.animations:self.current_animationname self.current_frame0self.looploop self.playingTrueself.last_updatepygame.time.get_ticks()defupdate(self):ifnotself.playing:returnnowpygame.time.get_ticks()ifnow-self.last_updateself.animation_speed:self.last_updatenow framesself.animations[self.current_animation]self.current_frame1ifself.current_framelen(frames):ifself.loop:self.current_frame0else:self.current_framelen(frames)-1self.playingFalse说明play用于切换动画loop决定是否循环非循环动画播放完后停在最后一帧9.4 精灵表处理精灵表是把多帧动画合并到一张大图中这样可以减少文件数量和加载开销。9.4.1 精灵表裁剪importpygameclassSpriteSheet:def__init__(self,filepath):self.sheetpygame.image.load(filepath).convert_alpha()defget_image(self,x,y,width,height):imagepygame.Surface((width,height),pygame.SRCALPHA)image.blit(self.sheet,(0,0),(x,y,width,height))returnimage9.4.2 连续帧提取defget_frames(self,start_x,start_y,width,height,count,directionhorizontal):frames[]foriinrange(count):ifdirectionhorizontal:xstart_xi*width ystart_yelse:xstart_x ystart_yi*height frames.append(self.get_image(x,y,width,height))returnframes说明横向排列帧按水平方向排列纵向排列帧按竖直方向排列适合多数 2D 角色动画资源9.5 补间动画补间动画是通过计算起点和终点之间的中间值实现平滑过渡。9.5.1 线性插值deflerp(start,end,t):returnstart(end-start)*t说明start起始值end目标值t进度范围 0 到 19.5.2 缓动函数defease_in_out(t):returnt*t*(3-2*t)defease_out_bounce(t):ift1/2.75:return7.5625*t*telift2/2.75:t-1.5/2.75return7.5625*t*t0.75elift2.5/2.75:t-2.25/2.75return7.5625*t*t0.9375else:t-2.625/2.75return7.5625*t*t0.9843759.5.3 Tween 类classTween:def__init__(self,start_value,end_value,duration,ease_funcNone):self.start_valuestart_value self.end_valueend_value self.durationduration self.ease_funcease_funcor(lambdat:t)self.start_timepygame.time.get_ticks()self.finishedFalsedefupdate(self):elapsedpygame.time.get_ticks()-self.start_time tmin(elapsed/self.duration,1.0)ift1.0:self.finishedTruereturnlerp(self.start_value,self.end_value,self.ease_func(t))说明Tween 常用于 UI 位移、缩放、透明度变化适合做按钮动画、弹出动画、提示框动画9.6 动画状态机动画状态机用于管理角色在不同状态下的动画切换例如待机、行走、攻击。9.6.1 状态机思路classAnimationStateMachine:def__init__(self):self.states{}self.transitions{}self.current_stateNonedefadd_state(self,name,animation):self.states[name]animationdefadd_transition(self,from_state,to_state,condition):iffrom_statenotinself.transitions:self.transitions[from_state][]self.transitions[from_state].append((to_state,condition))defset_state(self,name):ifnameinself.states:self.current_statename self.states[name].reset()9.6.2 更新逻辑defupdate(self,context):ifself.current_stateinself.transitions:forto_state,conditioninself.transitions[self.current_state]:ifcondition(context):self.set_state(to_state)breakifself.current_state:self.states[self.current_state].update()说明context是外部状态信息例如是否移动、是否攻击条件满足时自动切换状态9.7 中文字体安全加载方案在某些环境中pygame.font.SysFont可能触发系统字体扫描错误因此本书建议统一使用字体文件路径加载。9.7.1 推荐写法importpygameimportosdefget_font(size):font_paths[rC:\Windows\Fonts\simhei.ttf,rC:\Windows\Fonts\msyh.ttc,rC:\Windows\Fonts\simsun.ttc,]forpathinfont_paths:ifos.path.exists(path):try:returnpygame.font.Font(path,size)except:passreturnpygame.font.Font(None,size)说明这种写法避免了SysFont引发的系统字体枚举问题适合本书所有涉及中文显示的示例9.8 综合示例角色动画系统下面给出本章完整的综合示例包含帧动画状态机行走和待机切换中文字体安全加载importpygameimportsysimportos pygame.init()screenpygame.display.set_mode((800,600))pygame.display.set_caption(角色动画系统演示)clockpygame.time.Clock()defget_font(size):font_paths[rC:\Windows\Fonts\simhei.ttf,rC:\Windows\Fonts\msyh.ttc,rC:\Windows\Fonts\simsun.ttc,]forpathinfont_paths:ifos.path.exists(path):try:returnpygame.font.Font(path,size)except:passreturnpygame.font.Font(None,size)fontget_font(24)classFrameAnimation:def__init__(self,frames,speed100,loopTrue):self.framesframes self.speedspeed self.looploop self.current_frame0self.last_updatepygame.time.get_ticks()self.finishedFalsedefreset(self):self.current_frame0self.finishedFalseself.last_updatepygame.time.get_ticks()defupdate(self):ifself.finishedorlen(self.frames)0:returnnowpygame.time.get_ticks()ifnow-self.last_updateself.speed:self.last_updatenow self.current_frame1ifself.current_framelen(self.frames):ifself.loop:self.current_frame0else:self.current_framelen(self.frames)-1self.finishedTruedefget_image(self):iflen(self.frames)0:returnNonereturnself.frames[self.current_frame]classAnimationStateMachine:def__init__(self):self.states{}self.transitions{}self.current_stateNonedefadd_state(self,name,animation):self.states[name]animationdefadd_transition(self,from_state,to_state,condition):iffrom_statenotinself.transitions:self.transitions[from_state][]self.transitions[from_state].append((to_state,condition))defset_state(self,name):ifnameinself.statesandname!self.current_state:self.current_statename self.states[name].reset()defupdate(self,context):ifself.current_stateinself.transitions:forto_state,conditioninself.transitions[self.current_state]:ifcondition(context):self.set_state(to_state)breakifself.current_state:self.states[self.current_state].update()defget_image(self):ifself.current_state:returnself.states[self.current_state].get_image()returnNone# 生成示例帧idle_frames[]walk_frames[]foriinrange(4):framepygame.Surface((64,64),pygame.SRCALPHA)pygame.draw.rect(frame,(100i*20,150,220),(18,16,28,40))pygame.draw.circle(frame,(255,220,180),(32,16),10)idle_frames.append(frame)foriinrange(6):framepygame.Surface((64,64),pygame.SRCALPHA)offset8ifi%20else-8pygame.draw.rect(frame,(80,220,120),(18offset,16,28,40))pygame.draw.circle(frame,(255,220,180),(32offset,16),10)walk_frames.append(frame)asmAnimationStateMachine()asm.add_state(idle,FrameAnimation(idle_frames,200))asm.add_state(walk,FrameAnimation(walk_frames,100))asm.add_transition(idle,walk,lambdactx:ctx[moving])asm.add_transition(walk,idle,lambdactx:notctx[moving])asm.set_state(idle)player_rectpygame.Rect(368,268,64,64)player_speed5runningTruewhilerunning:movingFalseforeventinpygame.event.get():ifevent.typepygame.QUIT:runningFalsekeyspygame.key.get_pressed()ifkeys[pygame.K_LEFT]:player_rect.x-player_speed movingTrueifkeys[pygame.K_RIGHT]:player_rect.xplayer_speed movingTrueifkeys[pygame.K_UP]:player_rect.y-player_speed movingTrueifkeys[pygame.K_DOWN]:player_rect.yplayer_speed movingTrueasm.update({moving:moving})screen.fill((40,40,40))imageasm.get_image()ifimage:screen.blit(image,player_rect)info1font.render(方向键移动角色,True,(255,255,255))info2font.render(f当前状态:{asm.current_state},True,(255,220,80))screen.blit(info1,(10,10))screen.blit(info2,(10,40))pygame.display.flip()clock.tick(60)pygame.quit()sys.exit()9.9 本章总结本章介绍了 Pygame 中动画系统的基本实现方式。我们学习了帧动画、精灵表、补间动画和动画状态机的构建方法。动画系统是提升游戏表现力的重要组成部分合理地组织动画逻辑可以让角色行为更加自然流畅。本章知识点回顾知识点主要内容帧动画图像序列、定时切换精灵表合并帧、裁剪技术补间动画插值算法、缓动函数状态机动画状态管理、转换条件中文字体字体文件路径加载方案课后练习实现一个精灵表解析器支持多行多列裁剪。创建一个粒子动画系统。实现动画暂停、继续和反向播放功能。扩展状态机加入攻击和受击动画。实现一个角色跳跃动画系统。下章预告在下一章中我们将学习游戏状态管理和场景切换这是组织复杂游戏逻辑的重要基础。