Flutter Intermediate Lesson 112 of 135 3 min read
Score, Lives, Timers and Spawning Enemies in Flame
Add game rules in Flame: a score and lives HUD, TimerComponent, SpawnComponent for enemies, and difficulty that grows over time. Runnable game.
On this page
A game needs rules: points to earn, lives to lose, a clock, and enemies that keep coming. This lesson builds all four into a small “dodge the blocks” game.
Dodge the blocks
Drag (or use the arrow keys) to dodge. You have three lives; each second survived is a point; the blocks get faster.
import 'dart:math';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/events.dart';
import 'package:flame/game.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(GameWidget(game: DodgeGame()));
class DodgeGame extends FlameGame
with HasCollisionDetection, HasKeyboardHandlerComponents, TapCallbacks {
DodgeGame() : super(camera: CameraComponent.withFixedResolution(width: 400, height: 700));
final random = Random();
int score = 0;
int lives = 3;
double fallSpeed = 200;
late final TextComponent hud;
late final Runner runner;
@override
Future<void> onLoad() async {
camera.viewfinder.anchor = Anchor.topLeft;
world.add(RectangleComponent(size: Vector2(400, 700), paint: Paint()..color = const Color(0xFF14213D)));
runner = Runner()..position = Vector2(200, 640);
world.add(runner);
hud = TextComponent(
position: Vector2(12, 12),
priority: 10,
textRenderer: TextPaint(style: const TextStyle(color: Colors.white, fontSize: 16)),
);
world.add(hud);
updateHud();
// A point every second, and the game gets 5% faster.
world.add(TimerComponent(
period: 1,
repeat: true,
onTick: () {
score++;
fallSpeed *= 1.05;
updateHud();
},
));
// A new block every 0.6 seconds at a random x.
world.add(SpawnComponent(
period: 0.6,
selfPositioning: true, // keep the position we set below
factory: (_) => Block(Vector2(20 + random.nextDouble() * 360, -30)),
));
}
void updateHud() => hud.text = 'Score: $score Lives: ${'♥' * lives}';
void loseLife() {
lives--;
updateHud();
if (lives <= 0) {
hud.text = 'Game over! Score: $score (tap to restart)';
pauseEngine();
}
}
@override
void onTapDown(TapDownEvent event) {
if (paused) restart();
}
void restart() {
world.children.whereType<Block>().toList().forEach((b) => b.removeFromParent());
score = 0;
lives = 3;
fallSpeed = 200;
updateHud();
resumeEngine();
}
}
class Runner extends RectangleComponent
with DragCallbacks, KeyboardHandler, HasGameReference<DodgeGame> {
Runner()
: super(size: Vector2(50, 30), anchor: Anchor.center,
paint: Paint()..color = Colors.greenAccent, children: [RectangleHitbox()]);
double move = 0;
@override
void onDragUpdate(DragUpdateEvent event) => position.x += event.localDelta.x;
@override
bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keys) {
move = (keys.contains(LogicalKeyboardKey.arrowRight) ? 1.0 : 0.0) -
(keys.contains(LogicalKeyboardKey.arrowLeft) ? 1.0 : 0.0);
if (game.paused && keys.contains(LogicalKeyboardKey.space)) game.restart();
return true;
}
@override
void update(double dt) {
super.update(dt);
position.x = (position.x + move * 350 * dt).clamp(25.0, 375.0);
}
}
class Block extends RectangleComponent with CollisionCallbacks, HasGameReference<DodgeGame> {
Block(Vector2 position)
: super(position: position, size: Vector2(40, 40), anchor: Anchor.center,
paint: Paint()..color = Colors.redAccent, children: [RectangleHitbox()]);
@override
void update(double dt) {
super.update(dt);
position.y += game.fallSpeed * dt;
if (position.y > 740) removeFromParent();
}
@override
void onCollisionStart(Set<Vector2> points, PositionComponent other) {
super.onCollisionStart(points, other);
if (other is Runner) {
removeFromParent();
game.loseLife();
}
}
}
The building blocks
Score text (HUD). A TextComponent with a high priority so it is drawn on top. Change text whenever the score changes. With a following camera, add it to camera.viewport instead of the world (see Camera and world).
TimerComponent calls onTick after period seconds, once or with repeat: true forever. Use it for points over time, countdowns (“30 seconds left”) and power-up durations.
SpawnComponent creates a new component every period seconds using factory. With selfPositioning: true it keeps the position you set; without it, Flame places each new component at a random point inside an area (or the parent’s size).
Difficulty. Change one number over time (here fallSpeed *= 1.05). Small steady increases feel fairer than sudden jumps.
Game over. pauseEngine() freezes the game loop, resumeEngine() restarts it, and paused tells you which state you are in. In the next lesson the game over screen becomes a real Flutter menu.
Try it yourself
- Add a green “heart” block that falls rarely (10% of spawns) and gives an extra life when caught.
- Make the spawn faster over time too. (Tip: keep a reference to the
SpawnComponentand lower itsperiod.)