
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 23 天,点击查看活动详情
这是一套 张风捷特烈 出品的 Flutter&Flame 系列教程,发布于掘金社区。如果你在其他平台看到本文,可以根据对于链接移步到掘金中查看。因为文章可能会更新、修正,一切以掘金文章版本为准。本系列源码于 【toly_game】 ,如果本系列对你有所帮助,希望点赞支持,本系列文章一览:
未完待续 ~一般来说,休闲游戏并不会打开时立即进入游戏。会有一个菜单界面,让用户选择开始游戏,或通过设置按钮来打开配置界面,对游戏进行设置。而我们知道,Flame 的 “世界” 是通过 Ticker 不断触发更新的,但往往菜单是 静态 的,不需要一直更新。所以可以使用 Flutter 原生的组件来做菜单,再加上界面跳转也需要原生的路由。
其实本质上来说,Flame 所呈现的游戏界面也只是一个 Widget 而已,我们可以一视同仁。比如下面定义两个 GameWorld 组件,来表示游戏世界: 【22/01】
class GameWorld extends StatelessWidget {
const GameWorld({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GameWidget(game: TolyGame());
}
}
复制代码由于需要界面路由跳转,所以这里使用 MaterialApp ,其内部集成了路由体系。并且这里使用 navigatorKey ,便于在无上下文的情况下,获取导航状态。
class GameApp extends StatelessWidget {
const GameApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
navigatorKey: Keys.navKey,
themeMode: ThemeMode.dark,
darkTheme: ThemeData(brightness: Brightness.dark),
home: const MainMenu(),
);
}
}
class Keys {
Keys._();
static GlobalKey<NavigatorState> navKey = GlobalKey(debugLabel: 'navKey');
static NavigatorState? get navigator => navKey.currentState;
}
复制代码比如现在先给个简单的菜单界面,如下所示,一个名字文本,两个按钮:

如下所示,定义一个 Flutter 常规的 MainMenu 组件,对内容进行展示即可,代码如下。其中 开始 按钮通过 Keys 中的 navKey 获取导航栏状态,通过 pushReplacement 方法,跳转到 GameWorld 游戏界面,并将当前的 MainMenu 界面弹栈。
class MainMenu extends StatelessWidget {
const MainMenu({Key? key}) : super(key: key);
final TextStyle shadowStyle = const TextStyle(
fontSize: 30,
shadows: [Shadow(color: Colors.white,blurRadius: 10)]
);
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Wrap(
spacing: 20,
direction: Axis.vertical,
crossAxisAlignment: WrapCrossAlignment.center,
children: [
Text('Adventurer', style: shadowStyle,),
ElevatedButton(onPressed: _doPlay, child: const Text('开 始')),
ElevatedButton(onPressed: _toOptions, child: const Text('设 置'))
],
),
),
);
}
void _doPlay() {
Keys.navigator?.pushReplacement(
MaterialPageRoute(builder: (ctx) => const GameWorld()),
);
}
void _toOptions() {}
}
复制代码有人觉得默认字体可能并不是很好看,想要引入别的字体,但很多字体不可以商用。其实google_fonts 中提供了大量可以商用的字体,我们可以在 fonts.google.com/ 中进行挑选。

在某个字体的 License 中,可以瞄一眼,比如 Ma Shan Zheng 是允许在- 项目-印刷或数字,商业或其他场景使用的。

点击下载,在 OFL 中也可以看到,字体证书是 STL ,允许商用:

你可以通过 线上和 本地 两种方式来加载字体。线上加载,可以使用 google_fonts 的字体库,所有的字体样式都可以通过 GoogleFonts 类通过静态方法获取,使用时会自动下载字体。

线上的缺点是必须依赖网络,而且需要下载时间,对于很大的字体,首次下载时间比较长,突然的字体改变,体验并不是很好。可以把字体下载到本地,这样就没有延迟的风险,而且在没有网络的情况下也能使用,缺点是会增加应用体积,大家可以酌情选择。本地字体使用也非常方便,只需要引入,在 pubspec.yaml 的 fonts 节点下引入即可:

如果想要指定全局字体,可以在主题数据 ThemeData ,指定对应的 fontFamily :
class GameApp extends StatelessWidget {
const GameApp({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.dark,
darkTheme: ThemeData(
brightness: Brightness.dark,
fontFamily: 'ZCOOLKuaiLe' //<--- 指定字体
),
home: const MainMenu(),
);
}
}
复制代码这样就可以对应用的 Text 组件的字体进行统一设置,效果如下:

我们知道 Falme 中通过 GameLoop 维护一个持续触发的 Ticker 用于游戏的渲染更新。当然,游戏中也需要要有暂停和恢复的方法,如下案例中,通过按下空格键来切换游戏状态:

在 Game 类中提供了 resumeEngine 和 pauseEngine 两个方法,用于恢复和暂停游戏。此外 paused 属性可以得知游戏是否已经停止。由于 FlameGame 混入了 Game ,所以它有这些方法,如果在其他的构件中希望暂停或恢复游戏,可以通过混入 HasGameRef ,来得到 gameRef 对象触发这些方法。
void toggleGameState(){
if(paused){
resumeEngine();
}else{
pauseEngine();
}
}
复制代码有时我们有显示浮层的需求,比如暂停游戏时,显示暂停面板。不然用户不小心碰到了暂停键,有可能不知所措,显示一个浮层界面可以更好的引导交互。如下所示,在点击空格键时,显示浮层:代码详见 【22/02】

使用浮层需要三步:
内容组件这里和开始菜单类似,就不贴代码了,详见源码。在其中定义了 Game 成员,在构造方法中初始化,这是为了方便在 PauseMenu 的继续按钮触发时,调用引擎的相关方法,继续游戏。当然,你也可以把事件回调出去,让使用者处理,其实都差不多,酌情考量即可。
另外,定义了一个 menuId 的静态常量,为了方便标识这个菜单,而不是在每处使用时,都写一个死的字符串。

GameWidget 的 overlayBuilderMap 参数指定 浮层id 和 组件内容 的映射关系:

浮层id 开启或隐藏浮层,其中 overlays 是 Game 中的公开成员:

本文介绍了,如何在 Flame 游戏中,让 Flutter 原生的组件发挥价值。其实 Flame 是在 Flutter 中的,你可以随时随地,使用 Flutter 中的任何知识。并没有必要把 Flame 和 Flutter 进行割裂,Flutter 的基础设施仍然可以使用,比如国际化、主题切换、状态管理等等。
@张风捷特烈 2022.06.17 未允禁转我的 掘金主页 : 张风捷特烈我的 B站主页 : 张风捷特烈我的 github 主页 : toly1994328