Flutter&Flame游戏实践#12 | 打砖块 - 粒子与打包应用

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!


Flutter&Flame 游戏开发系列前言:

该系列是 [张风捷特烈] 的 Flame 游戏开发教程。Flutter 作为 全平台原生级 渲染框架,兼具 全端 跨平台和高性能的特点。目前官方对休闲游戏的宣传越来越多,以 Flame 游戏引擎为基础,Flutter 有游戏方向发展的前景。本系列教程旨在让更多的开发者了解 Flutter 游戏开发。

第一季:30 篇短文章,快速了解 Flame 基础。[已完结]

第二季:从休闲游戏实践,进阶 Flutter&Flame 游戏开发。

两季知识是独立存在的,第二季 不需要 第一季作为基础。本系列教程源码地址在 【toly1994328/toly_game】,系列文章列表可在《文章总集》【github 项目首页】 查看。


一、 粒子系统

粒子系统 ParticleSystemComponent 是 Flame 中提供的一类具有 生命时长 的构件。具有明确生命时长的大量个体称之为 Particle。粒子系统依赖 Particle 对象进行渲染与更新:

在第一季中已经对粒子系统的 使用方式 进行过全面的介绍。本篇将基于打砖块的案例,具体介绍粒子系统的应用。


1.砖块的爆炸效果

游戏是一种与用户高频交互的应用产品。用户的参与感,首要在于 游戏玩法,其次在于音效、视觉反馈交互体验。比如说小球的打击感:

  • [1] 靠撞击时的 反弹 这种现实物理经验,让用户在感知上体验打击感。
  • [2] 通过需求撞击时发出的音效,如用户在听觉上体验打击感。
  • [3] 通过添加砖块爆炸效果,在视觉上进一步加强打击感。

如下所示,当撞击或者射击等方式使砖块消失时,通过序列帧动画展示爆炸效果,爆炸完后就会消失。所以,这是一种大量、具有生命时长的构件。可以通过粒子系统来完成:

撞击爆炸 射击爆炸

Particle 的派生体系中,有 SpriteAnimationParticle 粒子可用于序列帧动画的播放。

之前硬币的序列帧动画,是通过一整张序列帧图片加载的资源:

这里来看一下,如何使用一张张的序列帧图片进行动画,如下所示,爆炸的序列帧是 14 张图片:

序列帧动画,主要是构建 SpriteAnimation 对象。可以通过 SpriteAnimation.spriteList 构造基于 Sprite 列表完成任务。其中 Sprite 列表可以根据文件名遍历得到:
: 游戏启动时需要将图片先加载进 loader 中,extraImages来收集零散的图片资源。

dart 复制代码
---->[lib/bricks/07/bricks_game.dart]----
final List<Sprite> spriteList = [];
for(int i=1;i<=14;i++){
  String name = 'TCSY_000${i.toString().padLeft(2,'0')}.png';
  spriteList.add(game.loader[name]);
}

SpriteAnimation sa = SpriteAnimation.spriteList(
    spriteList,
    stepTime: 0.05,
    loop: false
);

在世界中定义一个 showBoomParticle 方法,在指定的位置添加爆炸粒子。通过 add 方法加入 ParticleSystemComponent 组件。粒子系统构件会在lifespan 秒后自动移除,想要播放一次恰好移除,可以根据序列帧数量控制生命时长。

dart 复制代码
void showBoomParticle(Vector2 position) {
  /// 同上...
  add(
    ParticleSystemComponent(
        position: position,
        particle: SpriteAnimationParticle(
          lifespan: spriteList.length * 0.05,
          animation: sa,
        )),
  );
}

:默认情况下,序列帧的 stepTime*数量 会对和 lifespan 对齐,源码中可以看出,会通过生命时长修改动画的 stepTime。如果不希望对齐 ,可以将alignAnimationTime 置为 false:


最后一步是何处触发 showBoomParticle 方法。其实添加爆炸效果的时机很明确,我们也在之前封装过砖块销毁后统一操作的入口 PlayWorld#onBrickWillRemove :

