
Flame 游戏引擎 RouterComponent 完全指南用堆栈式路由管理多页面游戏导航【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame导读大多数游戏都由多个页面构成启动画面、主菜单、设置页、选关页、正式对局、确认弹窗等等。Flame 官方文档 router.md 所讲解的RouterComponent正是为此而生——它提供一套基于栈的导航模型用 Flame 组件而不是 Flutter widget来管理页面切换。读完本文你将掌握RouterComponent的完整 APIpushNamed、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.dartrouteFactories一组能动态解析路由的函数。形如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——不渲染时直接返回空迭代因此既不会绘制、也不会参与命中检测这就是不透明路由挡住下方一切的实现原理。导航操作 APIRouterComponent提供的导航方法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)替换当前叠加层路由pushAndWaitT(ValueRouteT route)压入路由并返回一个 Future等待其携带返回值弹出pop()弹出栈顶路由不允许弹出栈中最后一个路由会触发 assertpopUntilNamed(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.0route.dart。渲染特效render effectaddRenderEffect(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 Futurevoid onLoad() async { add(BackgroundComponent()); add(PlayerComponent()); } } class MyWorld2 extends World { override Futurevoid 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属性切换前会保存_previousWorldonPop时恢复上一个世界有相机则camera?.world _previousWorld否则恢复为_previousWorld ?? World()注意WorldRoute不支持渲染特效调用addRenderEffect/removeRenderEffect会抛出UnimplementedError若既未提供相机、游戏又不是FlameGameonPush会触发断言失败——两种使用前提必须满足其一。OverlayRoute把游戏叠加层变成路由OverlayRoute允许将游戏的 overlay叠加层当作普通路由一样添加/移除。这类路由默认透明。它与普通路由有两个本质区别见 overlay_route.dartoverlay 总是渲染在游戏画布之上——即使你在 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或pushReplacementrouter_component.dart。源码行为build()时若携带 builder会将其注册进game.overlays的入口表onPush调用game.overlays.add(name)onPop调用game.overlays.remove(name)——这正是通过路由管理 overlay 生命周期的底层机制。ValueRoute能返回值的路由对话框利器ValueRoute是一种在出栈时会返回一个值的路由非常适合用于向用户征求反馈的对话框。使用ValueRoute需要两步第一步创建派生自ValueRouteT的类T是路由将返回的值的类型。在该类中覆写build()方法构建要展示的组件组件内部通过completeWith(value)弹出路由并返回指定值class YesNoDialog extends ValueRoutebool { 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解析为该路由返回的值Futurevoid 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.dartValueRouteT是抽象类内部持有CompleterT因此必须派生使用不能直接传 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行为。小结RouterComponent把 FlutterNavigator的栈式导航思想带入了 Flame 的组件世界让多页面游戏的组织变得清晰可控普通页面用Routebuilder声明靠transparent控制遮挡与事件穿透靠maintainState控制状态保持关卡/世界切换用WorldRoute注意提供相机或使用FlameGame的前提约束UI 叠加层用OverlayRoute可通过pushOverlay动态注册GameWidget中已有的 overlay带返回值的对话框用ValueRouteTpushAndWaitcompleteWith或默认值机制保证 Future 总能完成。这四种路由配合pushNamed/pop/pushReplacement*等导航 API足以覆盖从主菜单、设置、选关到弹窗、叠加层的绝大多数游戏导航需求。【免费下载链接】flameA Flutter based game engine.项目地址: https://gitcode.com/GitHub_Trending/fl/flame创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考