【Pygame】第9章 动画系统与帧动画

摘要

动画为游戏注入了生命力,是游戏体验不可或缺的组成部分。本章将介绍 Pygame 中动画的基本实现方式,包括帧动画、精灵表、补间动画和状态机管理。我们将学习如何创建流畅、高效的角色动画系统,掌握动画的播放控制和过渡技巧。通过本章的学习,读者将能够为游戏角色和物体创建生动有趣的动画效果。


9.1 动画基础

动画本质上是通过快速连续显示一系列静态图像,制造运动错觉。

9.1.1 动画原理

动画的核心概念包括:

  • 帧率:每秒显示的帧数,常见为 12 到 30 FPS
  • 帧时间:每帧持续的时间,帧率越高,帧时间越短
  • 关键帧:动画中的重要姿态,中间帧可以由程序插值生成

9.1.2 动画类型

类型 特点 适用场景
帧动画 由多张图片组成 角色动作、特效
补间动画 由程序计算中间状态 UI 动画、移动效果
骨骼动画 基于骨骼驱动 复杂角色动画
粒子动画 由大量粒子组成 火焰、烟雾、爆炸

9.2 帧动画实现

帧动画是最常见、最容易理解的动画方式。它的基本思路是:

准备多张连续帧图片,然后按固定时间间隔切换显示。

9.2.1 基础帧动画

下面示例使用不同颜色的圆形来模拟动画帧:

python 复制代码
import pygame
import sys

pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()

class AnimatedSprite(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)
        ]

        for color in colors:
            frame = pygame.Surface((64, 64), pygame.SRCALPHA)
            pygame.draw.circle(frame, color, (32, 32), 30)
            self.frames.append(frame)

        self.current_frame = 0
        self.animation_speed = 100
        self.last_update = pygame.time.get_ticks()

        self.image = self.frames[0]
        self.rect = self.image.get_rect(center=(x, y))

    def update(self):
        now = pygame.time.get_ticks()
        if now - self.last_update >= self.animation_speed:
            self.last_update = now
            self.current_frame = (self.current_frame + 1) % len(self.frames)
            self.image = self.frames[self.current_frame]

all_sprites = pygame.sprite.Group()
sprite = AnimatedSprite(400, 300)
all_sprites.add(sprite)

running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    all_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 帧动画的核心逻辑

帧动画的关键点就是"定时切换"。

python 复制代码
now = pygame.time.get_ticks()
if now - last_update >= animation_speed:
    last_update = now
    current_frame = (current_frame + 1) % len(frames)

解释

  • 当当前时间减去上次更新时间达到设定间隔时
  • 切换到下一帧
  • 使用取模 % 可以实现循环播放

9.3 从文件加载帧动画

在实际项目中,动画帧通常来自图片文件,而不是程序绘制。

9.3.1 文件帧加载思路

通常会把一组图片按固定命名规则保存,比如:

  • frame_00.png
  • frame_01.png
  • frame_02.png

然后按顺序读取。

python 复制代码
import pygame
import os

def load_frames(folder, count):
    frames = []
    for i in range(count):
        filepath = f"{folder}/frame_{i:02d}.png"
        if os.path.exists(filepath):
            frame = pygame.image.load(filepath).convert_alpha()
            frames.append(frame)
    return frames

9.3.2 动画播放控制

python 复制代码
class AnimationPlayer:
    def __init__(self, animations):
        self.animations = animations
        self.current_animation = "idle"
        self.current_frame = 0
        self.animation_speed = 100
        self.last_update = pygame.time.get_ticks()
        self.loop = True
        self.playing = True

    def play(self, name, loop=True):
        if name in self.animations:
            self.current_animation = name
            self.current_frame = 0
            self.loop = loop
            self.playing = True
            self.last_update = pygame.time.get_ticks()

    def update(self):
        if not self.playing:
            return

        now = pygame.time.get_ticks()
        if now - self.last_update >= self.animation_speed:
            self.last_update = now
            frames = self.animations[self.current_animation]
            self.current_frame += 1

            if self.current_frame >= len(frames):
                if self.loop:
                    self.current_frame = 0
                else:
                    self.current_frame = len(frames) - 1
                    self.playing = False

说明

  • play 用于切换动画
  • loop 决定是否循环
  • 非循环动画播放完后停在最后一帧

9.4 精灵表处理

精灵表是把多帧动画合并到一张大图中,这样可以减少文件数量和加载开销。

9.4.1 精灵表裁剪

python 复制代码
import pygame

class SpriteSheet:
    def __init__(self, filepath):
        self.sheet = pygame.image.load(filepath).convert_alpha()

    def get_image(self, x, y, width, height):
        image = pygame.Surface((width, height), pygame.SRCALPHA)
        image.blit(self.sheet, (0, 0), (x, y, width, height))
        return image

9.4.2 连续帧提取