dart 复制代码
void onBrickWillRemove(Brick brick) {
  Vector2 brickCenter = brick.absolutePosition + brick.size / 2;
  /// 略其他...
  showBoomParticle(brickCenter);
}

到这里,我们就基于 ParticleSystemComponent 实现了爆炸粒子的处理。下面继续看一下其他粒子的使用,进一步优化打砖块的视觉表现:


2. 小球的路径展示

接下来基于粒子系统,实现如下所示的小球轨迹的展示。思路其实很简单,在小球运行时不断生成圆形的粒子,在一定时长之后消失即可:

展示小球路径 两个小球

运动路径的粒子在小球中产生,使用在 Ball 中增加一个 showPathParticle 方法,在当前小球的位置增加一个维持 1 秒的圆形粒子 CircleParticle,作为 ParticleSystemComponent 中的粒子,加入到世界中:

dart 复制代码
---->[lib/bricks/07/heroes/ball.dart]----
void showPathParticle() {
  CircleParticle circleParticle = CircleParticle(
    radius: 4,
    lifespan: 1,
    paint: Paint()..color = Colors.white.withOpacity(0.2),
  );
  
  final ParticleSystemComponent psc = ParticleSystemComponent(
    position: position - Vector2(0, size.y / 2),
    particle: circleParticle,
  );
  game.world.add(psc);
}

然后,只要在小球更新的回调中不断添加粒子即可。由于 update 方法更新的频率很高,可以通过 _timeRecord 记录一下经过的时间,来限制 showPathParticle 触发的频率:

dart 复制代码
double _timeRecord = 0;
@override
void update(double dt) {
  super.update(dt);
  _timeRecord += dt;
  if (game.status == GameStatus.playing && _timeRecord > 0.06) {
    showPathParticle();
    _timeRecord = 0;
  }
  position += v * dt;
}

3. 小球死亡场景优化

如下所示,底部增加闪电网的序列帧动画表示小球死亡的底线(如下左图)。另外小球死亡时,展示死亡粒子动画(如下右图)。这样可以避免小球死亡时突然消失和出现。

底部闪电网 小球死亡粒子动画

底部的闪电网通过 DiedLine 组件展示:

  • [1]. 它继承自 SpriteAnimationComponent 构建,展示序列帧动画。
  • [2]. 它混入 CollisionCallbacks 支持碰撞检测,当 onCollisionStart 监听到碰撞者是小球时,将小球移除。
dart 复制代码
--->[lib/bricks/07/heroes/died_line.dart]---
class DiedLine extends SpriteAnimationComponent
    with HasGameRef<BricksGame>, CollisionCallbacks {
  @override
  FutureOr<void> onLoad() {
    final List<Sprite> spriteList = [];
    for (int i = 1; i <= 4; i++) {
      String name = 'lightning${i.toString().padLeft(2, '0')}.png';
      spriteList.add(game.loader[name]);
    }

    animation = SpriteAnimation.spriteList(
      spriteList,
      stepTime: 0.1,
      loop: true,
    );
    size = Vector2(kViewPort.width, 40);

    position = Vector2(0, kViewPort.height - height - 40);

    add(RectangleHitbox());
    return super.onLoad();
  }

  @override
  void onCollisionStart(
      Set<Vector2> intersectionPoints, PositionComponent other) {
    if (other is Ball) {
      other.removeFromParent();
    }
    super.onCollisionStart(intersectionPoints, other);
  }
}

最后小球在死亡时开启粒子动画,这和砖块被击碎时类似。在小球死亡后,通过 showDieParticle 方法添加:

dart 复制代码
--->[lib/bricks/07/heroes/ball.dart]---
void showDieParticle(Vector2 position) {
  final List<Sprite> spriteList = [];
  for (int i = 1; i <= 12; i++) {
    String name = 'died_${i.toString().padLeft(4, '0')}.png';
    spriteList.add(game.loader[name]);
  }
  SpriteAnimation sa = SpriteAnimation.spriteList(spriteList, stepTime: 0.1, loop: false);
  game.world.add(
    ParticleSystemComponent(
        position: position - Vector2(0, 80),
        particle: SpriteAnimationParticle(
          lifespan: spriteList.length * 0.05,
          animation: sa,
        )),
  );
}

