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.anchor = Anchor.center,
115 }) : _textDrawable = TextPaint(
116 style: const TextStyle(
117 fontSize: 20,
118 color: Color(0xFF000000),
119 fontWeight: FontWeight.w800,
120 ),
121 ).toTextPainter(text) {
122 size = Vector2(150, 40);
123 _textOffset = Offset(
124 (size.x - _textDrawable.width) / 2,
125 (size.y - _textDrawable.height) / 2,
126 );
127 _rrect = RRect.fromLTRBR(0, 0, size.x, size.y, Radius.circular(size.y / 2));
128 _bgPaint = Paint()..color = color;
129 _borderPaint = Paint()
130 ..style = PaintingStyle.stroke
131 ..strokeWidth = 2
132 ..color = borderColor;
133 }
134
135 final String text;
136 final void Function() action;
137 final TextPainter _textDrawable;
138 late final Offset _textOffset;
139 late final RRect _rrect;
140 late final Paint _borderPaint;
141 late final Paint _bgPaint;
142
143 @override
144 void render(Canvas canvas) {
145 canvas.drawRRect(_rrect, _bgPaint);
146 canvas.drawRRect(_rrect, _borderPaint);
147 _textDrawable.paint(canvas, _textOffset);
148 }
149
150 @override
151 void onTapDown(TapDownEvent event) {
152 scale = Vector2.all(1.05);
153 }
154
155 @override
156 void onTapUp(TapUpEvent event) {
157 scale = Vector2.all(1.0);
158 action();
159 }
160
161 @override
162 void onTapCancel(TapCancelEvent event) {
163 scale = Vector2.all(1.0);
164 }
165}
166
167abstract class SimpleButton extends PositionComponent with TapCallbacks {
168 SimpleButton(this._iconPath, {super.position}) : super(size: Vector2.all(40));
169
170 final Paint _borderPaint = Paint()
171 ..style = PaintingStyle.stroke
172 ..color = const Color(0x66ffffff);
173 final Paint _iconPaint = Paint()
174 ..style = PaintingStyle.stroke
175 ..color = const Color(0xffaaaaaa)
176 ..strokeWidth = 7;
177 final Path _iconPath;
178
179 void action();
180
181 @override
182 void render(Canvas canvas) {
183 canvas.drawRRect(
184 RRect.fromRectAndRadius(size.toRect(), const Radius.circular(8)),
185 _borderPaint,
186 );
187 canvas.drawPath(_iconPath, _iconPaint);
188 }
189
190 @override
191 void onTapDown(TapDownEvent event) {
192 _iconPaint.color = const Color(0xffffffff);
193 }
194
195 @override
196 void onTapUp(TapUpEvent event) {
197 _iconPaint.color = const Color(0xffaaaaaa);
198 action();
199 }
200
201 @override
202 void onTapCancel(TapCancelEvent event) {
203 _iconPaint.color = const Color(0xffaaaaaa);
204 }
205}
206
207class BackButton extends SimpleButton with HasGameReference<RouterGame> {
208 BackButton()
209 : super(
210 Path()
211 ..moveTo(22, 8)
212 ..lineTo(10, 20)
213 ..lineTo(22, 32)
214 ..moveTo(12, 20)
215 ..lineTo(34, 20),
216 position: Vector2.all(10),
217 );
218
219 @override
220 void action() => game.router.pop();
221}
222
223class PauseButton extends SimpleButton with HasGameReference<RouterGame> {
224 PauseButton()
225 : super(
226 Path()
227 ..moveTo(14, 10)
228 ..lineTo(14, 30)
229 ..moveTo(26, 10)
230 ..lineTo(26, 30),
231 position: Vector2(60, 10),
232 );
233 @override
234 void action() => game.router.pushNamed('pause');
235}
236
237class Level1Page extends Component {
238 @override
239 Future<void> onLoad() async {
240 final game = findGame()!;
241 addAll([
242 Background(const Color(0xbb2a074f)),
243 BackButton(),
244 PauseButton(),
245 Planet(
246 radius: 25,
247 color: const Color(0xfffff188),
248 position: game.size / 2,
249 children: [
250 Orbit(
251 radius: 110,
252 revolutionPeriod: 6,
253 planet: Planet(
254 radius: 10,
255 color: const Color(0xff54d7b1),
256 children: [
257 Orbit(
258 radius: 25,
259 revolutionPeriod: 5,
260 planet: Planet(radius: 3, color: const Color(0xFFcccccc)),
261 ),
262 ],
263 ),
264 ),
265 ],
266 ),
267 ]);
268 }
269}
270
271class Level2Page extends Component {
272 @override
273 Future<void> onLoad() async {
274 final game = findGame()!;
275 addAll([
276 Background(const Color(0xff052b44)),
277 BackButton(),
278 PauseButton(),
279 Planet(
280 radius: 30,
281 color: const Color(0xFFFFFFff),
282 position: game.size / 2,
283 children: [
284 Orbit(
285 radius: 60,
286 revolutionPeriod: 5,
287 planet: Planet(radius: 10, color: const Color(0xffc9ce0d)),
288 ),
289 Orbit(
290 radius: 110,
291 revolutionPeriod: 10,
292 planet: Planet(
293 radius: 14,
294 color: const Color(0xfff32727),
295 children: [
296 Orbit(
297 radius: 26,
298 revolutionPeriod: 3,
299 planet: Planet(radius: 5, color: const Color(0xffffdb00)),
300 ),
301 Orbit(
302 radius: 35,
303 revolutionPeriod: 4,
304 planet: Planet(radius: 3, color: const Color(0xffdc00ff)),
305 ),
306 ],
307 ),
308 ),
309 ],
310 ),
311 ]);
312 }
313}
314
315class Planet extends PositionComponent {
316 Planet({
317 required this.radius,
318 required this.color,
319 super.position,
320 super.children,
321 }) : _paint = Paint()..color = color;
322
323 final double radius;
324 final Color color;
325 final Paint _paint;
326
327 @override
328 void render(Canvas canvas) {
329 canvas.drawCircle(Offset.zero, radius, _paint);
330 }
331}
332
333class Orbit extends PositionComponent {
334 Orbit({
335 required this.radius,
336 required this.planet,
337 required this.revolutionPeriod,
338 double initialAngle = 0,
339 }) : _paint = Paint()
340 ..style = PaintingStyle.stroke
341 ..color = const Color(0x888888aa),
342 _angle = initialAngle {
343 add(planet);
344 }
345
346 final double radius;
347 final double revolutionPeriod;
348 final Planet planet;
349 final Paint _paint;
350 double _angle;
351
352 @override
353 void render(Canvas canvas) {
354 canvas.drawCircle(Offset.zero, radius, _paint);
355 }
356
357 @override
358 void update(double dt) {
359 _angle += dt / revolutionPeriod * tau;
360 planet.position = Vector2(radius, 0)..rotate(_angle);
361 }
362}
363
364class PauseRoute extends Route {
365 PauseRoute() : super(PausePage.new, transparent: true);
366
367 @override
368 void onPush(Route? previousRoute) {
369 previousRoute!
370 ..stopTime()
371 ..addRenderEffect(
372 PaintDecorator.grayscale(opacity: 0.5)..addBlur(3.0),
373 );
374 }
375
376 @override
377 void onPop(Route nextRoute) {
378 nextRoute
379 ..resumeTime()
380 ..removeRenderEffect();
381 }
382}
383
384class PausePage extends Component
385 with TapCallbacks, HasGameReference<RouterGame> {
386 @override
387 Future<void> onLoad() async {
388 final game = findGame()!;
389 addAll([
390 TextComponent(
391 text: 'PAUSED',
392 position: game.canvasSize / 2,
393 anchor: Anchor.center,
394 children: [
395 ScaleEffect.to(
396 Vector2.all(1.1),
397 EffectController(
398 duration: 0.3,
399 alternate: true,
400 infinite: true,
401 ),
402 ),
403 ],
404 ),
405 ]);
406 }
407
408 @override
409 bool containsLocalPoint(Vector2 point) => true;
410
411 @override
412 void onTapUp(TapUpEvent event) => game.router.pop();
413}
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. 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 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}
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 } }