【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. 实现一个角色跳跃动画系统。

下章预告

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

相关推荐
maqr_110几秒前
Golang怎么对接ChatGPT_Golang ChatGPT教程【简明】
jvm·数据库·python
m0_514520572 分钟前
JavaScript中函数声明位置对解析器预编译的影响
jvm·数据库·python
m0_743623925 分钟前
SQL多维度统计优化_GROUP BY索引组合设计
jvm·数据库·python
AI是这个时代的魔法9 分钟前
Unpack Nested Data:照亮你的数据结构
数据结构·python
Greyson112 分钟前
HTML怎么创建时间轴布局_HTML结构化时间线写法【方法】
jvm·数据库·python
财经资讯数据_灵砚智能15 分钟前
基于全球经济类多源新闻的NLP情感分析与数据可视化(日间)2026年4月24日
人工智能·python·信息可视化·自然语言处理·ai编程
_阿衡_15 分钟前
python写洛克王国精灵蛋预测
python
qq_2069013916 分钟前
如何为 JSON 序列化中的不同浮点字段指定独立的小数精度
jvm·数据库·python
财经资讯数据_灵砚智能20 分钟前
基于全球经济类多源新闻的NLP情感分析与数据可视化(夜间-次晨)2026年4月23日
人工智能·python·信息可视化·自然语言处理·ai编程
思绪无限21 分钟前
YOLOv5至YOLOv12升级:机械器件识别系统的设计与实现(完整代码+界面+数据集项目)
人工智能·python·深度学习·目标检测·计算机视觉·机械器件识别