Flutter Tutorial

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

  1. Add a green “heart” block that falls rarely (10% of spawns) and gives an extra life when caught.
  2. Make the spawn faster over time too. (Tip: keep a reference to the SpawnComponent and lower its period.)
Practise in the playground Updated