Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Introduce the FixedResolutionViewport #2796

Merged
merged 23 commits into from
Oct 9, 2023
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 39 additions & 17 deletions doc/flame/camera_component.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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),
spydon marked this conversation as resolved.
Show resolved Hide resolved
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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))
spydon marked this conversation as resolved.
Show resolved Hide resolved
..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
Loading