theme: cyanosis
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 5 天,点击查看活动详情
前言
这是一套 张风捷特烈 出品的 Flutter&Flame
系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列文章一览:
- 【Flutter&Flame 游戏 - 壹】开启新世界的大门
- 【Flutter&Flame 游戏 - 贰】操纵杆与角色移动
- 【Flutter&Flame 游戏 - 叁】键盘事件与手势操作
- 【Flutter&Flame 游戏 - 肆】精灵图片加载方式
- 【Flutter&Flame 游戏 - 伍】Canvas 参上 | 角色的血条
- 【Flutter&Flame 游戏 - 陆】暴击 Dash | 文字构件的使用
- 【Flutter&Flame 游戏 - 柒】人随指动 | 动画点触与移动
- 【Flutter&Flame 游戏 - 捌】装弹完毕 | 角色武器发射
- 【Flutter&Flame 游戏 - 玖】探索构件 | Component 是什么
- 【Flutter&Flame 游戏 - 拾】探索构件 | Component 生命周期回调
- 【Flutter&Flame 游戏 - 拾壹】探索构件 | Component 使用细节
- 【Flutter&Flame 游戏 - 拾贰】探索构件 | 角色管理
- 【Flutter&Flame 游戏 - 拾叁】碰撞检测 | CollisionCallbacks
- 【Flutter&Flame 游戏 - 拾肆】碰撞检测 | 之前代码优化
- 【Flutter&Flame 游戏 - 拾伍】粒子系统 | ParticleSystemComponent
- 【Flutter&Flame 游戏 - 拾陆】粒子系统 | 粒子的种类
- 【Flutter&Flame 游戏 - 拾柒】构件特效 | 了解 Effect 体系
- 【Flutter&Flame 游戏 - 拾捌】构件特效 | ComponentEffect 一族
- 【Flutter&Flame 游戏 - 拾玖】构件特效 | 了解 EffectController 体系
- 【Flutter&Flame 游戏 - 贰拾】构件特效 | 其他 EffectControler
- 【Flutter&Flame 游戏 - 贰壹】视差组件 | ParallaxComponent
- 【Flutter&Flame 游戏 - 贰贰】菜单、字体和浮层
- 【Flutter&Flame 游戏 - 贰叁】 资源管理与国际化
- 【Flutter&Flame 游戏 - 贰肆】pinball 源码分析 - 项目结构介绍
- 【Flutter&Flame 游戏 - 贰伍】pinball 源码分析 - 资源加载与 Loading
- 【Flutter&Flame 游戏 - 贰陆】pinball 源码分析 - 游戏主菜单界面
- 【Flutter&Flame 游戏 - 贰柒】pinball 源码分析 - 角色选择与玩法面板
- 【Flutter&Flame 游戏 - 贰捌】pinball 源码分析 - 游戏主场景的构成
- 【Flutter&Flame 游戏 - 贰玖】pinball 源码分析 - 视口与相机
第一季完结,谢谢支持 ~
1. 什么是精灵图
我们前面用的角色动画帧有九张,就表示需要加载九次图片资源。对于动画帧来说,每帧的尺寸一般都是一样的,可以将它们拼接在一张图片中,如下图所示:图片取自于 【pinball】开源项目。
这在前端开发中比较常见,因为每个小图片都需要发一次请求,将小图片拼在一起,可以减少请求的次数。在游戏开发者也是如此,将小图片拼合在一起可以有效减少加载的次数。
2. 如何从精灵图中获取图片
Flame
中通过 SpriteSheet
类对精灵图进行处理,如下通过 fromColumnsAndRows
构造可以指定行列。这也就说明该类只能加载的图片要求:精灵图中的单体必须尺寸一致。
dart ---->[源码: SpriteSheet]---- SpriteSheet.fromColumnsAndRows({ required this.image, required int columns, required int rows, }) : srcSize = Vector2( image.width / columns, image.height / rows, );
然后通过 getSpriteById
传入索引来获取第几个图片对应的 Sprite
对象。另外还提供了 getSprite
方法,通过指定行列获取图片对应的 Sprite
对象。注意,索引和行列都是从 0
开始数的。
```dart ---->[源码: SpriteSheet]---- Sprite getSprite(int row, int column) { return getSpriteById(row * columns + column); }
Sprite getSpriteById(int spriteId) { return _spriteCache[spriteId] ??= _computeSprite(spriteId); } ```
3. 使用测试案例
如下案例中,加载第一帧作为另一个角色 Monster
,且该角色会随机出现在屏幕的最右侧。代码见 【04/01】
Monster
继承自 SpriteComponent
,在构造方法里传入 精灵
、尺寸
、位置
信息,方便初始化。
```dart class Monster extends SpriteComponent { Monster({ required Sprite sprite, required Vector2 size, required Vector2 position, }) : super( sprite: sprite, size: size, position: position, anchor: Anchor.center, );
@override Future onLoad() async { add(RectangleHitbox()..debugMode = true); } } ```
下面是 TolyGame
中的逻辑处理,在 onLoad
方法中加载精灵图,并取第一帧作为 Monster
的显示图片。另外随机出现在屏幕的最右侧,言外之意是横坐标固定,纵坐标随机,代码处理如下:
```dart ---->[04/01/TolyGame]---- final Random _random = Random();
@override Future onLoad() async { player = Adventurer(); add(player); // 加载 SpriteSheet const String src = 'adventurer/animatronic.png'; await images.load(src); var image = images.fromCache(src); SpriteSheet sheet = SpriteSheet.fromColumnsAndRows(image: image, columns: 13, rows: 6); Sprite sprite = sheet.getSpriteById(0); // 初始化 Monster Vector2 monsterSize = Vector2(64,64); final double pY = _random.nextDouble()*size.y; final double pX = size.x - monsterSize.x/2; // tag1 monster = Monster(sprite: sprite,size: monsterSize,position: Vector2(pX, pY)); add(monster); } ```
*注*
:tag1
处减去 monsterSize.x/2
,是因为 Monster
的锚点在中心,不减的话就会如下图所示。也就是说 Component
的 position
指的是锚点处的坐标,想让Monster
的右侧显示出来,向左偏移一半宽度即可。
4. 精灵图动画的加载
在第一篇 我们就介绍过使用 SpriteAnimationComponent
构件显示多帧动画,其实本质上就是多个 Sprite
对象,循环切换而已。前面知道如何通过 SpriteSheet
获取对应索引的 Sprite
,那接下来的事情就好办了。这里完成如下图所示的效果:代码见 【04/02】
实现,将 Monster
改为继承自 SpriteAnimationComponent
,支持帧动画。
```dart class Monster extends SpriteAnimationComponent { Monster({ required SpriteAnimation animation, required Vector2 size, required Vector2 position, }) : super( animation: animation, size: size, position: position, anchor: Anchor.center, );
@override Future onLoad() async { add(RectangleHitbox()..debugMode = true); } } ```
如下遍历 frameCount
此,获取 Sprite
列表,来生成 SpriteAnimation
即可,如下所示:
```dart ---->[04/02/TolyGame$onLoad]---- const String src = 'adventurer/animatronic.png'; await images.load(src); var image = images.fromCache(src); SpriteSheet sheet = SpriteSheet.fromColumnsAndRows( image: image, columns: 13, rows: 6, );
int frameCount = sheet.rows * sheet.columns; List sprites = List.generate(frameCount, sheet.getSpriteById); SpriteAnimation animation = SpriteAnimation.spriteList( sprites, stepTime: 1 / 24, loop: true, ); ```
对于这种一张图记录的全是一个动画的精灵图,也可以不使用 SpriteSheet
。通过 fromFrameData
构造可以更简单直接地创建动画精灵对象,也能完成同样的效果。也就是写法上简洁一点而已,本质上没有什么区别。
```dart const int rowCount = 6; const int columnCount = 13; final Vector2 textureSize = Vector2( image.width / columnCount, image.height / rowCount, );
SpriteAnimation animation = SpriteAnimation.fromFrameData( image, SpriteAnimationData.sequenced( amount: rowCount * columnCount, amountPerRow: columnCount, stepTime: 1 / 24, textureSize: textureSize, ), ); ```
5. 分包管理 - 简单拓展 SpriteSheet
通过 SpriteSheet
可以更灵活地操作需要哪些帧,比如像这种多个角色出现在一张精灵图里,SpriteAnimation.fromFrameData
就没法用了。
SpriteSheet
可以通过行列来获取指定的图片,比如下面红框所示的是 第四行,第五列图片,由于索引从 0
计数,也就是用 (3,4)
表示。
SpriteSheet
中的方法非常少,并没有获取索引区间段 Sprite
列表的方法,像这种图要自己来数,就比较麻烦。可以写个 extension
来拓展一下,可能一般人顺手就在 lib
中创建的文件夹开写了。看 flutter
官方的 pinball
项目中,会对模块进行分包,而不是所有代码都塞在一块。
这里的 extension
和项目本身关系不大,是对 flame
的拓展,相对独立。以后可能还会写其他的拓展方法以便使用,这里也在项目中创建一个 packages
来进行分包管理。这样的另一个好处是:我可以将 flame_ext
分享到 pub
中,让所有人都可以使用。
下面说下创建包的方式,在 New Flutter Projrct
中 Projrct type
选择 Package
即可,如下把包创建在项目根目录的 packages
下:
然后在 pubspec.yaml
中通过 path
来引入本地库:
yaml dependencies: #... flame_ext: path: packages/flame_ext
当前的包结构如下,在 flame_ext.dart
中导出 sprite_sheet_ext.dart
,这样引入 flame_ext.dart
即可使用 sprite_sheet_ext
中定义的拓展方法。
下面是 sprite_sheet_ext
中的处理逻辑,拓展一个 getRowSprites
的方法,返回 Sprite
列表。该方法的作用是:取第几行,从第几个开始的多少个 Sprite
形成列表。
```dart ---->[spritesheetext.dart]---- extension SpriteSheetExt on SpriteSheet { /// 获取指定索引区间的 [Sprite] 列表 /// [row] : 第几行 /// [start] : 第几个开始 /// [count] : 有几个 List getRowSprites({ required int row, int start = 0, required count, }) { return List.generate(count, (i) => getSprite(row, start + i)).toList(); } }
```
比如下面指定是第三行,从 0 开始取五帧,语义上就非常明确,而不需要每次使用都计算一下:
dart sheet.getRowSprites(row: 3,count: 5)
如下是通过这种方法显示的效果,代码见: 【04/03】
```dart import 'package:flameext/flameext.dart';
List sprites = sheet.getRowSprites(row: 3,count: 5); SpriteAnimation animation = SpriteAnimation.spriteList( sprites, stepTime: 1 / 10, loop: true, ); ```
再比如,第一行,从第六个开始,取 4 个,是石像怪的序列帧:
dart List<Sprite> sprites = sheet.getRowSprites(row: 0,start: 5,count: 4);
到这里,对精灵图的使用就介绍完毕了,大家可以结合上一章的内容,思考一下,如何让 Monster
主动向左运动。如下代码实现在 【04/04】 ,那本文就到这里,明天见 ~
@张风捷特烈 2022.05.29 未允禁转
我的 公众号: 编程之王
我的 掘金主页
: 张风捷特烈我的 B站主页
: 张风捷特烈我的 github 主页
: toly1994328
\