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