Flame 游戏引擎 RouterComponent 完全指南:用堆栈式路由管理多页面游戏导航
【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame
导读
大多数游戏都由多个"页面"构成:启动画面、主菜单、设置页、选关页、正式对局、确认弹窗等等。Flame 官方文档 router.md 所讲解的RouterComponent正是为此而生——它提供一套基于栈的导航模型,用 Flame 组件(而不是 Flutter widget)来管理页面切换。读完本文,你将掌握RouterComponent的完整 API(pushNamed、pop、pushReplacement等)、透明/不透明页面的差异、WorldRoute(世界切换)、OverlayRoute(游戏叠加层)与ValueRoute(带返回值的对话框路由)四种路由类型的正确用法,并能从源码层面理解其底层机制。
RouterComponent:游戏内的堆栈式导航器
游戏通常不止一个画面:主菜单、设置页面、对局画面、弹窗……手工管理这些画面之间的切换很容易变得混乱。RouterComponent通过一套堆栈式(stack-based)导航模型解决这个问题,其设计思路与 Flutter 的Navigator类似,区别在于它操作的是Flame 组件而非 Flutter widget(参考 Flutter Navigator 文档 可对照理解其概念)。
一个典型游戏往往包含多个页面:启动画面(splash)、起始菜单页、设置页、制作人员名单(credits)、主游戏页面、若干弹窗等。路由(router)负责组织所有这些目的地,并允许你在它们之间切换。
内部机制:RouterComponent内部维护一个路由栈。当你请求展示某个路由时,它会压入栈顶;随后你可以调用pop()移除栈顶页面。每个页面通过**唯一名称(unique name)**寻址。
在源码 router_component.dart 中可以看到,栈中路由会被挂载为组件;路由被 pop 时会被卸载。此外,路由可以停止或放慢其控制页面的时间,也可以通过装饰器(decorator)对页面施加视觉效果。
基本用法示例
class MyGame extends FlameGame { late final RouterComponent router; @override void onLoad() { add( router = RouterComponent( routes: { 'home': Route(HomePage.new), 'level-selector': Route(LevelSelectorPage.new), 'settings': Route(SettingsPage.new, transparent: true), 'pause': PauseRoute(), 'confirm-dialog': OverlayRoute.existing(), }, initialRoute: 'home', ), ); } } class PauseRoute extends Route { ... }注意(名称冲突):如果导入的某个包也导出了名为
Route的类,请使用hide语法隐藏它,例如:import 'package:flutter/material.dart' hide Route;
从源码看,RouterComponent的构造还支持三个扩展参数(router_component.dart):
routeFactories:一组能动态解析路由的函数。形如"prefix/arg"的路由名会调用名为prefix的工厂并传入参数arg,生成的路由会被缓存进主routes表;onUnknownRoute:当路由名既不在routes表、也无法由routeFactories解析时兜底调用的工厂函数(返回的路由不会被缓存);priority:默认值为0x7fffffff,确保路由组件始终处于高优先级渲染层。
透明与不透明页面(transparent / opaque)
路由中的每个页面要么透明,要么不透明:
- 不透明(opaque,默认):栈中位于它下方的页面不再渲染,也不接收指针事件(如点击、拖拽)。
- 透明(transparent):下方页面照常渲染并正常接收事件,非常适合实现模态对话框、物品栏(inventory)、对话 UI等场景。
关键注意点:如果你希望路由视觉上透明、但下方路由不要接收事件,就必须在该路由中加入一个背景组件,通过事件捕获 mixins(如TapCallbacks、DragCallbacks等)把事件拦截下来。
源码层面,透明/不透明由两个方法共同实现(router_component.dart):
_adjustRoutesOrder():按栈索引重排各路由的priority,保证栈顶在上;_adjustRoutesVisibility():从栈顶向下遍历,一旦遇到不透明路由,其下所有路由的isRendered均置为false。
而 route.dart 中的renderTree与componentsAtLocation都会先检查isRendered——不渲染时直接返回空迭代,因此既不会绘制、也不会参与命中检测,这就是"不透明路由挡住下方一切"的实现原理。
导航操作 API
RouterComponent提供的导航方法(router_component.dart)包括:
| 方法 | 说明 |
|---|---|
pushNamed(String name, {bool replace}) | 按名称将路由压入栈顶;若该路由已在栈中则移动到栈顶;已在栈顶则什么都不做 |
pushRoute(Route route, {String? name, bool replace}) | 直接压入一个路由实例,可附带name并缓存到routes表 |
pushReplacementNamed(String name) | 先 pop 当前路由,再 pushNamed 新路由 |
pushReplacement(Route route, {String? name}) | 先 pop 当前路由,再 pushRoute 新路由 |
pushOverlay(String name) | 压入一个叠加层路由(详见 OverlayRoute 一节) |
pushReplacementOverlay(String name) | 替换当前叠加层路由 |
pushAndWait<T>(ValueRoute<T> route) | 压入路由并返回一个 Future,等待其携带返回值弹出 |
pop() | 弹出栈顶路由;不允许弹出栈中最后一个路由(会触发 assert) |
popUntilNamed(String name) | 连续 pop 直到指定名称的路由成为栈顶 |
popRoute(Route route) | 连续 pop 直到指定路由被移除 |
canPop() | 栈中路由数 > 1 时返回true |
另外还提供了两个查询属性:currentRoute(栈顶路由)和previousRoute(栈顶之下的路由,若存在)。pushReplacementNamed/pushReplacement本质上就是"先对当前路由执行 pop,再执行 pushNamed / pushRoute"(见 route 文档 与 源码实现)。
Route:页面的内容载体
Route组件持有某个页面的内容信息。路由作为RouterComponent的子组件被挂载。
Route的核心属性是builder——一个负责创建页面内容组件的函数。
此外,路由可以是透明或不透明的(默认不透明)。经验法则:全屏页面声明为不透明;只覆盖屏幕一部分的页面声明为透明。
Route还支持以下高级能力:
- 状态保持(
maintainState):默认情况下,路由被 pop 后仍保留页面组件状态,builder只在路由首次激活时调用一次。若将maintainState设为false,则路由被 pop 时页面组件被丢弃,且每次激活都会重新调用builder。对应源码见 route.dart 的didPop:!maintainState时执行_page?.removeFromParent()并置空。 - 时间控制:
stopTime()将页面的timeScale置为 0,完全停止页面及其后代的 update(页面仍会渲染,生命周期事件仍会处理);resumeTime()恢复为 1.0(route.dart)。 - 渲染特效(render effect):
addRenderEffect(Decorator)可为整页叠加渲染特效(如整页模糊、灰度、色调),removeRenderEffect()移除;渲染时通过_renderEffect.applyChain应用到页面绘制链上(route.dart)。注意渲染特效与普通Effect是两回事。 - 加载页(loading builder):构造时可传入
_loadingBuilder,首次激活时先显示加载页组件,待页面加载完成后再切换过去(route.dart)。 - 生命周期回调:
onPush(Route? previousRoute)与onPop(Route nextRoute)分别在入栈/出栈时被调用,可在子类中覆写以执行自定义逻辑(如暂停/恢复音乐)。
当前路由可以使用pushReplacementNamed或pushReplacement替换。每个方法只是先对当前路由执行pop,然后分别执行pushNamed或pushRoute。
WorldRoute:通过路由切换游戏世界
WorldRoute是一种特殊路由,允许通过路由系统设置当前激活的游戏世界(world)。它非常适合用来实现"以独立 world 形式组织的关卡切换"。
默认行为:
- 激活时,
WorldRoute会用新世界替换当前世界; - 默认在出栈后保持世界状态;若希望每次激活都重建世界,设置
maintainState: false。
如果你没有使用内置的CameraComponent,可以在构造函数中显式传入希望使用的相机:
final router = RouterComponent( routes: { 'level1': WorldRoute(MyWorld1.new), 'level2': WorldRoute(MyWorld2.new, maintainState: false), }, ); class MyWorld1 extends World { @override Future<void> onLoad() async { add(BackgroundComponent()); add(PlayerComponent()); } } class MyWorld2 extends World { @override Future<void> onLoad() async { add(BackgroundComponent()); add(PlayerComponent()); add(EnemyComponent()); } }源码细节(world_route.dart):
- 构造签名
WorldRoute(this.builder, {this.camera, super.maintainState}); build()中依据maintainState决定是缓存world ??= builder()还是每次都world = builder()重建;onPush时切换:若提供了camera则camera?.world = build(),否则要求游戏必须是FlameGame,替换其world属性;切换前会保存_previousWorld;onPop时恢复上一个世界:有相机则camera?.world = _previousWorld,否则恢复为_previousWorld ?? World();- 注意
WorldRoute不支持渲染特效:调用addRenderEffect/removeRenderEffect会抛出UnimplementedError; - 若既未提供相机、游戏又不是
FlameGame,onPush会触发断言失败——两种使用前提必须满足其一。
OverlayRoute:把游戏叠加层变成路由
OverlayRoute允许将游戏的 overlay(叠加层)当作普通路由一样添加/移除。这类路由默认透明。
它与普通路由有两个本质区别(见 overlay_route.dart):
- overlay 总是渲染在游戏画布之上——即使你在 overlay 路由之上再压入普通路由,overlay 依然显示在最上层;
- overlay 路由的
builder产出的是Flutter widget而非组件。
OverlayRoute有两种构造函数:
OverlayRoute(OverlayBuilder builder):需要传入描述 widget 如何构建的 builder 函数;OverlayRoute.existing():当 builder 已经在GameWidget中声明过时使用。
final router = RouterComponent( routes: { 'ok-dialog': OverlayRoute( (context, game) { return Center( child: DecoratedContainer(...), ); }, ), // OverlayRoute 'confirm-dialog': OverlayRoute.existing(), }, );其中OverlayBuilder的类型定义为Widget Function(BuildContext context, Game game)(overlay_route.dart)。
动态注册与激活:在GameWidget中定义过的 overlay 甚至无需预先在routes表中声明——调用RouterComponent.pushOverlay()即可替你完成注册。一旦 overlay 路由注册完成,既可以通过常规的.pushNamed()激活,也可以使用.pushOverlay();两者效果完全相同,后者只是让代码意图更明确("我在添加一个 overlay 而不是普通路由")。
pushOverlay的实现(router_component.dart)会先检查名称是否已注册:若已注册则断言它是OverlayRoute并执行pushNamed;否则现场创建OverlayRoute.existing()并压栈。
当前 overlay 可以使用pushReplacementOverlay替换:该方法依据被压入 overlay 的注册状态,内部执行pushReplacementNamed或pushReplacement(router_component.dart)。
源码行为:build()时若携带 builder,会将其注册进game.overlays的入口表;onPush调用game.overlays.add(name),onPop调用game.overlays.remove(name)——这正是"通过路由管理 overlay 生命周期"的底层机制。
ValueRoute:能返回值的路由(对话框利器)
ValueRoute是一种在出栈时会返回一个值的路由,非常适合用于"向用户征求反馈"的对话框。
使用ValueRoute需要两步:
第一步:创建派生自ValueRoute<T>的类,T是路由将返回的值的类型。在该类中覆写build()方法构建要展示的组件;组件内部通过completeWith(value)弹出路由并返回指定值:
class YesNoDialog extends ValueRoute<bool> { YesNoDialog(this.text) : super(value: false); final String text; @override Component build() { return PositionComponent( children: [ RectangleComponent(), TextComponent(text: text), Button( text: 'Yes', action: () => completeWith(true), ), Button( text: 'No', action: () => completeWith(false), ), ], ); } }第二步:用Router.pushAndWait()展示路由,它会返回一个 Future,解析为该路由返回的值:
Future<void> foo() async { final result = await game.router.pushAndWait(YesNoDialog('Are you sure?')); if (result) { // ... the user is sure } else { // ... the user was not so sure } }源码细节(value_route.dart):
ValueRoute<T>是抽象类,内部持有Completer<T>,因此必须派生使用,不能直接传 builder;构造参数value作为默认返回值;completeWith(value)先完成 Completer,再调用parent.popRoute(this)弹出自身;complete()便捷方法等价于completeWith(_defaultValue);- 若路由在未调用
completeWith的情况下被弹出(例如用户点了对话框外部),didPop会自动以默认值完成 Future——保证await pushAndWait(...)永远能拿到结果而不挂起。
深入验证:仓库中的示例与测试
Flame 仓库为路由系统提供了可运行的示例与完整测试,便于你对照验证本文所述行为:
- 运行示例:
examples/lib/stories/router/router_world_example.dart演示了RouterComponent结合WorldRoute的关卡切换场景;doc 中的router与value_route两个内嵌示例应用(见 router.md)也是直接基于examples源码构建的。 - 组件测试:
- router_component_test.dart——覆盖
pushNamed、pop、popUntilNamed、canPop、替换路由、路由工厂、onUnknownRoute及透明/不透明可见性等核心行为; - route_test.dart、world_route_test.dart、value_route_test.dart——分别验证
Route的生命周期与渲染、WorldRoute的世界切换与恢复、ValueRoute的默认值与completeWith行为。
- router_component_test.dart——覆盖
小结
RouterComponent把 FlutterNavigator的栈式导航思想带入了 Flame 的组件世界,让多页面游戏的组织变得清晰可控:
- 普通页面用
Route+builder声明,靠transparent控制遮挡与事件穿透,靠maintainState控制状态保持; - 关卡/世界切换用
WorldRoute,注意"提供相机或使用FlameGame"的前提约束; - UI 叠加层用
OverlayRoute,可通过pushOverlay动态注册GameWidget中已有的 overlay; - 带返回值的对话框用
ValueRoute<T>+pushAndWait,completeWith或默认值机制保证 Future 总能完成。
这四种路由配合pushNamed/pop/pushReplacement*等导航 API,足以覆盖从主菜单、设置、选关到弹窗、叠加层的绝大多数游戏导航需求。
【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考