Skip to content

Commit

Permalink
feat: Introduce the FixedResolutionViewport (#2796)
Browse files Browse the repository at this point in the history
This enables you to zoom when using the CameraComponent.withFixedResolution constructor, without disabling the fixed resolution after doing so, by introducing a new viewport the FixedResolutionViewport.
  • Loading branch information
spydon committed Oct 9, 2023
1 parent a23f80e commit 4c762f9
Show file tree
Hide file tree
Showing 20 changed files with 380 additions and 102 deletions.
56 changes: 39 additions & 17 deletions doc/flame/camera_component.md
Expand Up @@ -21,21 +21,14 @@ With this mindset, we can now understand how camera-as-a-component works.

First, there is the [](#world) class, which contains all components that are
inside your game world. The `World` component can be mounted anywhere, for
example at the root of your game class.
example at the root of your game class, like the built-in `World` is.

Then, a [](#cameracomponent) class that "looks at" the `World`. The
`CameraComponent` has a `Viewport` and a `Viewfinder` inside, allowing both the
flexibility of rendering the world at any place on the screen, and also control
the viewing location and angle.

If you add children to the `Viewport` they will appear as static HUDs in
front of the world and if you add children to the `Viewfinder` they will appear
statically in front of the viewport.

To add static components behind the world you can add them to the `backdrop`
component, or replace the `backdrop` component. This is for example useful if
you want to have a static `ParallaxComponent` beneath a world that you can move
around it.
Then, a [](#cameracomponent) class that "looks at" the [](#world). The
`CameraComponent` has a [](#viewport) and a [](#viewfinder) inside, allowing
both the flexibility of rendering the world at any place on the screen, and
also control the viewing location and angle. The `CameraComponent` also
contains a [](#backdrop) component which is statically rendered below the
world.


## World
Expand Down Expand Up @@ -152,12 +145,17 @@ The following viewports are available:

- `MaxViewport` (default) -- this viewport expands to the maximum size allowed
by the game, i.e. it will be equal to the size of the game canvas.
- `FixedResolutionViewport` -- keeps the resolution and aspect ratio fixed, with black bars on the
sides if it doesn't match the aspect ratio.
- `FixedSizeViewport` -- a simple rectangular viewport with predefined size.
- `FixedAspectRatioViewport` -- a rectangular viewport which expands to fit
into the game canvas, but preserving its aspect ratio.
- `CircularViewport` -- a viewport in the shape of a circle, fixed size.


If you add children to the `Viewport` they will appear as static HUDs in front of the world.


## Viewfinder

This part of the camera is responsible for knowing which location in the
Expand All @@ -171,9 +169,33 @@ main character who is displayed not in the center of the screen but closer to
the lower-left corner. This off-center position would be the "logical center"
of the camera, controlled by the viewfinder's `anchor`.

Components added to the `Viewfinder` as children will be rendered as if they
were part of the world (but on top). It is more useful to add behavioral
components to the viewfinder, for example [](effects.md) or other controllers.
If you add children to the `Viewfinder` they will appear will appear in front
of the world, but behind the viewport and with the same transformations as are
applied to the world, so these components are not static.

You can also add behavioral components as children to the viewfinder, for
example [](effects.md) or other controllers. If you for example would add a
`ScaleEffect` you would be able to achieve a smooth zoom in your game.


## Backdrop

To add static components behind the world you can add them to the `backdrop`
component, or replace the `backdrop` component. This is for example useful if
you want to have a static `ParallaxComponent` beneath a world that you can move
around it.

Example:

```dart
camera.backdrop.add(MyStaticBackground());
```

or

```dart
camera.backdrop = MyStaticBackground();
```


## Camera controls
Expand Down
16 changes: 8 additions & 8 deletions examples/.metadata
@@ -1,23 +1,23 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
# This file should be version controlled and should not be manually edited.

version:
revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
channel: stable
revision: "367f9ea16bfae1ca451b9cc27c1366870b187ae2"
channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
- platform: android
create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8
create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2
base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2
- platform: linux
create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2
base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2

# User provided section

Expand Down
Expand Up @@ -45,13 +45,8 @@ void addCameraAndViewportStories(Dashbook dashbook) {
..add(
'Fixed Resolution viewport',
(context) {
return GameWidget(
game: FixedResolutionExample(
viewportResolution: Vector2(
context.numberProperty('viewport width', 600),
context.numberProperty('viewport height', 1024),
),
),
return const GameWidget.controlled(
gameFactory: FixedResolutionExample.new,
);
},
codeLink: baseLink('camera_and_viewport/fixed_resolution_example.dart'),
Expand Down
108 changes: 95 additions & 13 deletions examples/lib/stories/camera_and_viewport/fixed_resolution_example.dart
@@ -1,9 +1,10 @@
import 'dart:ui';

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

class FixedResolutionExample extends FlameGame
with ScrollDetector, ScaleDetector {
Expand All @@ -12,32 +13,84 @@ class FixedResolutionExample extends FlameGame
It is useful when you want the visible part of the game to be the same on
all devices no matter the actual screen size of the device.
Resize the window or change device orientation to see the difference.
If you tap once you will set the zoom to 2 and if you tap again it goes back
to 1, so that you can test how it works with a zoom level.
''';

FixedResolutionExample({
required Vector2 viewportResolution,
}) : super(
camera: CameraComponent.withFixedResolution(
width: viewportResolution.x,
height: viewportResolution.y,
),
FixedResolutionExample()
: super(
camera: CameraComponent.withFixedResolution(width: 600, height: 1024),
world: FixedResolutionWorld(),
);

@override
Future<void> onLoad() async {
final flameSprite = await loadSprite('layers/player.png');
final textRenderer = TextPaint(
style: TextStyle(fontSize: 25, color: BasicPalette.black.color),
);
camera.viewport.add(
TextButton(
text: 'Viewport\ncomponent',
position: Vector2.all(10),
textRenderer: textRenderer,
),
);
camera.viewfinder.add(
TextButton(
text: 'Viewfinder\ncomponent',
textRenderer: textRenderer,
position: Vector2(0, 200),
anchor: Anchor.center,
),
);
camera.viewport.add(
TextButton(
text: 'Viewport\ncomponent',
position: camera.viewport.virtualSize - Vector2.all(10),
textRenderer: textRenderer,
anchor: Anchor.bottomRight,
),
);
}
}

class FixedResolutionWorld extends World
with HasGameReference, TapCallbacks, DoubleTapCallbacks {
final red = BasicPalette.red.paint();

@override
Future<void> onLoad() async {
final flameSprite = await game.loadSprite('layers/player.png');

world.add(Background());
world.add(
add(Background());
add(
SpriteComponent(
sprite: flameSprite,
size: Vector2(149, 211),
)..anchor = Anchor.center,
);
}

@override
void onTapDown(TapDownEvent event) {
add(
CircleComponent(
radius: 2,
position: event.localPosition,
paint: red,
),
);
}

@override
void onDoubleTapDown(DoubleTapDownEvent event) {
final currentZoom = game.camera.viewfinder.zoom;
game.camera.viewfinder.zoom = currentZoom > 1 ? 1 : 2;
}
}

class Background extends PositionComponent with HasGameRef {
class Background extends PositionComponent {
@override
int priority = -1;

Expand All @@ -57,3 +110,32 @@ class Background extends PositionComponent with HasGameRef {
canvas.drawRect(hugeRect, white);
}
}

class TextButton extends ButtonComponent {
TextButton({
required String text,
required super.position,
super.anchor,
TextRenderer? textRenderer,
}) : super(
button: RectangleComponent(
size: Vector2(200, 100),
paint: Paint()
..color = Colors.orange
..strokeWidth = 2
..style = PaintingStyle.stroke,
),
buttonDown: RectangleComponent(
size: Vector2(200, 100),
paint: Paint()..color = BasicPalette.orange.color.withOpacity(0.5),
),
children: [
TextComponent(
text: text,
textRenderer: textRenderer,
position: Vector2(100, 50),
anchor: Anchor.center,
),
],
);
}
1 change: 1 addition & 0 deletions packages/flame/lib/effects.dart
Expand Up @@ -35,6 +35,7 @@ export 'src/effects/provider_interfaces.dart'
ScaleProvider,
SizeProvider,
ReadOnlyPositionProvider,
ReadOnlyScaleProvider,
ReadOnlySizeProvider,
OpacityProvider;
export 'src/effects/remove_effect.dart';
Expand Down
27 changes: 15 additions & 12 deletions packages/flame/lib/src/camera/camera_component.dart
@@ -1,10 +1,9 @@
import 'dart:ui';

import 'package:flame/extensions.dart';
import 'package:flame/src/camera/behaviors/bounded_position_behavior.dart';
import 'package:flame/src/camera/behaviors/follow_behavior.dart';
import 'package:flame/src/camera/viewfinder.dart';
import 'package:flame/src/camera/viewport.dart';
import 'package:flame/src/camera/viewports/fixed_aspect_ratio_viewport.dart';
import 'package:flame/src/camera/viewports/fixed_resolution_viewport.dart';
import 'package:flame/src/camera/viewports/max_viewport.dart';
import 'package:flame/src/camera/world.dart';
import 'package:flame/src/components/core/component.dart';
Expand All @@ -16,7 +15,6 @@ import 'package:flame/src/effects/move_to_effect.dart';
import 'package:flame/src/effects/provider_interfaces.dart';
import 'package:flame/src/experimental/geometry/shapes/shape.dart';
import 'package:flame/src/game/flame_game.dart';
import 'package:vector_math/vector_math_64.dart';

/// [CameraComponent] is a component through which a [World] is observed.
///
Expand Down Expand Up @@ -70,15 +68,16 @@ class CameraComponent extends Component {
factory CameraComponent.withFixedResolution({
required double width,
required double height,
Viewfinder? viewfinder,
World? world,
Component? backdrop,
List<Component>? hudComponents,
}) {
return CameraComponent(
world: world,
viewport: FixedAspectRatioViewport(aspectRatio: width / height)
viewport: FixedResolutionViewport(resolution: Vector2(width, height))
..addAll(hudComponents ?? []),
viewfinder: Viewfinder()..visibleGameSize = Vector2(width, height),
viewfinder: viewfinder ?? Viewfinder(),
backdrop: backdrop,
);
}
Expand Down Expand Up @@ -175,25 +174,29 @@ class CameraComponent extends Component {
viewport.position.x - viewport.anchor.x * viewport.size.x,
viewport.position.y - viewport.anchor.y * viewport.size.y,
);
backdrop.renderTree(canvas);
// Render the world through the viewport
if ((world?.isMounted ?? false) &&
currentCameras.length < maxCamerasDepth) {
canvas.save();
viewport.clip(canvas);
viewport.transformCanvas(canvas);
backdrop.renderTree(canvas);
canvas.save();
try {
currentCameras.add(this);
canvas.transform(viewfinder.transform.transformMatrix.storage);
canvas.transform2D(viewfinder.transform);
world!.renderFromCamera(canvas);
// Render the viewfinder elements, which will be in front of the world,
// but with the same transforms applied to them.
viewfinder.renderTree(canvas);
} finally {
currentCameras.removeLast();
}
canvas.restore();
// Render the viewport elements, which will be in front of the world.
viewport.renderTree(canvas);
canvas.restore();
}
// Render the viewport elements, which will be in front of the world.
viewport.renderTree(canvas);
// Render the viewfinder elements, which will be in front of the viewport.
viewfinder.renderTree(canvas);
canvas.restore();
}

Expand Down

0 comments on commit 4c762f9

Please sign in to comment.