router.dart
  1import 'package:flame/components.dart';
  2import 'package:flame/effects.dart';
  3import 'package:flame/events.dart';
  4import 'package:flame/game.dart';
  5import 'package:flame/geometry.dart';
  6import 'package:flame/rendering.dart';
  7import 'package:flutter/rendering.dart';
  8
  9class RouterGame extends FlameGame {
 10  late final RouterComponent router;
 11
 12  @override
 13  Future<void> onLoad() async {
 14    add(
 15      router = RouterComponent(
 16        routes: {
 17          'splash': Route(SplashScreenPage.new),
 18          'home': Route(StartPage.new),
 19          'level1': Route(Level1Page.new),
 20          'level2': Route(Level2Page.new),
 21          'pause': PauseRoute(),
 22        },
 23        initialRoute: 'splash',
 24      ),
 25    );
 26  }
 27}
 28
 29class SplashScreenPage extends Component
 30    with TapCallbacks, HasGameReference<RouterGame> {
 31  @override
 32  Future<void> onLoad() async {
 33    addAll([
 34      Background(const Color(0xff282828)),
 35      TextBoxComponent(
 36        text: '[Router demo]',
 37        textRenderer: TextPaint(
 38          style: const TextStyle(
 39            color: Color(0x66ffffff),
 40            fontSize: 16,
 41          ),
 42        ),
 43        align: Anchor.center,
 44        size: game.canvasSize,
 45      ),
 46    ]);
 47  }
 48
 49  @override
 50  bool containsLocalPoint(Vector2 point) => true;
 51
 52  @override
 53  void onTapUp(TapUpEvent event) => game.router.pushNamed('home');
 54}
 55
 56class StartPage extends Component with HasGameReference<RouterGame> {
 57  StartPage() {
 58    addAll([
 59      _logo = TextComponent(
 60        text: 'Syzygy',
 61        textRenderer: TextPaint(
 62          style: const TextStyle(
 63            fontSize: 64,
 64            color: Color(0xFFC8FFF5),
 65            fontWeight: FontWeight.w800,
 66          ),
 67        ),
 68        anchor: Anchor.center,
 69      ),
 70      _button1 = RoundedButton(
 71        text: 'Level 1',
 72        action: () => game.router.pushNamed('level1'),
 73        color: const Color(0xffadde6c),
 74        borderColor: const Color(0xffedffab),
 75      ),
 76      _button2 = RoundedButton(
 77        text: 'Level 2',
 78        action: () => game.router.pushNamed('level2'),
 79        color: const Color(0xffdebe6c),
 80        borderColor: const Color(0xfffff4c7),
 81      ),
 82    ]);
 83  }
 84
 85  late final TextComponent _logo;
 86  late final RoundedButton _button1;
 87  late final RoundedButton _button2;
 88
 89  @override
 90  void onGameResize(Vector2 size) {
 91    super.onGameResize(size);
 92    _logo.position = Vector2(size.x / 2, size.y / 3);
 93    _button1.position = Vector2(size.x / 2, _logo.y + 80);
 94    _button2.position = Vector2(size.x / 2, _logo.y + 140);
 95  }
 96}
 97
 98class Background extends Component {
 99  Background(this.color);
100  final Color color;
101
102  @override
103  void render(Canvas canvas) {
104    canvas.drawColor(color, BlendMode.srcATop);
105  }
106}
107
108class RoundedButton extends PositionComponent with TapCallbacks {
109  RoundedButton({
110    required this.text,
111    required this.action,
112    required Color color,
113    required Color borderColor,
114    super.position,
115    super.anchor = Anchor.center,
116  }) : _textDrawable = TextPaint(
117          style: const TextStyle(
118            fontSize: 20,
119            color: Color(0xFF000000),
120            fontWeight: FontWeight.w800,
121          ),
122        ).toTextPainter(text) {
123    size = Vector2(150, 40);
124    _textOffset = Offset(
125      (size.x - _textDrawable.width) / 2,
126      (size.y - _textDrawable.height) / 2,
127    );
128    _rrect = RRect.fromLTRBR(0, 0, size.x, size.y, Radius.circular(size.y / 2));
129    _bgPaint = Paint()..color = color;
130    _borderPaint = Paint()
131      ..style = PaintingStyle.stroke
132      ..strokeWidth = 2
133      ..color = borderColor;
134  }
135
136  final String text;
137  final void Function() action;
138  final TextPainter _textDrawable;
139  late final Offset _textOffset;
140  late final RRect _rrect;
141  late final Paint _borderPaint;
142  late final Paint _bgPaint;
143
144  @override
145  void render(Canvas canvas) {
146    canvas.drawRRect(_rrect, _bgPaint);
147    canvas.drawRRect(_rrect, _borderPaint);
148    _textDrawable.paint(canvas, _textOffset);
149  }
150
151  @override
152  void onTapDown(TapDownEvent event) {
153    scale = Vector2.all(1.05);
154  }
155
156  @override
157  void onTapUp(TapUpEvent event) {
158    scale = Vector2.all(1.0);
159    action();
160  }
161
162  @override
163  void onTapCancel(TapCancelEvent event) {
164    scale = Vector2.all(1.0);
165  }
166}
167
168abstract class SimpleButton extends PositionComponent with TapCallbacks {
169  SimpleButton(this._iconPath, {super.position}) : super(size: Vector2.all(40));
170
171  final Paint _borderPaint = Paint()
172    ..style = PaintingStyle.stroke
173    ..color = const Color(0x66ffffff);
174  final Paint _iconPaint = Paint()
175    ..style = PaintingStyle.stroke
176    ..color = const Color(0xffaaaaaa)
177    ..strokeWidth = 7;
178  final Path _iconPath;
179
180  void action();
181
182  @override
183  void render(Canvas canvas) {
184    canvas.drawRRect(
185      RRect.fromRectAndRadius(size.toRect(), const Radius.circular(8)),
186      _borderPaint,
187    );
188    canvas.drawPath(_iconPath, _iconPaint);
189  }
190
191  @override
192  void onTapDown(TapDownEvent event) {
193    _iconPaint.color = const Color(0xffffffff);
194  }
195
196  @override
197  void onTapUp(TapUpEvent event) {
198    _iconPaint.color = const Color(0xffaaaaaa);
199    action();
200  }
201
202  @override
203  void onTapCancel(TapCancelEvent event) {
204    _iconPaint.color = const Color(0xffaaaaaa);
205  }
206}
207
208class BackButton extends SimpleButton with HasGameReference<RouterGame> {
209  BackButton()
210      : super(
211          Path()
212            ..moveTo(22, 8)
213            ..lineTo(10, 20)
214            ..lineTo(22, 32)
215            ..moveTo(12, 20)
216            ..lineTo(34, 20),
217          position: Vector2.all(10),
218        );
219
220  @override
221  void action() => game.router.pop();
222}
223
224class PauseButton extends SimpleButton with HasGameReference<RouterGame> {
225  PauseButton()
226      : super(
227          Path()
228            ..moveTo(14, 10)
229            ..lineTo(14, 30)
230            ..moveTo(26, 10)
231            ..lineTo(26, 30),
232          position: Vector2(60, 10),
233        );
234  @override
235  void action() => game.router.pushNamed('pause');
236}
237
238class Level1Page extends Component {
239  @override
240  Future<void> onLoad() async {
241    final game = findGame()!;
242    addAll([
243      Background(const Color(0xbb2a074f)),
244      BackButton(),
245      PauseButton(),
246      Planet(
247        radius: 25,
248        color: const Color(0xfffff188),
249        position: game.size / 2,
250        children: [
251          Orbit(
252            radius: 110,
253            revolutionPeriod: 6,
254            planet: Planet(
255              radius: 10,
256              color: const Color(0xff54d7b1),
257              children: [
258                Orbit(
259                  radius: 25,
260                  revolutionPeriod: 5,
261                  planet: Planet(radius: 3, color: const Color(0xFFcccccc)),
262                ),
263              ],
264            ),
265          ),
266        ],
267      ),
268    ]);
269  }
270}
271
272class Level2Page extends Component {
273  @override
274  Future<void> onLoad() async {
275    final game = findGame()!;
276    addAll([
277      Background(const Color(0xff052b44)),
278      BackButton(),
279      PauseButton(),
280      Planet(
281        radius: 30,
282        color: const Color(0xFFFFFFff),
283        position: game.size / 2,
284        children: [
285          Orbit(
286            radius: 60,
287            revolutionPeriod: 5,
288            planet: Planet(radius: 10, color: const Color(0xffc9ce0d)),
289          ),
290          Orbit(
291            radius: 110,
292            revolutionPeriod: 10,
293            planet: Planet(
294              radius: 14,
295              color: const Color(0xfff32727),
296              children: [
297                Orbit(
298                  radius: 26,
299                  revolutionPeriod: 3,
300                  planet: Planet(radius: 5, color: const Color(0xffffdb00)),
301                ),
302                Orbit(
303                  radius: 35,
304                  revolutionPeriod: 4,
305                  planet: Planet(radius: 3, color: const Color(0xffdc00ff)),
306                ),
307              ],
308            ),
309          ),
310        ],
311      ),
312    ]);
313  }
314}
315
316class Planet extends PositionComponent {
317  Planet({
318    required this.radius,
319    required this.color,
320    super.position,
321    super.children,
322  }) : _paint = Paint()..color = color;
323
324  final double radius;
325  final Color color;
326  final Paint _paint;
327
328  @override
329  void render(Canvas canvas) {
330    canvas.drawCircle(Offset.zero, radius, _paint);
331  }
332}
333
334class Orbit extends PositionComponent {
335  Orbit({
336    required this.radius,
337    required this.planet,
338    required this.revolutionPeriod,
339    double initialAngle = 0,
340  })  : _paint = Paint()
341          ..style = PaintingStyle.stroke
342          ..color = const Color(0x888888aa),
343        _angle = initialAngle {
344    add(planet);
345  }
346
347  final double radius;
348  final double revolutionPeriod;
349  final Planet planet;
350  final Paint _paint;
351  double _angle;
352
353  @override
354  void render(Canvas canvas) {
355    canvas.drawCircle(Offset.zero, radius, _paint);
356  }
357
358  @override
359  void update(double dt) {
360    _angle += dt / revolutionPeriod * tau;
361    planet.position = Vector2(radius, 0)..rotate(_angle);
362  }
363}
364
365class PauseRoute extends Route {
366  PauseRoute() : super(PausePage.new, transparent: true);
367
368  @override
369  void onPush(Route? previousRoute) {
370    previousRoute!
371      ..stopTime()
372      ..addRenderEffect(
373        PaintDecorator.grayscale(opacity: 0.5)..addBlur(3.0),
374      );
375  }
376
377  @override
378  void onPop(Route nextRoute) {
379    nextRoute
380      ..resumeTime()
381      ..removeRenderEffect();
382  }
383}
384
385class PausePage extends Component
386    with TapCallbacks, HasGameReference<RouterGame> {
387  @override
388  Future<void> onLoad() async {
389    final game = findGame()!;
390    addAll([
391      TextComponent(
392        text: 'PAUSED',
393        position: game.canvasSize / 2,
394        anchor: Anchor.center,
395        children: [
396          ScaleEffect.to(
397            Vector2.all(1.1),
398            EffectController(
399              duration: 0.3,
400              alternate: true,
401              infinite: true,
402            ),
403          ),
404        ],
405      ),
406    ]);
407  }
408
409  @override
410  bool containsLocalPoint(Vector2 point) => true;
411
412  @override
413  void onTapUp(TapUpEvent event) => game.router.pop();
414}