python 复制代码
def get_frames(self, start_x, start_y, width, height, count, direction="horizontal"):
    frames = []
    for i in range(count):
        if direction == "horizontal":
            x = start_x + i * width
            y = start_y
        else:
            x = start_x
            y = start_y + i * height
        frames.append(self.get_image(x, y, width, height))
    return frames

说明

  • 横向排列:帧按水平方向排列
  • 纵向排列:帧按竖直方向排列
  • 适合多数 2D 角色动画资源

9.5 补间动画

补间动画是通过计算起点和终点之间的中间值实现平滑过渡。

9.5.1 线性插值

python 复制代码
def lerp(start, end, t):
    return start + (end - start) * t

说明

  • start:起始值
  • end:目标值
  • t:进度,范围 0 到 1

9.5.2 缓动函数

python 复制代码
def ease_in_out(t):
    return t * t * (3 - 2 * t)

def ease_out_bounce(t):
    if t < 1 / 2.75:
        return 7.5625 * t * t
    elif t < 2 / 2.75:
        t -= 1.5 / 2.75
        return 7.5625 * t * t + 0.75
    elif t < 2.5 / 2.75:
        t -= 2.25 / 2.75
        return 7.5625 * t * t + 0.9375
    else:
        t -= 2.625 / 2.75
        return 7.5625 * t * t + 0.984375

9.5.3 Tween 类

python 复制代码
class Tween:
    def __init__(self, start_value, end_value, duration, ease_func=None):
        self.start_value = start_value
        self.end_value = end_value
        self.duration = duration
        self.ease_func = ease_func or (lambda t: t)
        self.start_time = pygame.time.get_ticks()
        self.finished = False

    def update(self):
        elapsed = pygame.time.get_ticks() - self.start_time
        t = min(elapsed / self.duration, 1.0)

        if t >= 1.0:
            self.finished = True

        return lerp(self.start_value, self.end_value, self.ease_func(t))

说明

  • Tween 常用于 UI 位移、缩放、透明度变化
  • 适合做按钮动画、弹出动画、提示框动画

9.6 动画状态机

动画状态机用于管理角色在不同状态下的动画切换,例如待机、行走、攻击。

9.6.1 状态机思路

python 复制代码
class AnimationStateMachine:
    def __init__(self):
        self.states = {}
        self.transitions = {}
        self.current_state = None

    def add_state(self, name, animation):
        self.states[name] = animation

    def add_transition(self, from_state, to_state, condition):
        if from_state not in self.transitions:
            self.transitions[from_state] = []
        self.transitions[from_state].append((to_state, condition))

    def set_state(self, name):
        if name in self.states:
            self.current_state = name
            self.states[name].reset()

9.6.2 更新逻辑

python 复制代码
def update(self, context):
    if self.current_state in self.transitions:
        for to_state, condition in self.transitions[self.current_state]:
            if condition(context):
                self.set_state(to_state)
                break

    if self.current_state:
        self.states[self.current_state].update()

说明

  • context 是外部状态信息,例如是否移动、是否攻击
  • 条件满足时自动切换状态

9.7 中文字体安全加载方案

在某些环境中,pygame.font.SysFont 可能触发系统字体扫描错误,因此本书建议统一使用字体文件路径加载。

9.7.1 推荐写法

python 复制代码
import pygame
import os

def get_font(size):
    font_paths = [
        r"C:\Windows\Fonts\simhei.ttf",
        r"C:\Windows\Fonts\msyh.ttc",
        r"C:\Windows\Fonts\simsun.ttc",
    ]

    for path in font_paths:
        if os.path.exists(path):
            try:
                return pygame.font.Font(path, size)
            except:
                pass

    return pygame.font.Font(None, size)

说明

  • 这种写法避免了 SysFont 引发的系统字体枚举问题
  • 适合本书所有涉及中文显示的示例

9.8 综合示例:角色动画系统

下面给出本章完整的综合示例,包含:

  • 帧动画
  • 状态机
  • 行走和待机切换
  • 中文字体安全加载
python 复制代码
import pygame
import sys
import os

pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("角色动画系统演示")
clock = pygame.time.Clock()

def get_font(size):
    font_paths = [
        r"C:\Windows\Fonts\simhei.ttf",
        r"C:\Windows\Fonts\msyh.ttc",
        r"C:\Windows\Fonts\simsun.ttc",
    ]

    for path in font_paths:
        if os.path.exists(path):
            try:
                return pygame.font.Font(path, size)
            except:
                pass

    return pygame.font.Font(None, size)

font = get_font(24)

class FrameAnimation:
    def __init__(self, frames, speed=100, loop=True):
        self.frames = frames
        self.speed = speed
        self.loop = loop
        self.current_frame = 0
        self.last_update = pygame.time.get_ticks()
        self.finished = False

    def reset(self):
        self.current_frame = 0
        self.finished = False
        self.last_update = pygame.time.get_ticks()

    def update(self):
        if self.finished or len(self.frames) == 0:
            return

        now = pygame.time.get_ticks()
        if now - self.last_update >= self.speed:
            self.last_update = now
            self.current_frame += 1

            if self.current_frame >= len(self.frames):
                if self.loop:
                    self.current_frame = 0
                else:
                    self.current_frame = len(self.frames) - 1
                    self.finished = True

    def get_image(self):
        if len(self.frames) == 0:
            return None
        return self.frames[self.current_frame]

