持续创作,加速成长!这是我参与「掘金日新计划 · 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