This example app shows the use of the RouterComponent to move across multiple screens within the game. In addition, the “pause” button stops time and applies visual effects to the content of the page below it.

RouterComponent

The RouterComponent’s job is to manage navigation across multiple screens within the game. It is similar in spirit to Flutter’s Navigator class, except that it works with Flame components instead of Flutter widgets.

A typical game will usually consist of multiple pages: the splash screen, the starting menu page, the settings page, credits, the main game page, several pop-ups, etc. The router will organize all these destinations and allow you to transition between them.

Internally, the RouterComponent contains a stack of routes. When you request it to show a route, it will be placed on top of all other pages in the stack. Later you can pop() to remove the topmost page from the stack. The pages of the router are addressed by their unique names.

Each page in the router can be either transparent or opaque. If a page is opaque, then the pages below it in the stack are not rendered and do not receive pointer events (such as taps or drags). On the contrary, if a page is transparent, then the page below it will be rendered and receive events normally. Such transparent pages are useful for implementing modal dialogs, inventory or dialogue UIs, etc. If you want your route to be visually transparent but for the routes below it to not receive events, make sure to add a background component to your route that captures the events by using one of the event capturing mixins.

Usage example:

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 { ... }

Note

Use hide Route if any of your imported packages export another class called Route

eg: import 'package:flutter/material.dart' hide Route;

