Camera and Viewport¶
When rendering on Flutter, the regular coordinate space used are logical pixels. That means one pixel for Flutter is already not necessarily one real pixel on the device, because of the device’s pixel ratio. When it gets to the Flame level, we always consider the most fundamental level to be logical pixels, so all the device specific complexity is abstracted away.
However, that still leaves you with arbitrarily shaped and sized screens. And it’s very likely that your game has some sort of game world with an intrinsic coordinate system that does not map to screen coordinates. Flame adds two distinct concepts to help transform coordinate spaces. For the former, we have the Viewport class. And for the later, we have the Camera class.
Viewport¶
The Viewport is an attempt to unify multiple screen (or, rather, game widget) sizes into a single configuration for your game by translating and resizing the canvas.
The Viewport
interface has multiple implementations and can be used from scratch on your Game
or, if you are using FlameGame
instead, it’s already built-in (with a default no-op viewport).
These are the viewports available to pick from (or you can implement the interface yourself to suit your needs):
DefaultViewport
: this is the no-op viewport that is associated by default with anyFlameGame
.FixedResolutionViewport
: this viewport transforms your Canvas so that, from the game perspective, the dimensions are always set to a fixed pre-defined value. This means it will scale the game as much as possible and add black bars if needed.
When using FlameGame
, the operations performed by the viewport are done automatically to every
render operation, and the size
property in the game, instead of the logical widget size, becomes
the size as seen through the viewport together with the zoom of the camera. If for some reason you
need to access the original real logical pixel size, you can use canvasSize
. For a more in depth
description on what each Viewport
does and how it operates, check the documentation on its class.
Camera¶
Unlike the Viewport
, the Camera
is a more dynamic Canvas
transformation that is normally
dependent on:
World coordinates that do not match screen coordinates 1:1.
Centering or following the player around the game world (if the world is bigger than the screen).
User controlled zooming in and out.
There is only one Camera implementation but it allows for many different configurations. Again, you
can use it standalone on your Game
but it’s already included and wired into FlameGame
.
One important thing to note about the Camera is that since (unlike the Viewport) it’s intended to be
dynamic, most camera movements won’t immediately happen. Instead, the camera has a configurable
speed and is updated on the game loop. If you want to immediately move your camera (like on your
first camera setup at game start) you can use the snap
function. Calling snap during the game can
lead to jarring or unnatural camera movements though, so avoid that unless desired (say for a map
transition, for example). Carefully check the docs for each method for more details about how it
affects the camera movement.
Another important note is that the camera is applied after the viewport, and only to non-HUD components. So screen size here is considering the effective size after Viewport transformations.
There are two types of transformations that the Camera can apply to the Canvas. The first and most complex one is translation. That can be applied by several factors:
nothing: by default the camera won’t apply any transformation, so it’s optional to use it.
relative offset: you can configure this to decide “where the center of the camera should be on the screen”. By default it’s the top left corner, meaning that the centered coordinate or object will always be on the top left corner of the screen. You can smoothly change the relative offset during gameplay (that can be used to apply a dialogue or item pickup temporary camera transition for example).
moveTo: if you want to ad-hoc move your camera you can use this method; it will smoothly transition the camera to a new position, ignoring follows but respecting relative offset and world bounds. This can be reset by
resetMovement
if used in conjunction to follow so that the followed object starts being considered again.follow: you can use this method so that your camera continuously “follow” an object (for example, a
PositionComponent
). This is not smooth because the movement of the followed object itself is assumed to already be smooth (i.e. if your character teleport the camera will also immediately teleport).world bounds: when using follow, you can optionally define the bounds of the world. If that is done, the camera will stop following/moving so that out-of-bounds areas are not shown (as long as the world is bigger than the screen).
Finally the second transformation that the camera applies is scaling. That allows for dynamic
zooming, and it’s controlled by the zoom
field. There is no zoom speed, that must be controlled by
you when changing. The zoom
variable is immediately applied.
When dealing with input events, it is imperative to convert screen coordinates to world coordinates
(or, for some reasons, you might want to do the reverse). The Camera provides two functions,
screenToWorld
and worldToScreen
to easily convert between these coordinate spaces.
Camera.followVector2¶
Immediately snaps the camera to start following a Vector2
.
This means that the camera will move so that the position vector is in a fixed position on the
screen. That position is determined by a fraction of screen size defined by the relativeOffset
argument (defaults to the center). The worldBounds
argument can be optionally set to add
boundaries to how far the camera is allowed to move.
Example:
class MyGame extends FlameGame {
final someVector = Vector2(100, 100);
Future<void> onLoad() async {
camera.followVector2(someVector);
}
}
Camera.followComponent¶
Immediately snaps the camera to start following a PositionComponent
.
This means that the camera will move so that the position vector of the component is in a fixed
position on the screen. That position is determined by a fraction of screen size defined by the
relativeOffset
argument (defaults to the center).
The worldBounds
argument can be optionally set to add boundaries to how far the camera is allowed
to move.
The component is “grabbed” by its anchor (default top left). So for example if you want the center of the object to be at the fixed position, set the components anchor to center.
Example:
class MyGame extends FlameGame {
@override
Future<void> onLoad() async {
final sprite = await loadSprite('pizza.png');
final player = SpriteComponent(
sprite: sprite,
size: size,
anchor: Anchor.center,
);
add(player);
camera.followComponent(player);
}
}
Using the camera with the Game class¶
If you are not using FlameGame
, but instead are using the Game
mixin, then you need to manage
calling certain camera methods yourself. Let’s say we have the following game structure, and we
want to add the camera functionality:
class YourGame extends Game {
Camera? camera;
@override
Future<void> onLoad() async {}
@override
void render(Canvas canvas) {}
@override
void update(double dt) {}
}
We first create a new camera instance on load and assign our game as the reference:
@override
Future<void> onLoad() async {
camera = Camera();
// This is required for the camera to work.
camera?.gameRef = this;
// Not required but recommend to set it now or when you set the follow target.
camera?.worldBounds = yourWorldBounds;
// Rest of your on load code.
}
The camera can also be made aware of which position to follow, this is an optional feature as you can also use the camera for just moving,snapping or shaking.
To do this the Camera
class provides multiple methods for it but let’s showcase the simplest one
and that is the followVector2
:
// Somewhere in your code:
camera?.followVector2(
yourPositionToFollow,
worldBounds: yourWorldBounds, // Optional to pass, it will overwrite the previous bounds.
);
Now that the camera is created and it is aware of both the world bounds and the position it should follow, it can be used to translate the canvas in the render method:
@override
void render(Canvas canvas) {
camera?.apply(canvas); // This will apply the camera transformation.
// Rest of your rendering code.
}
The only thing left to do is to call the update
method on the Camera
so it can smoothly follow
your given position:
@override
void update(double dt) {
camera?.update(dt);
// Rest of your update code.
}