news 2026/9/15 16:26:46

Flame 游戏引擎 RouterComponent 完全指南:用堆栈式路由管理多页面游戏导航

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Flame 游戏引擎 RouterComponent 完全指南:用堆栈式路由管理多页面游戏导航

Flame 游戏引擎 RouterComponent 完全指南:用堆栈式路由管理多页面游戏导航

【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame

导读

大多数游戏都由多个"页面"构成:启动画面、主菜单、设置页、选关页、正式对局、确认弹窗等等。Flame 官方文档 router.md 所讲解的RouterComponent正是为此而生——它提供一套基于栈的导航模型,用 Flame 组件(而不是 Flutter widget)来管理页面切换。读完本文,你将掌握RouterComponent的完整 API(pushNamedpoppushReplacement等)、透明/不透明页面的差异、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(如TapCallbacksDragCallbacks等)把事件拦截下来。

源码层面,透明/不透明由两个方法共同实现(router_component.dart):

  • _adjustRoutesOrder():按栈索引重排各路由的priority,保证栈顶在上;
  • _adjustRoutesVisibility():从栈顶向下遍历,一旦遇到不透明路由,其下所有路由的isRendered均置为false

而 route.dart 中的renderTreecomponentsAtLocation都会先检查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)分别在入栈/出栈时被调用,可在子类中覆写以执行自定义逻辑(如暂停/恢复音乐)。

当前路由可以使用pushReplacementNamedpushReplacement替换。每个方法只是先对当前路由执行pop,然后分别执行pushNamedpushRoute

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时切换:若提供了cameracamera?.world = build(),否则要求游戏必须是FlameGame,替换其world属性;切换前会保存_previousWorld
  • onPop时恢复上一个世界:有相机则camera?.world = _previousWorld,否则恢复为_previousWorld ?? World()
  • 注意WorldRoute不支持渲染特效:调用addRenderEffect/removeRenderEffect会抛出UnimplementedError
  • 若既未提供相机、游戏又不是FlameGameonPush会触发断言失败——两种使用前提必须满足其一。

OverlayRoute:把游戏叠加层变成路由

OverlayRoute允许将游戏的 overlay(叠加层)当作普通路由一样添加/移除。这类路由默认透明

它与普通路由有两个本质区别(见 overlay_route.dart):

  1. overlay 总是渲染在游戏画布之上——即使你在 overlay 路由之上再压入普通路由,overlay 依然显示在最上层;
  2. overlay 路由的builder产出的是Flutter widget而非组件。

OverlayRoute有两种构造函数:

  1. OverlayRoute(OverlayBuilder builder):需要传入描述 widget 如何构建的 builder 函数;
  2. 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 的注册状态,内部执行pushReplacementNamedpushReplacement(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 中的routervalue_route两个内嵌示例应用(见 router.md)也是直接基于examples源码构建的。
  • 组件测试
    • router_component_test.dart——覆盖pushNamedpoppopUntilNamedcanPop、替换路由、路由工厂、onUnknownRoute及透明/不透明可见性等核心行为;
    • route_test.dart、world_route_test.dart、value_route_test.dart——分别验证Route的生命周期与渲染、WorldRoute的世界切换与恢复、ValueRoute的默认值与completeWith行为。

小结

RouterComponent把 FlutterNavigator的栈式导航思想带入了 Flame 的组件世界,让多页面游戏的组织变得清晰可控:

  • 普通页面Route+builder声明,靠transparent控制遮挡与事件穿透,靠maintainState控制状态保持;
  • 关卡/世界切换WorldRoute,注意"提供相机或使用FlameGame"的前提约束;
  • UI 叠加层OverlayRoute,可通过pushOverlay动态注册GameWidget中已有的 overlay;
  • 带返回值的对话框ValueRoute<T>+pushAndWaitcompleteWith或默认值机制保证 Future 总能完成。

这四种路由配合pushNamed/pop/pushReplacement*等导航 API,足以覆盖从主菜单、设置、选关到弹窗、叠加层的绝大多数游戏导航需求。

【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 16:26:15

RedHat 6.8安装Oracle 12c完整实战:从环境配置到建库全记录

干了这么多年Linux运维和Oracle DBA的活儿&#xff0c;最怕听到的一句话就是“帮我在服务器上装套Oracle”。Oracle安装本身并不难&#xff0c;难的是安装前那些绕不开的系统配置&#xff0c;还有各种依赖、权限、内核参数的坑。尤其是RedHat 6.8这种老系统&#xff0c;配Oracl…

作者头像 李华
网站建设 2026/9/15 16:25:23

阶次分析实战:角度重采样与变速工况振动频谱解析

简介&#xff1a;压缩包提供了一个基于MATLAB的阶次分析脚本&#xff0c;适用于旋转机械振动信号处理、状态监测与故障诊断等场景。核心功能包括转速信号读取、角度重采样以及阶次谱计算&#xff0c;配合steptdm相关思路&#xff0c;可将时域信号映射到角度域&#xff0c;帮助识…

作者头像 李华
网站建设 2026/9/15 16:25:14

STM32G070驱动CS5530高精度ADC:SPI时序、校准与噪声验证

简介&#xff1a;STM32G070与CS5530联合开发资料包&#xff0c;面向嵌入式开发者、电子爱好者及需要实现高精度数据采集的工程师。该资源整合了电路原理图与配套C语言驱动代码&#xff0c;覆盖MCU初始化、SPI/I2C通信、数据处理等关键环节&#xff0c;可帮助读者快速理解两款芯…

作者头像 李华
网站建设 2026/9/15 16:24:55

Xpay-3.1支付网关抽象层深度解析与安全接入指南

简介&#xff1a;Xpay-3.1版全开源无授权免签约支付源码&#xff0c;面向Java Web开发者及中小型技术团队&#xff0c;提供可商用、可深度定制的轻量级支付系统解决方案&#xff0c;有效降低接入第三方支付的开发门槛与合规成本。资源包共823个文件&#xff0c;涵盖108个JS前端…

作者头像 李华
网站建设 2026/9/15 16:24:13

NGOOS极益开源公益平台源码拆解:PHP部署与二次开发实战

简介&#xff1a;面向公益组织与PHP开发者的NGOOS极益开源公益平台源码包&#xff0c;可用于快速搭建捐赠管理、志愿者管理、活动组织与项目跟踪等功能的公益站点。压缩包共2000个文件&#xff0c;总大小102.47MB&#xff0c;包含XML配置、HTML页面、JS交互脚本、CSS样式、Mark…

作者头像 李华