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. Route
s 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¶
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 ValueRoute
s, two steps are required:
Create a route derived from the
ValueRoute<T>
class, whereT
is the type of the value that your route will return. Inside that class override thebuild()
method to construct the component that will be displayed. The component should use thecompleteWith(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), ), ], ); } }
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 } }