How to make a Flappy Bird clone with Flutter and Flame Engine

Posted: 11/21/2022

Lukas Klingsbo (spydon) teaches Alex and Brittney how to setup and use Flame to build a Flappy Bird clone. In this tutorial we will walk through everything from repo setup to final game.

Project Setup

We will be using VSCode for this tutorial, you will also want your favorite terminal to get started. Make sure that you have the Flutter VSCode Extension installed so that you can get support for running Flutter. You will also need a git client installed.

Starting Repo

spydon has provided a starter repo that has everything we will need, it also has the full application incase you get stuck. You can start out on the dev branch and build from there.

git clone -b dev https://github.com/spydon/flappy_ember.git

If you get stuck or want to skip to the end just run git checkout main.

File Structure

All of our code can be found in the lib directory. All of our sprites and other images will be found in assets.

Untitled

Dependencies

In Flutter all of your dependencies are stored in pubspec.yaml. I would recommend changing from the git version to a published version.

-flame:
-    git:
-      url: https://github.com/flame-engine/flame.git
-      ref: main
-      path: packages/flame
+flame: 1.2.1

If you Flutter VSCode extension is running correctly you should now see your packages install / update.

Programming

main.dart

This is the file that starts your Flutter application. There is only a small amount of Flutter required in your main.dart file. The majority of the setup here is providing the GameWidget component an instance of our Flame game.

import 'package:flame/game.dart';
import 'package:flutter/material.dart';

import 'game.dart';

void main() {
  final game = FlappyEmber();
  runApp(GameWidget(game: game));
}

game.dart

The FlappyEmber class is an extension of FlameGame which comes form the Flame package. This is essential the main game that we are building, it is where we will import all of the aspects that it takes to create a game like Player, Sky, ScreenHitbox and set all of the variables that are needed to run the game like speed and random.

import 'dart:math';

import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
import 'package:flappy_ember/player.dart';

import 'sky.dart';
import 'boxstack.dart';

class FlappyEmber extends FlameGame with TapDetector, HasCollisionDetection {
  late final Player player;
  double speed = 500;
  final random = Random();
  
  Future<void>? onLoad() async {
    player = Player();
    add(Sky());
    add(ScreenHitbox());
    add(player);
	  return null;
  }

  void gameover() {
    pauseEngine();
  }

  double _timeSinceBox = 0;
  double _boxInterval = 1;
  
  void update(double dt) {
    super.update(dt);
    speed += 10 * dt;
    _timeSinceBox += dt;

    if (_timeSinceBox > _boxInterval) {
      add(BoxStack(isBottom: random.nextBool()));
      _timeSinceBox = 0;
    }
  }

  
  void onTap() {
    super.onTap();
    player.fly();
  }
}

player.dart

In the player.dart file create a new Class called Player this is an extension of SpriteAnimationComponent which is a component provided by the flame package for animating our Ember Sprite.

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/flame.dart';
import 'package:flutter/material.dart';

import 'game.dart';

class Player extends SpriteAnimationComponent
    with CollisionCallbacks, HasGameRef<FlappyEmber> {
  Player() : super(size: Vector2(100, 100), position: Vector2(100, 100));

  
  Future<void>? onLoad() async {
    add(CircleHitbox());

    final image = await Flame.images.load('ember.png');
    animation = SpriteAnimation.fromFrameData(
      image,
      SpriteAnimationData.sequenced(
        amount: 4,
        stepTime: 0.10,
        textureSize: Vector2.all(16),
      ),
    );
  }

  
  void onCollisionStart(_, __) {
    super.onCollisionStart(_, __);
    gameRef.gameover();
  }

  
  void update(double dt) {
    super.update(dt);

    position.y += 200 * dt;
  }

  void fly() {
    final effect = MoveByEffect(
        Vector2(0, -100),
        EffectController(
          duration: 0.5,
          curve: Curves.decelerate,
        ));

    add(effect);
  }
}

sky.dart

This is a fairly simple component called Sky that extends the SpriteComponent from Flame. Its only job is to load the parallax background.

import 'package:flame/components.dart';
import 'package:flame/flame.dart';

class Sky extends SpriteComponent {
  Sky() : super(priority: -1);

  
  Future<void>? onLoad() async {
    final image = await Flame.images.load('./parallax/bg_sky.png');

    sprite = Sprite(image);
  }

  
  void onGameResize(Vector2 size) {
    super.onGameResize(size);

    this.size = size;
  }
}

box.dart

The Box component is equally as easy as Sky, with one small difference that we also add a RectangleHitbox so that when our ember runs into the box we understand that it has struck the box.

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/widgets.dart';
import 'package:flame/flame.dart';

class Box extends SpriteComponent {
  static Vector2 initialSize = Vector2.all(150);
  Box({super.position}) : super(size: initialSize);

  
  Future<void>? onLoad() async {
    final image = await Flame.images.load('./boxes/1.png');

    sprite = Sprite(image);

    add(RectangleHitbox());
  }
}

boxstack.dart

The BoxStack component extends PositionComponent and also uses the game that we created called FlappyEmber for a reference. This is massively useful as we can calculate things like gameHeight directly from the gameRef.size.y of the instance. This allows you to calculate how many boxes can be stacked randomly while still fitting within the boundaries of the game.

import 'dart:math';

import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/widgets.dart';
import 'package:flame/flame.dart';

import 'game.dart';
import 'box.dart';

class BoxStack extends PositionComponent with HasGameRef<FlappyEmber> {
  final bool isBottom;
  static final random = Random();

  BoxStack({required this.isBottom});

  
  Future<void>? onLoad() async {
    position.x = gameRef.size.x;
    final gameHeight = gameRef.size.y;
    final boxHeight = Box.initialSize.y;
    final maxStackHeight = (gameHeight / boxHeight).floor() - 2;

    final stackHeight = random.nextInt(maxStackHeight + 1);
    final boxSpacing = boxHeight * (2 / 3);
    final initialY = isBottom ? gameHeight - boxHeight : -boxHeight / 3;

    final boxs = List.generate(stackHeight, (index) {
      return Box(
        position:
            Vector2(0, initialY + index * boxSpacing * (isBottom ? -1 : 1)),
      );
    });
    addAll(isBottom ? boxs : boxs.reversed);
  }

  
  void update(double dt) {
    super.update(dt);
    if (position.x < -Box.initialSize.x) {
      removeFromParent();
    }
    position.x -= gameRef.speed * dt;
  }
}

Testing the Game

Throughout this process you most likely will want to debug the game. Once again have the VSCode Flutter plugin this is made super simple. All you need to do is be in any of the the files in the source directory and use the run and debug screen like normal and this will build out your desktop or mobile version of the game.

Untitled