二、Loading 界面与加载资源

一般游戏会在开始时先加载图片、配置数据数据等资源。展示 Loading 界面让玩家看到加载资源的进度,加载完成后才进入游戏。

Loading 20% Loading 80%

1.资源加载器

目前打砖块的资源加载主要在 BricksGame#onLoad 中,包括本地配置的初始化、加载关卡数据、加载图片的异步任务。我们可以将这些任务得到的数据,统一通过资源管理器来封装处理。

资源管理器将作为可持久化的数据资源仓库,BricksGame 依赖资源管理器访问数据。资源管理器由于在应用过程中始终存在,而且只需要单一实例,可以通过单例模式进行维护。如下所示,定义 ResManager 持有 GameConfigManagerGoodsManagerList<Level>TextureLoader 等需要异步加载的资源:

dart 复制代码
class ResManager {

  ResManager._();
  static ResManager instance = ResManager._();

  late SharedPreferences sp;
  late GameConfigManager configManager;
  GoodsManager goodsManager = GoodsManager();
  List<Level> _levels = [];
  List<Level> get levels => _levels;
  TextureLoader loader = TextureLoader();
  
  void load() async{
    //将BricksGame#onLoad加载数据逻辑迁移到这里。
  }

2.异步任务的加载进度

包括图片加载在内,目前有非常多的加载任务,如何在定义进度加载规则,以及通知外界加载的进度变化。是资源管理器的要点。我们可以使用 Stream 在每个异步任务完成后,通过 Stream 通知外界进度变化。当加载完毕,该流完成任务,进行关闭:

dart 复制代码
final StreamController<double> _progressCtrl = StreamController();

Stream<double> get loadStream => _progressCtrl.stream;

void load() async{
  sp = await SharedPreferences.getInstance();
  _progressCtrl.add(0.1);
  configManager = GameConfigManager(sp);
  configManager.loadConfig(sp);
  await loadLevels();
  _progressCtrl.add(0.2);
  await loader.load(
    'assets/images/break_bricks/break_bricks.json',
    'break_bricks/break_bricks.png',
    extra: extraImages,
    loadingCallBack: (total,cur){
      _progressCtrl.add(0.8*(cur/total));
    }
  );
  await goodsManager.loadGoods();
  _progressCtrl.add(1);
  _progressCtrl.close();
}

其中加载图片是确定个数的异步任务,我们可以通过回调函数来通知外界图片记载的进度变化。如下所示,定义 LoadProgressCallBack 函数类型进行表示:

java 复制代码
typedef LoadProgressCallBack = void Function(int total, int cur);

3.Loading 界面中加载进度使用

此时,应用打开后可以先展示 AssetsLoadingPage 界面。

AssetsLoadingPage 状态类初始化中触发资源管理器加载数据,并监听 loadStream 触发更新通知:

dart 复制代码
class AssetsLoadingPage extends StatefulWidget {
  const AssetsLoadingPage({Key? key}) : super(key: key);

  @override
  State<AssetsLoadingPage> createState() => _AssetsLoadingPageState();
}

class _AssetsLoadingPageState extends State<AssetsLoadingPage> {

  bool get isLoading => _progress != 1;
  double _progress = 0;

