30天计划第22天-游戏动画(SpriteAnimation)

-
-
2025-12-09 09:23
  • 学习内容

    1. 精灵动画:SpriteAnimation(多张图片循环播放,如走路动画)

    2. 动画组件:SpriteAnimationComponent

  • 实践任务

    1. 准备一组 “走路动画图片”(如 4 张不同姿势的图片)

    2. 实现玩家 “移动时播放动画,静止时显示默认帧”

一、核心概念解析

1. 精灵动画(SpriteAnimation)

SpriteAnimation是Flame中用于管理帧动画的核心类,本质是“一组按顺序排列的精灵帧 + 播放规则”,核心作用:

  • 加载多张动画帧图片,按指定间隔循环/单次播放

  • 控制动画的播放、暂停、重置、切换帧等操作

  • 适配不同帧率设备,保证动画播放速度统一

核心属性/方法

名称类型/返回值作用
framesList<SpriteAnimationFrame>动画帧列表(包含每帧的Sprite和显示时长)
loopbool是否循环播放(默认true,走路动画需循环;攻击动画可设为false)
currentIndexint当前播放的帧索引(可手动切换,如:静止时切默认帧)
play()void开始/恢复播放动画
pause()void暂停动画(保留当前帧)
reset()void重置动画到第一帧
isPlayingbool判断动画是否正在播放

2. 动画组件(SpriteAnimationComponent)

SpriteAnimationComponent是Flame封装的动画显示组件,继承自PositionComponent,核心优势:

  • 内置SpriteAnimation管理逻辑,无需手动在render中绘制每一帧
  • 支持组件的位置、尺寸、锚点等属性,与静态SpriteComponent用法一致
  • 可直接添加到FlameGame场景中,自动处理动画帧的切换与绘制

核心属性

名称类型作用
animationSpriteAnimation?关联的动画实例(必传,否则组件不可见)
sizeVector2组件显示尺寸(覆盖动画帧的原始尺寸)
anchorAnchor组件锚点(默认Anchor.topLeft,推荐设置为Anchor.center便于定位)
playingbool控制动画是否播放(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(),
          );
        }
      }
  • 运行效果


目录