Flutter Tutorial

Flutter Intermediate Lesson 107 of 135 3 min read

Input in Flame: Tap, Drag and Keyboard Controls

Handle input in Flutter Flame games: tap with TapCallbacks, drag with DragCallbacks, and arrow keys with KeyboardHandler. Runnable examples.

On this page

Games need controls. Flame sends input to components through mixins:

MixinEvents
TapCallbacksonTapDown, onTapUp, onLongTapDown, onTapCancel
DragCallbacksonDragStart, onDragUpdate, onDragEnd
KeyboardHandler (+ HasKeyboardHandlerComponents on the game)onKeyEvent

A component only receives taps and drags inside its own area.

Tap: pop the balloons

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

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

class BalloonGame extends FlameGame {
  final random = Random();
  int popped = 0;
  late final TextComponent label;

  @override
  Future<void> onLoad() async {
    camera.viewfinder.anchor = Anchor.topLeft;
    label = TextComponent(text: 'Popped: 0', position: Vector2(16, 16), priority: 10);
    world.add(label);
    for (var i = 0; i < 6; i++) {
      spawn();
    }
  }

  void spawn() {
    world.add(Balloon(
      position: Vector2(
        40 + random.nextDouble() * (size.x - 80),
        80 + random.nextDouble() * (size.y - 160),
      ),
      color: Colors.primaries[random.nextInt(Colors.primaries.length)],
    ));
  }

  void onPopped() {
    popped++;
    label.text = 'Popped: $popped';
    spawn();
  }
}

class Balloon extends CircleComponent with TapCallbacks, HasGameReference<BalloonGame> {
  Balloon({required super.position, required Color color})
      : super(radius: 30, anchor: Anchor.center, paint: Paint()..color = color);

  @override
  void onTapDown(TapDownEvent event) {
    removeFromParent();
    game.onPopped();
  }
}

Tap (or click) a balloon: it disappears, the score goes up and a new balloon appears.

Drag: move the player

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

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

class DragGame extends FlameGame {
  @override
  Future<void> onLoad() async {
    camera.viewfinder.anchor = Anchor.topLeft;
    world.add(Paddle()..position = Vector2(size.x / 2, size.y - 60));
  }
}

class Paddle extends RectangleComponent with DragCallbacks, HasGameReference<DragGame> {
  Paddle()
      : super(
          size: Vector2(140, 24),
          anchor: Anchor.center,
          paint: Paint()..color = Colors.tealAccent,
        );

  @override
  void onDragUpdate(DragUpdateEvent event) {
    position.x = (position.x + event.localDelta.x).clamp(size.x / 2, game.size.x - size.x / 2);
  }
}

event.localDelta is how far the finger moved since the last update.

Keyboard: arrow keys

The game needs HasKeyboardHandlerComponents, and each component that listens needs KeyboardHandler.

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

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

class KeysGame extends FlameGame with HasKeyboardHandlerComponents {
  @override
  Future<void> onLoad() async {
    camera.viewfinder.anchor = Anchor.topLeft;
    world.add(Knight()..position = size / 2);
    world.add(TextComponent(text: 'Click here, then use the arrow keys or WASD', position: Vector2(16, 16)));
  }
}

class Knight extends RectangleComponent with KeyboardHandler, HasGameReference<KeysGame> {
  Knight() : super(size: Vector2.all(50), anchor: Anchor.center, paint: Paint()..color = Colors.amber);

  final direction = Vector2.zero();
  static const speed = 250.0;

  @override
  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keysPressed) {
    direction.setZero();
    if (keysPressed.contains(LogicalKeyboardKey.arrowLeft) || keysPressed.contains(LogicalKeyboardKey.keyA)) direction.x -= 1;
    if (keysPressed.contains(LogicalKeyboardKey.arrowRight) || keysPressed.contains(LogicalKeyboardKey.keyD)) direction.x += 1;
    if (keysPressed.contains(LogicalKeyboardKey.arrowUp) || keysPressed.contains(LogicalKeyboardKey.keyW)) direction.y -= 1;
    if (keysPressed.contains(LogicalKeyboardKey.arrowDown) || keysPressed.contains(LogicalKeyboardKey.keyS)) direction.y += 1;
    return true; // handled
  }

  @override
  void update(double dt) {
    super.update(dt);
    if (!direction.isZero()) {
      position += direction.normalized() * speed * dt;
      position.clamp(size / 2, game.size - size / 2);
    }
  }
}

Store the direction in onKeyEvent and move in update. That keeps movement smooth while a key is held down, and diagonal movement (two keys) is not faster thanks to normalized().

Taps on the whole game

To react to a tap anywhere (for example “tap to jump”), add TapCallbacks to the game itself:

class JumpGame extends FlameGame with TapCallbacks {
  @override
  void onTapDown(TapDownEvent event) {
    // event.canvasPosition is where the player tapped
  }
}

Phones without keyboards

For mobile, add on-screen controls: Flame has JoystickComponent and HudButtonComponent, or simply place Flutter buttons over the game with overlays.

Try it yourself

In the keyboard example, press Space to change the knight’s colour. (Tip: check event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.space.)

Practise in the playground Updated