Flutter Intermediate Lesson 113 of 135 3 min read
Overlays in Flame: Main Menu, Pause and Game Over with Flutter Widgets
Put Flutter widgets on top of a Flame game with overlays: main menu, pause button, game over screen and restart, in a complete runnable game.
On this page
Menus, buttons, dialogs and scores are easier to build with normal Flutter widgets than inside the game. Flame’s overlays let you show any widget on top of the game and turn it on and off from game code.
How overlays work
- Give
GameWidgetanoverlayBuilderMap: names mapped to widget builders. - Show one with
game.overlays.add('Name'), hide it withgame.overlays.remove('Name'). - Use
initialActiveOverlaysfor the ones shown at the start (like the main menu).
A complete game with menus
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
void main() {
runApp(MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: GameWidget<TapGame>(
game: TapGame(),
overlayBuilderMap: {
'MainMenu': (context, game) => MainMenu(game: game),
'Hud': (context, game) => Hud(game: game),
'GameOver': (context, game) => GameOverMenu(game: game),
},
initialActiveOverlays: const ['MainMenu'],
),
),
));
}
class TapGame extends FlameGame {
final random = Random();
final score = ValueNotifier<int>(0); // the HUD listens to this
double timeLeft = 15;
@override
Future<void> onLoad() async {
camera.viewfinder.anchor = Anchor.topLeft;
pauseEngine(); // wait on the main menu
}
void start() {
score.value = 0;
timeLeft = 15;
world.removeAll(world.children.toList());
world.add(Target());
overlays
..remove('MainMenu')
..remove('GameOver')
..add('Hud');
resumeEngine();
}
@override
void update(double dt) {
super.update(dt);
timeLeft -= dt;
if (timeLeft <= 0) {
pauseEngine();
overlays
..remove('Hud')
..add('GameOver');
}
}
}
class Target extends CircleComponent with TapCallbacks, HasGameReference<TapGame> {
Target() : super(radius: 35, anchor: Anchor.center, paint: Paint()..color = Colors.redAccent);
@override
void onMount() {
super.onMount();
jump();
}
void jump() {
position = Vector2(
radius + game.random.nextDouble() * (game.size.x - radius * 2),
100 + game.random.nextDouble() * (game.size.y - 200),
);
}
@override
void onTapDown(TapDownEvent event) {
game.score.value++;
jump();
}
}
// ---------- Flutter overlays ----------
class MainMenu extends StatelessWidget {
const MainMenu({super.key, required this.game});
final TapGame game;
@override
Widget build(BuildContext context) {
return Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Tap the Target', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
const Text('Hit the red circle as many times as you can in 15 seconds.'),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: game.start,
icon: const Icon(Icons.play_arrow),
label: const Text('Play'),
),
],
),
),
),
);
}
}
class Hud extends StatelessWidget {
const Hud({super.key, required this.game});
final TapGame game;
@override
Widget build(BuildContext context) {
return SafeArea(
child: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
ValueListenableBuilder<int>(
valueListenable: game.score,
builder: (context, score, _) => Text(
'Score: $score',
style: const TextStyle(color: Colors.white, fontSize: 22),
),
),
const Spacer(),
StatefulBuilder(
builder: (context, setState) => IconButton.filledTonal(
icon: Icon(game.paused ? Icons.play_arrow : Icons.pause),
onPressed: () {
game.paused ? game.resumeEngine() : game.pauseEngine();
setState(() {}); // refresh the icon
},
),
),
],
),
),
);
}
}
class GameOverMenu extends StatelessWidget {
const GameOverMenu({super.key, required this.game});
final TapGame game;
@override
Widget build(BuildContext context) {
return Center(
child: Card(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Time up!', style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text('You scored ${game.score.value}', style: const TextStyle(fontSize: 20)),
const SizedBox(height: 24),
FilledButton(onPressed: game.start, child: const Text('Play again')),
],
),
),
),
);
}
}
Talking between the game and the widgets
- Widget → game: overlays receive the
game, so a button can callgame.start()orgame.pauseEngine(). - Game → widget: put values the UI shows in a
ValueNotifier(likescore) and use aValueListenableBuilder. For bigger apps, a state management package such as Riverpod or Bloc works too; Flame hasflame_riverpodandflame_blocpackages.
Tips
- Keep the HUD as an overlay or as
TextComponents in the camera viewport; overlays are best when you need buttons. - Pause the engine while a menu is open so nothing moves behind it.
- Overlays are normal widgets: use animations, themes and fonts as in any app.
Try it yourself
Add a “Settings” overlay reachable from the main menu, with a slider that changes the round length from 10 to 60 seconds.