A playable Flame game fits in one file of about 65 lines. You drag a cyan square, orange rocks fall from the top, and a hit restarts the round. It uses no images, only a fixed camera, a drag input, a timer and hitboxes.
This is for Flutter developers who have never used Flame. It uses flame 1.38.2, the latest stable version in October 2026. If the words "component" and "world" are new, read the beginner's guide first.
In short:
- A fixed-resolution camera gives every device the same 400 by 800 field.
- A full-field component catches drags that start anywhere.
- Mark things that never need to hit each other as passive.
- React in
onCollisionStart, notonCollision.
Set up the project in two commands
flutter create dodge && cd dodge
flutter pub add flame
Paste the five blocks below into lib/main.dart, in order, then run flutter run -d chrome. Start the file with these imports and main.
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/foundation.dart';
import 'package:flutter/material.dart';
void main() => runApp(GameWidget(game: DodgeGame()));
A fixed camera makes one field for every device
The game class sets the camera and adds three things to the world.
class DodgeGame extends FlameGame with HasCollisionDetection {
DodgeGame() : super(camera: CameraComponent.withFixedResolution(
width: 400, height: 800));
final _rng = Random();
late final Player player;
@override
Future<void> onLoad() async {
camera.viewfinder.anchor = Anchor.topLeft; // (0,0) is top left
world.addAll([
DragZone(),
player = Player(),
TimerComponent(period: 0.5, repeat: true,
onTick: () => world.add(Rock(_rng.nextDouble() * 360))),
]);
}
void restart() => world.removeAll(world.children.query<Rock>().toList());
}
withFixedResolution pretends every screen is 400 by 800 and letterboxes it. A rock takes the same time to fall on a phone and a wide browser. The cost, per Flame's camera page, is bars at the sides or above and below when the aspect ratio differs.
The camera centres the world origin by default. The anchor line moves it to the top-left corner, so the player can start at (180, 720).
Objects go in world, not in the game. Anything added to the game directly is not moved by the camera and will not line up.
An invisible zone catches every drag
A component only receives drags that start inside it, and the player is just 40 pixels wide. So one invisible component covers the whole field.
class DragZone extends PositionComponent
with DragCallbacks, HasGameReference<DodgeGame> {
DragZone() : super(size: Vector2(400, 800));
@override
void onDragUpdate(DragUpdateEvent event) {
game.player.x = clampDouble(game.player.x + event.localDelta.x, 0, 360);
}
}
event.localDelta.x is how far the finger moved since the last event. clampDouble keeps the square inside the field. HasGameReference<DodgeGame> gives the zone a typed game field. A zone with no size gives a touch nothing to land on.
Passive hitboxes keep collisions cheap
The player is active. The rocks are passive.
class Player extends RectangleComponent
with CollisionCallbacks, HasGameReference<DodgeGame> {
Player() : super(
position: Vector2(180, 720),
size: Vector2.all(40),
paint: Paint()..color = Colors.cyan,
children: [RectangleHitbox()]);
@override
void onCollisionStart(Set<Vector2> points, PositionComponent other) {
super.onCollisionStart(points, other);
if (other is Rock) game.restart();
}
}
A RectangleHitbox() with no arguments fills its parent. Per Flame's collision docs, active hitboxes test against active and passive ones, and passive ones never test against each other. Rocks never need to hit rocks, so passive is free speed.
onCollisionStart fires once when two hitboxes begin to touch. onCollision fires every frame of overlap, so use it for effects that last while two things touch.
Rocks move by speed times dt
class Rock extends RectangleComponent {
Rock(double x) : super(
position: Vector2(x, -40),
size: Vector2.all(40),
paint: Paint()..color = Colors.orange,
children: [RectangleHitbox(collisionType: CollisionType.passive)]);
@override
void update(double dt) {
y += 240 * dt;
if (y > 800) removeFromParent();
}
}
dt is the seconds since the last frame, so 240 units per second holds on a 60 Hz phone and a 120 Hz tablet. A rock that leaves the bottom removes itself from the tree.
Four mistakes break this game
- Nothing falls. The
TimerComponentwas never added. - Everything sits in the wrong corner. The anchor line is missing.
- The restart misbehaves. Call
.toList()onquerybefore you remove things while going through it, asrestartdoes. - The square ignores the finger. The drag zone has no size.
GrowApps can build this from a sentence
GrowApps builds 2D games on Flame from a description, so you can ask for a dodge game and read the generated Flame code. See making a Flame game from a prompt.
Add a score in the next 10 minutes
Add a TextComponent to camera.viewport, which draws above the world and never moves with it. Update its text when a rock leaves the screen. The HUD and menus article shows the pattern. For nicer spawning, look at SpawnComponent on Flame's utility components page.