Flutter Tutorial

Flutter Intermediate Lesson 123 of 135 4 min read

Endless Runner Game in Flutter with Flame (Dino Style, Full Code)

Build a Chrome dino style endless runner in Flutter with Flame: jumping, random obstacles, rising speed and best score. Full runnable source code.

On this page

Run, jump, survive. The world moves towards the player, obstacles appear at random distances, and the game gets faster the longer you last. This is the base of countless mobile hits.

Controls: tap, click, Space or the up arrow to jump. Hold for a higher jump.

The full game

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';

const w = 700.0;
const h = 350.0;
const groundY = 290.0;

void main() => runApp(GameWidget(game: RunnerGame()));

class RunnerGame extends FlameGame with HasCollisionDetection, TapCallbacks, KeyboardEvents {
  RunnerGame() : super(camera: CameraComponent.withFixedResolution(width: w, height: h));

  final random = Random();
  final runner = Runner();
  late final TextComponent hud;
  double speed = 300;        // how fast the world moves (pixels per second)
  double distance = 0;
  double nextObstacle = 1;   // seconds until the next obstacle
  int best = 0;
  bool running = false;
  bool dead = false;

  @override
  Color backgroundColor() => const Color(0xFFF7F7F7);

  @override
  Future<void> onLoad() async {
    camera.viewfinder.anchor = Anchor.topLeft;
    world.add(RectangleComponent(
      position: Vector2(0, groundY),
      size: Vector2(w, 4),
      paint: Paint()..color = const Color(0xFF535353),
    ));
    world.add(runner);
    hud = TextComponent(
      position: Vector2(16, 12),
      textRenderer: TextPaint(style: const TextStyle(color: Color(0xFF535353), fontSize: 18)),
    );
    world.add(hud);
    updateHud();
  }

  int get score => distance ~/ 10;

  void updateHud() {
    hud.text = dead
        ? 'GAME OVER   Score: $score   Best: $best   (tap to restart)'
        : running
            ? 'Score: $score   Best: $best'
            : 'Tap or press Space to start';
  }

  @override
  void update(double dt) {
    super.update(dt);
    if (!running || dead) return;
    distance += speed * dt;
    speed += 6 * dt; // slowly faster
    nextObstacle -= dt;
    if (nextObstacle <= 0) {
      world.add(Obstacle(random));
      nextObstacle = 0.9 + random.nextDouble() * 1.2;
    }
    updateHud();
  }

  void jumpPressed() {
    if (dead) {
      restart();
    } else if (!running) {
      running = true;
    } else {
      runner.jump();
    }
  }

  void jumpReleased() => runner.cutJump();

  void die() {
    dead = true;
    best = max(best, score);
    updateHud();
  }

  void restart() {
    world.removeAll(world.children.whereType<Obstacle>().toList());
    speed = 300;
    distance = 0;
    nextObstacle = 1;
    dead = false;
    running = true;
    runner.reset();
  }

  @override
  void onTapDown(TapDownEvent event) => jumpPressed();

  @override
  void onTapUp(TapUpEvent event) => jumpReleased();

  @override
  KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    final isJumpKey = event.logicalKey == LogicalKeyboardKey.space ||
        event.logicalKey == LogicalKeyboardKey.arrowUp;
    if (isJumpKey && event is KeyDownEvent) jumpPressed();
    if (isJumpKey && event is KeyUpEvent) jumpReleased();
    return KeyEventResult.handled;
  }
}

class Runner extends RectangleComponent with CollisionCallbacks, HasGameReference<RunnerGame> {
  Runner()
      : super(
          size: Vector2(36, 44),
          anchor: Anchor.bottomLeft,
          paint: Paint()..color = const Color(0xFF535353),
          children: [RectangleHitbox(size: Vector2(30, 40), position: Vector2(3, 2))],
        );

  double velocityY = 0;
  bool get onGround => position.y >= groundY;

  @override
  Future<void> onLoad() async => reset();

  void reset() {
    position = Vector2(80, groundY);
    velocityY = 0;
  }

  void jump() {
    if (onGround) velocityY = -720;
  }

  // Releasing early makes a short hop.
  void cutJump() {
    if (velocityY < -250) velocityY = -250;
  }

  @override
  void update(double dt) {
    super.update(dt);
    if (game.dead) return;
    velocityY += 2200 * dt; // gravity
    position.y += velocityY * dt;
    if (position.y > groundY) {
      position.y = groundY;
      velocityY = 0;
    }
  }

  @override
  void onCollisionStart(Set<Vector2> points, PositionComponent other) {
    super.onCollisionStart(points, other);
    if (other is Obstacle) game.die();
  }
}

class Obstacle extends RectangleComponent with HasGameReference<RunnerGame> {
  Obstacle(Random random)
      : super(
          // Random cactus size: short and wide, or tall and thin
          size: random.nextBool() ? Vector2(24, 40) : Vector2(40, 30),
          anchor: Anchor.bottomLeft,
          position: Vector2(w + 20, groundY),
          paint: Paint()..color = const Color(0xFF2E7D32),
          children: [RectangleHitbox(collisionType: CollisionType.passive)],
        );

  @override
  void update(double dt) {
    super.update(dt);
    if (game.dead) return;
    position.x -= game.speed * dt;
    if (position.x < -size.x) removeFromParent();
  }
}

How it works

The world moves, not the player. The runner only moves up and down. Obstacles move left at game.speed, which creates the feeling of running forward. speed grows a little every second.

A better jump. Pressing gives a strong upward speed. Releasing early cuts that speed down (cutJump), so a quick tap is a small hop and holding is a big jump. Many famous platformers use this trick.

Fair obstacles. The time to the next obstacle is random but never shorter than 0.9 seconds, so there is always enough room to land and jump again.

Smaller hitbox. The runner’s hitbox is a little smaller than its drawing. Players feel cheated when a pixel-perfect touch kills them; a forgiving hitbox feels fair.

Score. Distance travelled divided by 10, using integer division (~/).

Make it yours

  1. Add flying obstacles that you must not jump into (duck with the down arrow by making the runner shorter).
  2. Add coins in the air for bonus points.
  3. Draw a scrolling background of clouds and mountains at slower speeds (parallax).
  4. Replace the rectangle with a running sprite animation.
  5. Save the best score with shared_preferences.
Practise in the playground Updated