一个高性能、易集成的Pygame动画播放器,所有功能集成在单个文件中,无需复杂依赖。
- Python: 3.8+
- Pygame: 2.0+
- 单文件设计 - 只需复制一个文件即可开始使用
- 多种播放模式 - 支持循环(loop)、单次(once)、往返(pingpong)播放
- 智能缓存 - LRU缓存管理,自动优化内存使用
- 类型安全 - 完整的类型注解,更好的开发体验
- 灵活配置 - 支持图片路径和pygame.Surface两种帧数据源
- 高性能 - 线程安全设计,适合游戏开发
- 无缝集成 - 继承自
pygame.sprite.Sprite,与Pygame生态完美兼容
只需将 animation.py 文件复制到你的项目目录中!
from animation import FramePlayer, AnimationConfig
import pygame
# 初始化pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
# 创建一些动画帧(使用pygame.Surface)
frames = {}
for state in ["idle", "walk"]:
frames[state] = []
for i in range(4):
surf = pygame.Surface((32, 32), pygame.SRCALPHA)
color = (255, 100 + i*40, 50) if state == "idle" else (50, 100 + i*40, 255)
pygame.draw.circle(surf, color, (16, 16), 10 + i*2)
frames[state].append(surf)
# 创建动画播放器
config = AnimationConfig(
frames=frames,
frames_times={"idle": 0.2, "walk": 0.1},
frame_scale=(64, 64),
play_mode="loop" # 这一行可以省略
)
animation = FramePlayer(config)
running = True
while running:
dt = clock.tick(60) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
if animation.get_state() == "idle":
animation.set_state("walk")
elif animation.get_state() == "walk":
animation.set_state("idle")
# 更新动画
animation.update_frame(dt)
screen.fill((0, 0, 0))
animation.rect.center = (400, 300)
animation.draw(screen)
pygame.display.flip()
# 清理资源
animation.release() # 你也可以使用 animation.kill()
pygame.quit()update_frame(dt: float, direction: Tuple[bool, bool] = (False, False), scale: Tuple[int, int] = (0, 0), angle: float = 0.0)
更新动画帧
dt: 时间增量(秒)direction: 翻转方向(flip_x, flip_y)scale: 缩放尺寸(width, height)angle: 旋转角度范围为[0, 360)
设置动画状态
state: 状态名称reset_frame: 是否重置到第一帧
绘制到目标surface
is_playing: bool- 是否正在播放rect: pygame.Rect- 动画位置和尺寸image: pygame.Surface- 当前帧图像
def load_image(path):
return pygame.image.load(path).convert_alpha()
# 定义帧序列
frames = {
"run": ["run_0.png", "run_1.png", "run_2.png", "run_3.png"],
"jump": ["jump_0.png", "jump_1.png", "jump_2.png"]
}
# 创建配置
config = AnimationConfig(
frames=frames,
frames_times={"run": 0.1, "jump": 0.15},
frame_scale=(48, 48)
)
# 创建动画播放器(需要提供图片资源字典)
injection = AnimationParamInjection(
image_provider={path: load_image(path) for path in set(sum(frames.values(), []))}
)
animation = FramePlayer(config, injection)def on_animation_complete():
print("动画播放完成!")
animation.set_state("idle")
def on_frame_change(frame_index):
print(f"切换到第 {frame_index} 帧")
def on_state_change(new_state):
print(f"状态切换到: {new_state}")
# 添加回调
animation.add_complete_callback(on_animation_complete)
animation.add_frame_change_callback(on_frame_change)
animation.add_state_change_callback(on_state_change)# 设置播放模式
animation.set_play_mode("once") # 播放一次
animation.set_play_mode("loop") # 循环播放
animation.set_play_mode("pingpong") # 往返播放
# 控制播放
animation.pause() # 暂停
animation.resume() # 继续播放
animation.rewind() # 重置到开始| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
frames |
Dict[str, List[str, pygame.Surface]] |
动画帧数据 | 必填 |
frames_times |
Dict[str, float] |
每帧持续时间(秒) | 必填 |
frame_scale |
Tuple[int, int] |
帧缩放尺寸 | (0, 0) |
play_mode |
Literal["loop", "once", "pingpong"] |
播放模式 | "loop" |
max_cache_size |
int |
缓存大小 | 200 |
设置帧的缩放尺寸,默认为 (0, 0),即不缩放。
-
"loop"- 循环播放(0->1->2->0->1->2...) -
"once"- 播放一次后停止(0->1->2->结束) -
"pingpong"- 往返播放(0->1->2->1->0)
设置缓存最大容量,单位为帧数。当缓存超过最大容量时,会自动删除最早的帧。
注:max_cache_size 不能小于 10(_AnimationMagicNumber.CACHE_MIN_SIZE)
| 参数 | 类型 | 说明 | 默认值 | 提供为None时FramePlayer.__init__初始化给予的值 |
|---|---|---|---|---|
image_provider |
Optional[Dict[str, pygame.Surface]] |
图片数据 | None |
{} |
logger_instance |
Optional[AbstractLogger] |
日志实例 | None |
DefaultLogger() |
A: 单文件设计使得集成更加简单,无需处理多文件依赖关系,特别适合小型到中型项目。
A: 内置LRU缓存系统会自动管理内存使用,当缓存达到上限时会自动移除最久未使用的资源。
A: 是的!所有缓存操作都是线程安全的(使用了threading.RLock()),可以在多线程环境中使用。
A: 调用 animation.release() 或使用上下文管理器:
with FramePlayer(config) as animation:
...虽然不推荐用__del__或del animation自动销毁,但__del__直接调用release(),也可以正常释放(在没有release()使用__del__会有警告)
欢迎提交Issue和Pull Request!对于单文件项目,建议:
保持向后兼容性
添加详细的类型注解
在添加新功能时考虑文件大小
如果你遇到问题:
- 查看文件内的详细注释
- 检查类型提示获取参数信息
- 提交Issue时请提供最小重现示例
from animation import FramePlayerEasilyGenerator, AnimationConfig
# 简单创建方式
animation = FramePlayerEasilyGenerator.create(
frames={"idle": ["idle_1.png", "idle_2.png"]},
frames_times={"idle": 0.2}
)# 定义多个动画状态
states = {
"idle": ["idle_1.png", "idle_2.png", "idle_3.png"],
"walk": ["walk_1.png", "walk_2.png", "walk_3.png", "walk_4.png"],
"jump": ["jump_1.png", "jump_2.png"]
}
# 配置不同状态的播放速度
times = {
"idle": 0.3,
"walk": 0.1,
"jump": 0.2
}
config = AnimationConfig(
frames=states,
frames_times=times,
play_mode="loop"
)
animation = FramePlayer(config)
# 根据游戏逻辑切换状态
def handle_input():
if player.is_walking(): # 此为实际项目用户自己实现的方法,下方的 player.is_jumping() 方法同理
animation.set_state("walk")
elif player.is_jumping():
animation.set_state("jump")
else:
animation.set_state("idle")- 对于大量动画,适当增加max_cache_size
- 重用AnimationConfig对象创建多个动画播放器
- 对于不常用的动画状态,可以手动调用clear_cache()
- 使用Surface帧比图片路径加载更快
欢迎提交Issue和Pull Request!贡献时请注意:
- 保持向后兼容性
- 添加详细的类型注解
- 在添加新功能时考虑文件大小
- 遵循现有代码风格
- 为新增功能添加测试用例
- 更新文档和示例
- 提交Issue前请先搜索是否已有类似问题
- 提交Pull Request时请描述清楚变更内容
- 讨论问题时保持专业和友善
- 遵守开源社区行为准则
A high-performance, easy-to-integrate Pygame animation player, with all functionality integrated into a single file, requiring no complex dependencies.
- Compatibility
- Features
- Quick Start
- Installation
- Detailed Usage
- API Reference
- FAQ
- Contributing
- Support
- License
- Python: 3.8+
- Pygame: 2.0+
- Single File Design - Just copy one file to start using
- Multiple Playback Modes - Supports loop, once, and pingpong playback
- Smart Caching - LRU cache management, automatically optimizes memory usage
- Type Safety - Full type annotations for a better development experience
- Flexible Configuration - Supports both image paths and
pygame.Surfaceobjects as frame data sources - High Performance - Thread-safe design, suitable for game development
- Seamless Integration - Inherits from
pygame.sprite.Sprite, perfectly compatible with the Pygame ecosystem
Simply copy the animation.py file into your project directory!
from animation import FramePlayer, AnimationConfig
import pygame
# Initialize pygame
pygame.init()
screen = pygame.display.set_mode((800, 600))
clock = pygame.time.Clock()
# Create some animation frames (using pygame.Surface)
frames = {}
for state in ["idle", "walk"]:
frames[state] = []
for i in range(4):
surf = pygame.Surface((32, 32), pygame.SRCALPHA)
color = (255, 100 + i*40, 50) if state == "idle" else (50, 100 + i*40, 255)
pygame.draw.circle(surf, color, (16, 16), 10 + i*2)
frames[state].append(surf)
# Create the animation player
config = AnimationConfig(
frames=frames,
frames_times={"idle": 0.2, "walk": 0.1},
frame_scale=(64, 64),
play_mode="loop" # This line can be omitted #
)
animation = FramePlayer(config)
running = True
while running:
dt = clock.tick(60) / 1000.0
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
if animation.get_state() == "idle":
animation.set_state("walk")
elif animation.get_state() == "walk":
animation.set_state("idle")
# Update animation
animation.update_frame(dt)
screen.fill((0, 0, 0))
animation.rect.center = (400, 300)
animation.draw(screen)
pygame.display.flip()
# Cleanup resources
animation.release() # You also can use animation.kill()
pygame.quit()update_frame(dt: float, direction: Tuple[bool, bool] = (False, False), scale: Tuple[int, int] = (0, 0), angle: float = 0.0)
Update the animation frame
dt: Time delta (seconds)direction: Flip direction(flip_x, flip_y)scale: Scaling dimensions(width, height)angle: Rotation angle range[0, 360)
Set the animation state
state: State namereset_frame: Whether to reset to the first frame
Draw to the target surface
is_playing: bool- Whether it is currently playingrect: pygame.Rect- Animation position and dimensionsimage: pygame.Surface- Current frame image
def load_image(path):
return pygame.image.load(path).convert_alpha()
# Define frame sequences
frames = {
"run": ["run_0.png", "run_1.png", "run_2.png", "run_3.png"],
"jump": ["jump_0.png", "jump_1.png", "jump_2.png"]
}
# Create configuration
config = AnimationConfig(
frames=frames,
frames_times={"run": 0.1, "jump": 0.15},
frame_scale=(48, 48)
)
# Create animation player (requires providing an image resource dictionary)
injection = AnimationParamInjection(
image_provider={path: load_image(path) for path in set(sum(frames.values(), []))}
)
animation = FramePlayer(config, injection)def on_animation_complete():
print("Animation complete!")
animation.set_state("idle")
def on_frame_change(frame_index):
print(f"Switched to frame {frame_index}")
def on_state_change(new_state):
print(f"State changed to: {new_state}")
# Add callbacks
animation.add_complete_callback(on_animation_complete)
animation.add_frame_change_callback(on_frame_change)
animation.add_state_change_callback(on_state_change)# Set playback mode
animation.set_play_mode("once") # Play once
animation.set_play_mode("loop") # Loop playback
animation.set_play_mode("pingpong") # Pingpong playback
# Control playback
animation.pause() # Pause
animation.resume() # Resume playback
animation.rewind() # Reset to start| Parameter | Type | Description | Default |
|---|---|---|---|
frames |
Dict[str, List[str, pygame.Surface]] |
Animation frame data | Required |
frames_times |
Dict[str, float] |
Frame duration (seconds) | Required |
frame_scale |
Tuple[int, int] |
Frame scaling dimensions | (0, 0) |
play_mode |
Literal["loop", "once", "pingpong"] |
Playback mode | "loop" |
max_cache_size |
int |
Cache size | 200 |
Sets the scaling dimensions for frames. Default is (0, 0), meaning no scaling.
"loop"- Loop playback (0->1->2->0->1->2...)"once"- Play once and stop (0->1->2->end)"pingpong"- Pingpong playback (0->1->2->1->0)
Sets the maximum cache capacity, measured in number of frames. When the cache exceeds the maximum capacity, the oldest frames are automatically removed.
Note: max_cache_size cannot be less than 10 (_AnimationMagicNumber.CACHE_MIN_SIZE)
| Parameter | Type | Description | Default | Value Initialized by FramePlayer.__init__ if Provided as None |
|---|---|---|---|---|
image_provider |
Optional[Dict[str, pygame.Surface]] |
Image data | None |
{} |
logger_instance |
Optional[AbstractLogger] |
Logger instance | None |
DefaultLogger() |
A: The single file design makes integration simpler, no need to handle multi-file dependencies, especially suitable for small to medium-sized projects.
A: The built-in LRU cache system automatically manages memory usage, removing the least recently used resources when the cache reaches its limit.
A: Yes! All cache operations are thread-safe (using threading.RLock()), and can be used in multi-threaded environments.
A: Call animation.release() or use the context manager:
with FramePlayer(config) as animation:
...Although not recommended to rely on __del__ or del animation for automatic cleanup, __del__ directly calls release(), so it can also release normally (a warning will be issued if __del__ is used without release() having been called).
Welcome to submit Issues and Pull Requests! For a single-file project, it is recommended to:
- Maintain backward compatibility
- Add detailed type annotations
- Consider file size when adding new features
If you encounter problems:
- Check the detailed comments within the file
- Check type hints for parameter information
- Please provide a minimal reproducible example when submitting an Issue
from animation import FramePlayerEasilyGenerator, AnimationConfig
# Simple creation method
animation = FramePlayerEasilyGenerator.create(
frames={"idle": ["idle_1.png", "idle_2.png"]},
frames_times={"idle": 0.2}
)
# Define multiple animation states
states = {
"idle": ["idle_1.png", "idle_2.png", "idle_3.png"],
"walk": ["walk_1.png", "walk_2.png", "walk_3.png", "walk_4.png"],
"jump": ["jump_1.png", "jump_2.png"]
}
# Configure playback speeds for different states
times = {
"idle": 0.3,
"walk": 0.1,
"jump": 0.2
}
config = AnimationConfig(
frames=states,
frames_times=times,
play_mode="loop"
)
animation = FramePlayer(config)
# Switch states based on game logic
def handle_input(player):
if player.is_walking(): # This is a method implemented by the user themselves, and the player. player.is_jumping() method below is the same
animation.set_state("walk")
elif player.is_jumping():
animation.set_state("jump")
else:
animation.set_state("idle")- For a large number of animations, appropriately increase
max_cache_size - Reuse
AnimationConfigobjects to create multiple animation players - For infrequently used animation states, you can manually call
clear_cache() - Using Surface frames is faster than loading from image paths
Welcome to submit Issues and Pull Requests! Please note when contributing:
- Maintain backward compatibility
- Add detailed type annotations
- Consider file size when adding new features
- Follow the existing code style
- Add test cases for new features
- Update documentation and examples
- Please search for existing similar issues before submitting a new one
- Clearly describe the changes when submitting a Pull Request
- Maintain professionalism and friendliness when discussing issues
- Adhere to the open source community code of conduct