Flutter Tutorial

Flutter Intermediate Lesson 110 of 135 3 min read

Camera and World in Flame: Follow the Player, Fixed Resolution and HUD

Learn the Flame camera and world: follow the player, set bounds, use a fixed resolution for every screen, and keep a HUD on screen. Runnable example.

On this page

So far the whole game fitted on one screen. Real games have worlds bigger than the screen: a long level, a large map. Flame separates the two:

  • World: where the game objects live, in world coordinates.
  • Camera: looks at part of the world and shows it on the screen. It can move, follow the player and zoom.
  • Viewport: the area of the screen the camera draws into. Things added to the viewport (the HUD: score, lives, buttons) do not move with the world.
FlameGame
├── world      ← player, enemies, trees  (moves under the camera)
└── camera
    ├── viewfinder  ← position, zoom, angle of the camera
    └── viewport    ← HUD: score, health bar (fixed on screen)

A camera that follows the player

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

const worldSize = 2000.0;

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

class ExploreGame extends FlameGame with HasKeyboardHandlerComponents {
  late final Explorer player;

  @override
  Future<void> onLoad() async {
    // Some landmarks so you can see the world move.
    for (var x = 0.0; x <= worldSize; x += 200) {
      for (var y = 0.0; y <= worldSize; y += 200) {
        world.add(CircleComponent(
          radius: 20,
          position: Vector2(x, y),
          anchor: Anchor.center,
          paint: Paint()..color = Colors.green.withValues(alpha: 0.6),
        ));
      }
    }
    // The edges of the world
    world.add(RectangleComponent(
      size: Vector2.all(worldSize),
      paint: Paint()
        ..color = Colors.white24
        ..style = PaintingStyle.stroke
        ..strokeWidth = 4,
    ));

    player = Explorer()..position = Vector2.all(worldSize / 2);
    world.add(player);

    camera.follow(player);                                        // keep the player in the centre
    camera.setBounds(Rectangle.fromLTRB(0, 0, worldSize, worldSize), considerViewport: true);

    // HUD: added to the viewport, so it never moves.
    camera.viewport.add(TextComponent(
      text: 'Arrow keys / WASD to explore',
      position: Vector2(16, 16),
    ));
  }
}

class Explorer extends RectangleComponent with KeyboardHandler {
  Explorer() : super(size: Vector2.all(40), anchor: Anchor.center, paint: Paint()..color = Colors.orange);

  final direction = Vector2.zero();

  @override
  bool onKeyEvent(KeyEvent event, Set<LogicalKeyboardKey> keys) {
    direction
      ..x = (keys.contains(LogicalKeyboardKey.arrowRight) || keys.contains(LogicalKeyboardKey.keyD) ? 1.0 : 0.0) -
          (keys.contains(LogicalKeyboardKey.arrowLeft) || keys.contains(LogicalKeyboardKey.keyA) ? 1.0 : 0.0)
      ..y = (keys.contains(LogicalKeyboardKey.arrowDown) || keys.contains(LogicalKeyboardKey.keyS) ? 1.0 : 0.0) -
          (keys.contains(LogicalKeyboardKey.arrowUp) || keys.contains(LogicalKeyboardKey.keyW) ? 1.0 : 0.0);
    return true;
  }

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

Click the game, then use the arrow keys. The player stays in the middle and the world scrolls. At the edges the camera stops, because of setBounds.

Fixed resolution: the same game on every screen

Phones, tablets and monitors all have different sizes. A fixed resolution camera lets you design for one size (say 400×800) and scales it to fit any screen, adding bars if needed:

class MyGame extends FlameGame {
  MyGame()
      : super(
          camera: CameraComponent.withFixedResolution(width: 400, height: 800),
        );

  @override
  Future<void> onLoad() async {
    camera.viewfinder.anchor = Anchor.topLeft; // (0,0) top-left of the 400×800 area
  }
}

Now 400 is always the full width, whatever the device. Most of the game projects use this.

Zoom and shake

camera.viewfinder.zoom = 2;          // twice as close
camera.viewfinder.angle = 0.1;       // tilt
camera.moveTo(Vector2(500, 300));    // jump to a point

A simple screen shake: move the viewfinder a few pixels back and forth with a MoveEffect (see Effects and particles).

World vs screen coordinates

  • A component in the world uses world coordinates.
  • A component in the viewport uses screen coordinates.
  • Convert between them with camera.globalToLocal(screenPoint) and camera.localToGlobal(worldPoint).

Try it yourself

Add a “minimap” text in the HUD that shows the player’s position rounded to whole numbers, updated every frame. (Tip: give the TextComponent a reference to the player and set text in its update.)

Practise in the playground Updated