学习内容:
精灵动画:
SpriteAnimation(多张图片循环播放,如走路动画)动画组件:
SpriteAnimationComponent实践任务:
准备一组 “走路动画图片”(如 4 张不同姿势的图片)
实现玩家 “移动时播放动画,静止时显示默认帧”
一、核心概念解析※
1. 精灵动画(SpriteAnimation)※
SpriteAnimation是Flame中用于管理帧动画的核心类,本质是“一组按顺序排列的精灵帧 + 播放规则”,核心作用:
加载多张动画帧图片,按指定间隔循环/单次播放
控制动画的播放、暂停、重置、切换帧等操作
适配不同帧率设备,保证动画播放速度统一
核心属性/方法
| 名称 | 类型/返回值 | 作用 |
| frames | List<SpriteAnimationFrame> | 动画帧列表(包含每帧的Sprite和显示时长) |
| loop | bool | 是否循环播放(默认true,走路动画需循环;攻击动画可设为false) |
| currentIndex | int | 当前播放的帧索引(可手动切换,如:静止时切默认帧) |
| play() | void | 开始/恢复播放动画 |
| pause() | void | 暂停动画(保留当前帧) |
| reset() | void | 重置动画到第一帧 |
| isPlaying | bool | 判断动画是否正在播放 |
2. 动画组件(SpriteAnimationComponent)※
SpriteAnimationComponent是Flame封装的动画显示组件,继承自PositionComponent,核心优势:
- 内置
SpriteAnimation管理逻辑,无需手动在render中绘制每一帧 - 支持组件的位置、尺寸、锚点等属性,与静态
SpriteComponent用法一致 - 可直接添加到FlameGame场景中,自动处理动画帧的切换与绘制
核心属性
| 名称 | 类型 | 作用 |
| animation | SpriteAnimation? | 关联的动画实例(必传,否则组件不可见) |
| size | Vector2 | 组件显示尺寸(覆盖动画帧的原始尺寸) |
| anchor | Anchor | 组件锚点(默认Anchor.topLeft,推荐设置为Anchor.center便于定位) |
| playing | bool | 控制动画是否播放(true=播放,false=暂停) |
3. 动画帧加载方式※
Flame支持两种动画帧加载方式,适配不同资源格式
| 加载方式 | 适用场景 | 核心方法 |
| 多张独立图片 | 动画帧为单独文件(如:walk_1.png,walk_2.png) | SpriteAnimation.fromFrameData |
| 精灵图集(SpriteSheet) | 所有帧合并到一张图片(节省资源) | SpriteSheet.createAnimation |
二、综合实战※
- 任务
- 准备4张走路动画帧图片(walk_1.png~walk_4.png)+ 1张静止默认帧(idle.png)
- 加载走路动画,配置帧间隔与循环播放
- 触屏拖动/键盘控制方向移动时,播放走路动画;停止移动时,暂停动画并切回默认帧
- 边界限制:不超出游戏容器范围
- 资源准备
- 动画帧:4张走路姿势图片(命名:walk_1.png~walk_4.png),尺寸建议100X100像素
- 静止帧:1张默认姿势图片(命名:idle.png),尺寸与动画帧一致
- 路径:统一放入
项目根目录/assets/images/player/目录
资源声明
打开
pubspec.yaml,添加资源路径(注意缩进)flutter: uses-material-design: true assets: - assets/images/player/执行
flutter pub get同步资源- 代码实现
游戏类(
player_animation_game.dart)集成动画加载、输入控制、动画状态切换逻辑
import 'dart:async'; import 'package:flame/game.dart'; import 'package:flame/components.dart'; import 'package:flame/cache.dart'; import 'package:flame/input.dart'; import 'package:flame/events.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; // 游戏类:混入PanDetector处理触屏拖动,兼容键盘控制 class PlayerAnimationGame extends FlameGame with PanDetector, KeyboardEvents { final Images _images = Images(); late SpriteAnimationComponent _player; late SpriteAnimation _walkAnimation; late SpriteAnimation _idleAnimation; final double _moveSpeed = 200; final double _walkFrameRate = 0.1; // 动画侦间隔(秒/帧),越小动画越快 final double _idleFrameRate = 1.0; // 静止帧间隔(无限长,伪静态) bool _isMoving = false; bool _moveLeft = false; bool _moveRight = false; bool _moveUp = false; bool _moveDown = false; @override Future<void> onLoad() async { await super.onLoad(); await _loadSpritesAndAnimations(); _initPlayerCompnent(); } Future<void> _loadSpritesAndAnimations() async { final idleImage = await _images.load('player/idle.png'); _idleAnimation = SpriteAnimation.fromFrameData( idleImage, SpriteAnimationData.sequenced(amount: 1, stepTime: _idleFrameRate, textureSize: Vector2(100, 100), loop: false)); final walkFrames = <Sprite>[]; for (var i = 0; i <= 4; i++) { final frameImage = await _images.load('player/walk_$i.png'); walkFrames.add(Sprite(frameImage)); } _walkAnimation = SpriteAnimation.fromFrameData( walkFrames.first.image, SpriteAnimationData.sequenced(amount: walkFrames.length, stepTime: _walkFrameRate, textureSize: Vector2(100, 100), loop: true) ); _walkAnimation.frames = walkFrames.map((sprite) => SpriteAnimationFrame(sprite, _walkFrameRate)).toList(); } void _initPlayerCompnent() { _player = SpriteAnimationComponent( animation: _idleAnimation, size: Vector2(100, 100), anchor: Anchor.center, position: Vector2(size.x / 2, size.y / 2), playing: false ); add(_player); } @override void onPanUpdate(DragUpdateInfo info) { super.onPanUpdate(info); final delta = info.delta.global; _player.position += delta; _isMoving = true; _limitPlayerBounds(); } @override void onPanEnd(DragEndInfo info) { super.onPanEnd(info); _isMoving = false; } @override KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) { // 更新方向键状态(按下/抬起) _moveLeft = keysPressed.contains(LogicalKeyboardKey.arrowLeft); _moveRight = keysPressed.contains(LogicalKeyboardKey.arrowRight); _moveUp = keysPressed.contains(LogicalKeyboardKey.arrowUp); _moveDown = keysPressed.contains(LogicalKeyboardKey.arrowDown); _isMoving = _moveLeft || _moveRight || _moveUp || _moveDown; return super.onKeyEvent(event, keysPressed); } @override void update(double dt) { super.update(dt); _handleKeyboardMovement(dt); _switchAnimation(); _limitPlayerBounds(); } void _handleKeyboardMovement(double dt) { if (!_isMoving) return; Vector2 moveDelta = Vector2.zero(); if (_moveLeft) { moveDelta.x -= _moveSpeed * dt; } if (_moveRight) { moveDelta.x += _moveSpeed * dt; } if (_moveDown) { moveDelta.y += _moveSpeed * dt; } if (_moveUp) { moveDelta.y -= _moveSpeed * dt; } _player.position += moveDelta; } void _switchAnimation() { if (_isMoving) { if (_player.animation != _walkAnimation) { _player.animation = _walkAnimation; _player.playing = true; } } else { if (_player.animation != _idleAnimation) { _player.animation = _idleAnimation; _player.playing = false; } } } void _limitPlayerBounds() { final halfWidth = _player.size.x / 2; final halfHeight = _player.size.y / 2; _player.position.x = _player.position.x.clamp(halfWidth, size.x - halfWidth); _player.position.y = _player.position.y.clamp(halfHeight, size.y - halfHeight); } }游戏页面(
game_screen.dart)import 'package:flutter/material.dart'; import 'package:flame/game.dart'; import 'player_animation_game.dart'; class GameScreen extends StatelessWidget { const GameScreen({super.key}); @override Widget build(BuildContext context) { final game = PlayerAnimationGame(); return Scaffold( appBar: AppBar( title: const Text('Flame精灵动画:移动/静止'), centerTitle: true, backgroundColor: Colors.blueAccent, ), body: GameWidget( game: game, autofocus: true, backgroundBuilder: (context) => Container(color: Colors.black,), loadingBuilder: (context) => const Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ CircularProgressIndicator(color: Colors.blueAccent,), SizedBox(height: 20,), Text( '加载动画资源中...', style: TextStyle(color: Colors.white, fontSize: 16), ) ], ), ), errorBuilder: (context, error) => Center( child: Text( '资源加载失败:$error', style: TextStyle(color: Colors.red, fontSize: 16), textAlign: TextAlign.center, ), ), ), ); } }入口文件(
main.dart)import 'package:flutter/material.dart'; import 'game_screen.dart'; void main() => runApp(const App()); class App extends StatelessWidget { const App({super.key}); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flame精灵动画示例', theme: ThemeData(primarySwatch: Colors.blue), home: const GameScreen(), ); } }
运行效果
