Flutter Beginner Lesson 120 of 135 4 min read
Flappy Bird Game in Flutter with Flame (Full Source Code)
Make a Flappy Bird style game in Flutter with Flame: gravity, tap to flap, moving pipes, collisions, score and restart. Full code that runs in your browser.
On this page
A one-button game: tap to flap, gravity pulls you down, fly through the gaps. It teaches gravity, spawning obstacles, collisions and game states (ready, playing, game over).
Controls: tap, click or press Space.
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 gameWidth = 400.0;
const gameHeight = 700.0;
const groundY = 650.0;
void main() => runApp(GameWidget(game: FlappyGame()));
enum Phase { ready, playing, over }
class FlappyGame extends FlameGame with HasCollisionDetection, TapCallbacks, KeyboardEvents {
FlappyGame()
: super(camera: CameraComponent.withFixedResolution(width: gameWidth, height: gameHeight));
final random = Random();
final bird = Bird();
late final TextComponent message;
Phase phase = Phase.ready;
int score = 0;
double pipeTimer = 0;
@override
Future<void> onLoad() async {
camera.viewfinder.anchor = Anchor.topLeft;
world.add(RectangleComponent(
size: Vector2(gameWidth, gameHeight),
paint: Paint()..color = const Color(0xFF70C5CE), // sky
));
world.add(Ground());
world.add(bird);
message = TextComponent(
anchor: Anchor.topCenter,
position: Vector2(gameWidth / 2, 60),
priority: 10,
textRenderer: TextPaint(
style: const TextStyle(color: Colors.white, fontSize: 28, fontWeight: FontWeight.bold),
),
);
world.add(message);
showMessage();
}
void showMessage() {
message.text = switch (phase) {
Phase.ready => 'Tap to start',
Phase.playing => '$score',
Phase.over => 'Game over: $score. Tap!',
};
}
void flap() {
switch (phase) {
case Phase.ready:
phase = Phase.playing;
bird.flap();
case Phase.playing:
bird.flap();
case Phase.over:
restart();
}
showMessage();
}
@override
void onTapDown(TapDownEvent event) => flap();
@override
KeyEventResult onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
if (event is KeyDownEvent &&
(event.logicalKey == LogicalKeyboardKey.space || event.logicalKey == LogicalKeyboardKey.arrowUp)) {
flap();
}
return KeyEventResult.handled;
}
@override
void update(double dt) {
super.update(dt);
if (phase != Phase.playing) return;
pipeTimer += dt;
if (pipeTimer > 1.6) {
pipeTimer = 0;
spawnPipes();
}
}
void spawnPipes() {
const gap = 170.0;
final gapCenter = 150 + random.nextDouble() * 300;
final topHeight = gapCenter - gap / 2;
final bottomY = gapCenter + gap / 2;
world.add(Pipe(position: Vector2(gameWidth + 10, 0), height: topHeight, isTop: true));
world.add(Pipe(position: Vector2(gameWidth + 10, bottomY), height: groundY - bottomY));
}
void addPoint() {
score++;
showMessage();
}
void gameOver() {
if (phase != Phase.playing) return;
phase = Phase.over;
showMessage();
}
void restart() {
world.removeAll(world.children.whereType<Pipe>().toList());
bird.reset();
score = 0;
pipeTimer = 0;
phase = Phase.ready;
}
}
class Bird extends CircleComponent with CollisionCallbacks, HasGameReference<FlappyGame> {
Bird()
: super(
radius: 16,
anchor: Anchor.center,
paint: Paint()..color = const Color(0xFFFFD54F),
children: [CircleHitbox()],
);
double velocityY = 0;
@override
Future<void> onLoad() async => reset();
void reset() {
position = Vector2(120, 330);
velocityY = 0;
angle = 0;
}
void flap() => velocityY = -330;
@override
void update(double dt) {
super.update(dt);
if (game.phase != Phase.playing) return;
velocityY += 900 * dt; // gravity
position.y += velocityY * dt;
angle = (velocityY / 600).clamp(-0.5, 1.2); // tilt up and down
if (position.y < radius) { // ceiling
position.y = radius;
velocityY = 0;
}
}
@override
void onCollisionStart(Set<Vector2> points, PositionComponent other) {
super.onCollisionStart(points, other);
if (other is Pipe || other is Ground) game.gameOver();
}
}
class Pipe extends RectangleComponent with HasGameReference<FlappyGame> {
Pipe({required super.position, required double height, this.isTop = false})
: super(
size: Vector2(70, height),
paint: Paint()..color = const Color(0xFF4CAF50),
children: [RectangleHitbox()],
);
final bool isTop;
bool scored = false;
@override
void update(double dt) {
super.update(dt);
if (game.phase != Phase.playing) return;
position.x -= 160 * dt;
// One point when the top pipe passes the bird.
if (isTop && !scored && position.x + size.x < game.bird.position.x) {
scored = true;
game.addPoint();
}
if (position.x < -size.x) removeFromParent();
}
}
class Ground extends RectangleComponent {
Ground()
: super(
position: Vector2(0, groundY),
size: Vector2(gameWidth, gameHeight - groundY),
paint: Paint()..color = const Color(0xFFDED895),
children: [RectangleHitbox()],
);
}
How it works
Gravity and flapping. Every frame gravity adds to the bird’s downward speed (velocityY += 900 * dt). A flap sets the speed to a fixed upward value (-330). That simple pair of rules is the whole feel of the game; change the two numbers and it plays completely differently.
Phases. An enum (ready, playing, over) decides what a tap does and whether things move. A switch expression picks the message for each phase.
Pipes. Every 1.6 seconds a pair of pipes is created just off the right edge with a random gap position. They move left and remove themselves when off screen.
Scoring. Each top pipe gives one point the first time it passes the bird (scored stops it counting twice).
Collisions. The bird has a CircleHitbox; pipes and the ground have RectangleHitboxes. Touching any of them ends the game.
Make it yours
- Make the gap smaller as the score goes up.
- Add a “Best” score saved with shared_preferences.
- Replace the circle with a bird sprite animation.
- Add a flap sound and a hit sound.
- Add moving clouds in the background using a slower speed (parallax).