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 在游戏方面的潜能。敬请期待 ~

相关推荐
HerayChen1 小时前
HbuildderX运行到手机或模拟器的Android App基座识别不到设备 mac
android·macos·智能手机
顾北川_野1 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
hairenjing11231 小时前
在 Android 手机上从SD 卡恢复数据的 6 个有效应用程序
android·人工智能·windows·macos·智能手机
小黄人软件2 小时前
android浏览器源码 可输入地址或关键词搜索 android studio 2024 可开发可改地址
android·ide·android studio
dj15402252032 小时前
group_concat配置影响程序出bug
android·bug
周全全2 小时前
MySQL报错解决:The user specified as a definer (‘root‘@‘%‘) does not exist
android·数据库·mysql
- 羊羊不超越 -3 小时前
App渠道来源追踪方案全面分析(iOS/Android/鸿蒙)
android·ios·harmonyos
wk灬丨4 小时前
Android Kotlin Flow 冷流 热流
android·kotlin·flow
千雅爸爸4 小时前
Android MVVM demo(使用DataBinding,LiveData,Fresco,RecyclerView,Room,ViewModel 完成)
android