Route

The Route component holds information about the content of a particular page. Routes are mounted as children to the RouterComponent.

The main property of a Route is its builder – the function that creates the component with the content of its page.

In addition, the routes can be either transparent or opaque (default). An opaque prevents the route below it from rendering or receiving pointer events, a transparent route doesn’t. As a rule of thumb, declare the route opaque if it is full-screen, and transparent if it is supposed to cover only a part of the screen.

By default, routes maintain the state of the page component after being popped from the stack and the builder function is only called the first time a route is activated. Setting maintainState to false drops the page component after the route is popped from the route stack and the builder function is called each time the route is activated.

The current route can be replaced using pushReplacementNamed or pushReplacement. Each method simply executes pop on the current route and then pushNamed or pushRoute.

OverlayRoute

The OverlayRoute is a special route that allows adding game overlays via the router. These routes are transparent by default.

There are two constructors for the OverlayRoute. The first constructor requires a builder function that describes how the overlay’s widget is to be built. The second constructor can be used when the builder function was already specified within the GameWidget:

final router = RouterComponent(
  routes: {
    'ok-dialog': OverlayRoute(
      (context, game) {
        return Center(
          child: DecoratedContainer(...),
        );
      },
    ),  // OverlayRoute
    'confirm-dialog': OverlayRoute.existing(),
  },
);

