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