本文为稀土掘金技术社区首发签约文章,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
持有 GameConfigManager
、GoodsManager
、List<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:
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 在游戏方面的潜能。敬请期待 ~