Overlays that were defined within the GameWidget don’t even need to be declared within the routes map beforehand: the RouterComponent.pushOverlay() method can do it for you. Once an overlay route was registered, it can be activated either via the regular .pushNamed() method, or via the .pushOverlay() – the two methods will do exactly the same, though you can use the second one to make it more clear in your code that an overlay is being added instead of a regular route.

The current overlay can be replaced using pushReplacementOverlay. This method executes pushReplacementNamed or pushReplacement based on the status of the overlay being pushed.

ValueRoute

value_route.dart
  1import 'dart:math';
  2import 'dart:ui';
  3
  4import 'package:doc_flame_examples/router.dart';
  5import 'package:flame/components.dart';
  6import 'package:flame/events.dart';
  7import 'package:flame/game.dart';
  8import 'package:flame/geometry.dart';
  9
 10class ValueRouteExample extends FlameGame {
 11  late final RouterComponent router;
 12
 13  @override
 14  Future<void> onLoad() async {
 15    router = RouterComponent(
 16      routes: {'home': Route(HomePage.new)},
 17      initialRoute: 'home',
 18    );
 19    add(router);
 20  }
 21}
 22
 23class HomePage extends Component with HasGameReference<ValueRouteExample> {
 24  @override
 25  Future<void> onLoad() async {
 26    add(
 27      RoundedButton(
 28        text: 'Rate me',
 29        action: () async {
 30          final score = await game.router.pushAndWait(RateRoute());
 31          firstChild<TextComponent>()!.text = 'Score: $score';
 32        },
 33        color: const Color(0xff758f9a),
 34        borderColor: const Color(0xff60d5ff),
 35      )..position = game.size / 2,
 36    );
 37    add(
 38      TextComponent(
 39        text: 'Score: –',
 40        anchor: Anchor.topCenter,
 41        position: game.size / 2 + Vector2(0, 30),
 42        scale: Vector2.all(0.7),
 43      ),
 44    );
 45  }
 46}
 47
 48class RateRoute extends ValueRoute<int>
 49    with HasGameReference<ValueRouteExample> {
 50  RateRoute() : super(value: -1, transparent: true);
 51
 52  @override
 53  Component build() {
 54    final size = Vector2(250, 130);
 55    const radius = 18.0;
 56    final starGap = (size.x - 5 * 2 * radius) / 6;
 57    return DialogBackground(
 58      position: game.size / 2,
 59      size: size,
 60      children: [
 61        RoundedButton(
 62          text: 'Ok',
 63          position: position = Vector2(size.x / 2, 100),
 64          action: () {
 65            completeWith(
 66              descendants().where((c) => c is Star && c.active).length,
 67            );
 68          },
 69          color: const Color(0xFFFFFFFF),
 70          borderColor: const Color(0xFF000000),
 71        ),
 72        for (var i = 0; i < 5; i++)
 73          Star(
 74            value: i + 1,
 75            radius: radius,
 76            position: Vector2(starGap * (i + 1) + radius * (2 * i + 1), 40),
 77          ),
 78      ],
 79    );
 80  }
 81}
 82
 83class DialogBackground extends RectangleComponent with TapCallbacks {
 84  DialogBackground({super.position, super.size, super.children})
 85      : super(
 86          anchor: Anchor.center,
 87          paint: Paint()..color = const Color(0xee858585),
 88        );
 89}
 90
 91class Star extends PositionComponent with TapCallbacks {
 92  Star({required this.value, required this.radius, super.position})
 93      : super(size: Vector2.all(2 * radius), anchor: Anchor.center);
 94
 95  final int value;
 96  final double radius;
 97  final Path path = Path();
 98  final Paint borderPaint = Paint()
 99    ..style = PaintingStyle.stroke
100    ..color = const Color(0xffffe395)
101    ..strokeWidth = 2;
102  final Paint fillPaint = Paint()..color = const Color(0xffffe395);
103  bool active = false;
104
105  @override
106  Future<void> onLoad() async {
107    path.moveTo(radius, 0);
108    for (var i = 0; i < 5; i++) {
109      path.lineTo(
110        radius + 0.6 * radius * sin(tau / 5 * (i + 0.5)),
111        radius - 0.6 * radius * cos(tau / 5 * (i + 0.5)),
112      );
113      path.lineTo(
114        radius + radius * sin(tau / 5 * (i + 1)),
115        radius - radius * cos(tau / 5 * (i + 1)),
116      );
117    }
118    path.close();
119  }
120
121  @override
122  void render(Canvas canvas) {
123    if (active) {
124      canvas.drawPath(path, fillPaint);
125    }
126    canvas.drawPath(path, borderPaint);
127  }
128
129  @override
130  void onTapDown(TapDownEvent event) {
131    var on = true;
132    for (final star in parent!.children.whereType<Star>()) {
133      star.active = on;
134      if (star == this) {
135        on = false;
136      }
137    }
138  }
139}

A ValueRoute is a route that will return a value when it is eventually popped from the stack. Such routes can be used, for example, for dialog boxes that ask for some feedback from the user.

In order to use ValueRoutes, two steps are required:

  1. Create a route derived from the ValueRoute<T> class, where T is the type of the value that your route will return. Inside that class override the build() method to construct the component that will be displayed. The component should use the completeWith(value) method to pop the route and return the specified 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),
            ),
          ],
        );
      }
    }
    
  2. Display the route using Router.pushAndWait(), which returns a future that resolves with the value returned from the route.

    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
      }
    }