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

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.

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/experimental.dart';
  8import 'package:flame/game.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 RectangleComponent(
 58      position: game.size / 2,
 59      size: size,
 60      anchor: Anchor.center,
 61      paint: Paint()..color = const Color(0xee858585),
 62      children: [
 63        RoundedButton(
 64          text: 'Ok',
 65          action: () {
 66            completeWith(
 67              descendants().where((c) => c is Star && c.active).length,
 68            );
 69          },
 70          color: const Color(0xFFFFFFFF),
 71          borderColor: const Color(0xFF000000),
 72        )..position = Vector2(size.x / 2, 100),
 73        for (var i = 0; i < 5; i++)
 74          Star(
 75            value: i + 1,
 76            radius: radius,
 77            position: Vector2(starGap * (i + 1) + radius * (2 * i + 1), 40),
 78          ),
 79      ],
 80    );
 81  }
 82}
 83
 84class Star extends PositionComponent with TapCallbacks {
 85  Star({required this.value, required this.radius, super.position})
 86      : super(size: Vector2.all(2 * radius), anchor: Anchor.center);
 87
 88  final int value;
 89  final double radius;
 90  final Path path = Path();
 91  final Paint borderPaint = Paint()
 92    ..style = PaintingStyle.stroke
 93    ..color = const Color(0xffffe395)
 94    ..strokeWidth = 2;
 95  final Paint fillPaint = Paint()..color = const Color(0xffffe395);
 96  bool active = false;
 97
 98  @override
 99  Future<void> onLoad() async {
100    path.moveTo(radius, 0);
101    for (var i = 0; i < 5; i++) {
102      path.lineTo(
103        radius + 0.6 * radius * sin(tau / 5 * (i + 0.5)),
104        radius - 0.6 * radius * cos(tau / 5 * (i + 0.5)),
105      );
106      path.lineTo(
107        radius + radius * sin(tau / 5 * (i + 1)),
108        radius - radius * cos(tau / 5 * (i + 1)),
109      );
110    }
111    path.close();
112  }
113
114  @override
115  void render(Canvas canvas) {
116    if (active) {
117      canvas.drawPath(path, fillPaint);
118    }
119    canvas.drawPath(path, borderPaint);
120  }
121
122  @override
123  void onTapDown(TapDownEvent event) {
124    var on = true;
125    for (final star in parent!.children.whereType<Star>()) {
126      star.active = on;
127      if (star == this) {
128        on = false;
129      }
130    }
131  }
132}
133
134const tau = pi * 2;

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