  @override
  void initState() {
    super.initState();
    ResManager.instance.load();
    ResManager.instance.loadStream.listen(_onLoading);
  }

当监听到进度变化时,如果进度小于 1 ,更新进度值,触发界面更新,来展示当前进度值。当进度为 1 时,触发跳转到游戏主界面。

dart 复制代码
void _onLoading(double progress) {
if (progress == 1) {
  Navigator.of(context).pushReplacement(
    MaterialPageRoute(
      builder: (_) => const PlatformAdapterApp(),
    ),
  );
}
_progress = progress;
setState(() {});
}

到这里,打砖块游戏的内容就基本结束。虽然是个小游戏,但麻雀虽小五脏俱全,其中整合了商店、背包、道具、金币、关卡、设置等游戏的常见的模块。大家也可以在此基础上进行进一步地拓展。


三、各平台应用打包

最后,我们将把打砖块的这个游戏在各个平台进行打包,这样就可以分享给其他人玩耍。Flutter应用可以产出原生级的Andriod、iOS、Windows、MacOS、Linux、web 六大主流平台应用。游戏自然也可以一套代码,完成六大平台的应用构建。


1. 构建 web 应用

web 平台应用,本身也是可以在各个平台通过浏览器访问的。其特点是无需安装包,可以直接通过浏览器访问。

flutter build web

该命令构建出的产物在 build/web 文件夹下,可以把它部署到服务器中,index.html 是它的访问入口:

没有服务器的朋友,可以将其作为网站部署到 gitee page 或者 github page 中。如下所示:

如下是部署到的访问链接, web 中游戏的性能和设备本身有关。这里桌面端也可以轻松地跑到 120 FPS:

toly1994328.gitee.io/game_box/


2. 构建 windows 应用

Flutter 可以将应用打包为 windows 平台的可执行文件,也就是 .exe

flutter build windows

该命令构建出的产物在 build/windows/runner/Release 文件夹下。将其压缩分享给其他人,就可以在 windows 操作系统中进行游戏。你也可以通过其他工具打包成安装文件,这点在以后单独介绍。

目前在我的小破本上可以跑到 140 + 的 FPS :


3. 构建 Macos 应用

Flutter 可以将应用打包为 MacOS 平台的可执行文件:

flutter build macos

该命令构建出的产物在 build/macos/Build/Products/Release 文件夹下。将其压缩分享给其他人,就可以在 Android 操作系统中安装进行游戏:

10年前的老 mac 本表示,我还能再战几年:也能维持在 60 FPS:


4. 构建 Android 应用

Flutter 可以将应用打包为 Android 平台的可执行文件,也就是 .apk。下面是打包 android-arm64 架构的 apk 包命令:

flutter build apk --target-platform android-arm64 --split-per-abi

该命令构建出的产物在 build/app/outputs/flutter-apk 文件夹下。将其压缩分享给其他人,就可以在 Android 操作系统中安装进行游戏:

安装后,四年前的 Android 旧设备,也能轻松保持 60 FPS。这表示 Flutter & Flame 的性能还有很大的压榨空间。


最后 iOS 和 Linux 平台类似,目前没有相关设备,暂时就不打包了。

iOS 打包应用: flutter build ios

Linux 打包应用: flutter build linux

到这里,打砖块游戏就告一段落,我们也得到了相关的成果。接下来,我们将继续前进,去往下一类别的游戏,进一步探讨 Flutter 在游戏方面的潜能。敬请期待 ~

相关推荐
找藉口是失败者的习惯26 分钟前
从传统到未来:Android XML布局 与 Jetpack Compose的全面对比
android·xml
Jinkey2 小时前
FlutterBasic - GetBuilder、Obx、GetX<Controller>、GetxController 有啥区别
android·flutter·ios
大白要努力!3 小时前
Android opencv使用Core.hconcat 进行图像拼接
android·opencv
懷淰メ4 小时前
PyQt飞机大战游戏(附下载地址)
开发语言·python·qt·游戏·pyqt·游戏开发·pyqt5
天空中的野鸟4 小时前
Android音频采集
android·音视频
小白也想学C5 小时前
Android 功耗分析(底层篇)
android·功耗
曙曙学编程5 小时前
初级数据结构——树
android·java·数据结构
Summer不秃6 小时前
Flutter之使用mqtt进行连接和信息传输的使用案例
前端·flutter
旭日猎鹰6 小时前
Flutter踩坑记录(二)-- GestureDetector+Expanded点击无效果
前端·javascript·flutter
sunly_6 小时前
Flutter:AnimatedSwitcher当子元素改变时,触发动画
flutter