class AnimationStateMachine:
    def __init__(self):
        self.states = {}
        self.transitions = {}
        self.current_state = None

    def add_state(self, name, animation):
        self.states[name] = animation

    def add_transition(self, from_state, to_state, condition):
        if from_state not in self.transitions:
            self.transitions[from_state] = []
        self.transitions[from_state].append((to_state, condition))

    def set_state(self, name):
        if name in self.states and name != self.current_state:
            self.current_state = name
            self.states[name].reset()

    def update(self, context):
        if self.current_state in self.transitions:
            for to_state, condition in self.transitions[self.current_state]:
                if condition(context):
                    self.set_state(to_state)
                    break

        if self.current_state:
            self.states[self.current_state].update()

    def get_image(self):
        if self.current_state:
            return self.states[self.current_state].get_image()
        return None

# 生成示例帧
idle_frames = []
walk_frames = []

for i in range(4):
    frame = pygame.Surface((64, 64), pygame.SRCALPHA)
    pygame.draw.rect(frame, (100 + i * 20, 150, 220), (18, 16, 28, 40))
    pygame.draw.circle(frame, (255, 220, 180), (32, 16), 10)
    idle_frames.append(frame)

for i in range(6):
    frame = pygame.Surface((64, 64), pygame.SRCALPHA)
    offset = 8 if i % 2 == 0 else -8
    pygame.draw.rect(frame, (80, 220, 120), (18 + offset, 16, 28, 40))
    pygame.draw.circle(frame, (255, 220, 180), (32 + offset, 16), 10)
    walk_frames.append(frame)

asm = AnimationStateMachine()
asm.add_state("idle", FrameAnimation(idle_frames, 200))
asm.add_state("walk", FrameAnimation(walk_frames, 100))

asm.add_transition("idle", "walk", lambda ctx: ctx["moving"])
asm.add_transition("walk", "idle", lambda ctx: not ctx["moving"])

asm.set_state("idle")

player_rect = pygame.Rect(368, 268, 64, 64)
player_speed = 5

running = True
while running:
    moving = False

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    keys = pygame.key.get_pressed()
    if keys[pygame.K_LEFT]:
        player_rect.x -= player_speed
        moving = True
    if keys[pygame.K_RIGHT]:
        player_rect.x += player_speed
        moving = True
    if keys[pygame.K_UP]:
        player_rect.y -= player_speed
        moving = True
    if keys[pygame.K_DOWN]:
        player_rect.y += player_speed
        moving = True

    asm.update({"moving": moving})

    screen.fill((40, 40, 40))

    image = asm.get_image()
    if image:
        screen.blit(image, player_rect)

    info1 = font.render("方向键移动角色", True, (255, 255, 255))
    info2 = font.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 中动画系统的基本实现方式。我们学习了帧动画、精灵表、补间动画和动画状态机的构建方法。动画系统是提升游戏表现力的重要组成部分,合理地组织动画逻辑可以让角色行为更加自然流畅。

本章知识点回顾

知识点 主要内容
帧动画 图像序列、定时切换
精灵表 合并帧、裁剪技术
补间动画 插值算法、缓动函数
状态机 动画状态管理、转换条件
中文字体 字体文件路径加载方案

课后练习

  1. 实现一个精灵表解析器,支持多行多列裁剪。
  2. 创建一个粒子动画系统。
  3. 实现动画暂停、继续和反向播放功能。
  4. 扩展状态机,加入攻击和受击动画。
  5. 实现一个角色跳跃动画系统。

下章预告

在下一章中,我们将学习游戏状态管理和场景切换,这是组织复杂游戏逻辑的重要基础。

相关推荐
智算菩萨2 小时前
【Pygame】第18章 游戏性能优化与帧率控制
游戏·性能优化·pygame
放飞自我的Coder2 小时前
【基于xGBoost的钓鱼邮件智能识别与拦截系统】
python
DFT计算杂谈2 小时前
eDMFT安装教程
java·服务器·前端·python·算法
小陈工2 小时前
2026年4月3日技术资讯洞察:微服务理性回归、AI代码生成争议与开源安全新挑战
开发语言·数据库·人工智能·python·安全·微服务·回归
CesareCheung2 小时前
Python+Vue +K6接口性能压测平台搭建
开发语言·vue.js·python
云烟成雨TD2 小时前
Spring AI 1.x 系列【23】:工具配置详解(全局默认+运行时动态)
人工智能·python·spring
人工干智能2 小时前
科普:Python / Numpy / PyTorch 的数据拼接方法
pytorch·python·numpy
Dxy12393102162 小时前
Python图片转PDF:高效实现多图合并与自定义布局
java·python·pdf
搂着猫睡的小鱼鱼2 小时前
反向海淘优势逐步释放,重构跨境贸易格局
python