Skip to content

Commit

Permalink
feat: Added .anchor property to CameraComponent.Viewfinder (#1458)
Browse files Browse the repository at this point in the history
  • Loading branch information
st-pasha committed Mar 18, 2022
1 parent 101f728 commit d51dc5e
Show file tree
Hide file tree
Showing 9 changed files with 217 additions and 62 deletions.
9 changes: 8 additions & 1 deletion doc/flame/camera_component.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ This part of the camera is responsible for knowing which location in the
underlying game world we are currently looking at. The `Viewfinder` also
controls the zoom level, and the rotation angle of the view.

The `anchor` property of the viewfinder allows you to designate which point
inside the viewport serves as a "logical center" of the camera. For example,
in side-scrolling action games it is common to have the camera focused on the
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.
Expand All @@ -109,6 +116,6 @@ Pros:
- (NYI) Effects can be applied either to the viewport, or to the viewfinder;
- (NYI) More flexible camera controllers.

Cons (we are planning to address these in the near future):
Cons (we are planning to eliminate these in the near future):
- Camera controls are not yet implemented;
- Events propagation may not always work correctly.
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'package:flame/game.dart';

import '../../commons/commons.dart';
import 'camera_component_example.dart';
import 'camera_component_properties_example.dart';
import 'coordinate_systems_example.dart';
import 'fixed_resolution_example.dart';
import 'follow_component_example.dart';
Expand Down Expand Up @@ -66,5 +67,13 @@ void addCameraAndViewportStories(Dashbook dashbook) {
(context) => GameWidget(game: CameraComponentExample()),
codeLink: baseLink('camera_and_viewport/camera_component_example.dart'),
info: CameraComponentExample.description,
)
..add(
'CameraComponent properties',
(context) => GameWidget(game: CameraComponentPropertiesExample()),
codeLink: baseLink(
'camera_and_viewport/camera_component_properties_example.dart',
),
info: CameraComponentPropertiesExample.description,
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,13 @@ class CameraComponentExample extends FlameGame with PanDetector {
}

void _updateMagnifyingGlassPosition(Vector2 point) {
// shifts the original [point] by 1.4142*radius, which happens to be in the
// middle of a handle
// [point] is in the canvas coordinate system.
// This shifts the original [point] by 1.4142*radius, which happens to be
// in the middle of the magnifying glass' handle.
final handlePoint = point - Vector2.all(radius);
magnifyingGlass.viewport.position = handlePoint;
magnifyingGlass.viewfinder.position =
(handlePoint - canvasSize / 2 + center) * zoom;
magnifyingGlass
..viewport.position = handlePoint
..viewfinder.position = handlePoint - canvasSize / 2 + center;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame/experimental.dart';
import 'package:flame/game.dart' hide Viewport;

class CameraComponentPropertiesExample extends FlameGame {
static const description = '''
This example uses FixedSizeViewport which is dynamically sized and
positioned based on the size of the game widget.
The underlying world is represented as a simple coordinate plane, with
green dot being the origin. The viewfinder uses custom anchor in order to
declare its "center" half-way between the bottom left corner and the true
center.
''';

CameraComponent? _camera;

@override
Color backgroundColor() => const Color(0xff333333);

@override
Future<void> onLoad() async {
final world = World();
world.add(Background());
_camera = CameraComponent(
world: world,
viewport: FixedSizeViewport(200, 200)..add(ViewportFrame()),
)
..viewfinder.zoom = 5
..viewfinder.anchor = const Anchor(0.25, 0.75);
await add(world);
await add(_camera!);
onGameResize(canvasSize);
}

@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
_camera?.viewport.size = size * 0.7;
_camera?.viewport.position = size * 0.6;
}
}

class ViewportFrame extends Component {
final paint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3
..color = const Color(0xff87c4e2);

@override
void render(Canvas canvas) {
final size = (parent! as Viewport).size;
canvas.drawRRect(
RRect.fromRectAndRadius(
Rect.fromLTWH(-size.x / 2, -size.y / 2, size.x, size.y),
const Radius.circular(5),
),
paint,
);
}
}

class Background extends Component {
final bgPaint = Paint()..color = const Color(0xffff0000);
final originPaint = Paint()..color = const Color(0xff2f8750);
final axisPaint = Paint()
..strokeWidth = 1
..style = PaintingStyle.stroke
..color = const Color(0xff878787);
final gridPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 0
..color = const Color(0xff555555);

@override
void render(Canvas canvas) {
canvas.drawColor(const Color(0xff000000), BlendMode.src);
for (var i = -100.0; i <= 100.0; i += 10) {
canvas.drawLine(Offset(i, -100), Offset(i, 100), gridPaint);
canvas.drawLine(Offset(-100, i), Offset(100, i), gridPaint);
}
canvas.drawLine(Offset.zero, const Offset(0, 10), axisPaint);
canvas.drawLine(Offset.zero, const Offset(10, 0), axisPaint);
canvas.drawCircle(Offset.zero, 1.0, originPaint);
}
}
48 changes: 37 additions & 11 deletions packages/flame/lib/src/experimental/camera_component.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:ui';

import 'package:meta/meta.dart';

import '../components/component.dart';
Expand All @@ -8,17 +10,17 @@ import 'world.dart';

/// [CameraComponent] is a component through which a [World] is observed.
///
/// A camera consists of two main parts: a [Viewport] and a [Viewfinder]. It
/// also a references a [World] component, and by "references" we mean that the
/// world is not mounted to the camera, but the camera merely knows about the
/// world, which may exist anywhere in the game tree.
/// A camera consists of two parts: a [Viewport], and a [Viewfinder]. It also
/// references a [World] component, which is not mounted to the camera, but the
/// camera still knows about it. The world must be mounted somewhere else in
/// the game tree.
///
/// A camera is a regular component that can be placed anywhere in the game
/// tree. Most games will have at least one "main" camera for displaying the
/// main game world. However, additional cameras may also be used for some
/// special effects. These extra cameras may be placed either in parallel with
/// the main camera, or even within the world itself. It is even possible to
/// create a camera that looks at itself.
/// the main camera, or within the world. It is even possible to create a camera
/// that looks at itself.
///
/// Since [CameraComponent] is a [Component], it is possible to attach other
/// components to it. In particular, adding components directly to the camera is
Expand Down Expand Up @@ -60,16 +62,40 @@ class CameraComponent extends Component {
/// itself.
///
/// The [world] component is generally mounted externally to the camera, and
/// this variable is a mere reference to it. In practice, the [world] may be
/// mounted anywhere in the game tree, including inside the camera if you
/// wish so.
/// this variable is a mere reference to it.
World world;

@mustCallSuper
@override
Future<void> onLoad() async {
await add(viewport);
await add(viewfinder);
await addAll([viewport, viewfinder]);
}

/// Renders the [world] as seen through this camera.
///
/// If the world is not mounted yet, only the viewport HUD elements will be
/// rendered.
@override
void renderTree(Canvas canvas) {
canvas.save();
canvas.translate(viewport.position.x, viewport.position.y);
// Render the world through the viewport
if (world.isMounted && currentCameras.length < maxCamerasDepth) {
canvas.save();
viewport.clip(canvas);
try {
currentCameras.add(this);
canvas.transform(viewfinder.transformMatrix.storage);
world.renderFromCamera(canvas);
viewfinder.renderTree(canvas);
} finally {
currentCameras.removeLast();
}
canvas.restore();
}
// Now render the HUD elements
viewport.renderTree(canvas);
canvas.restore();
}

/// A camera that currently performs rendering.
Expand Down
3 changes: 2 additions & 1 deletion packages/flame/lib/src/experimental/fixed_size_viewport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import 'viewport.dart';
class FixedSizeViewport extends Viewport {
FixedSizeViewport(double width, double height) {
size = Vector2(width, height);
onViewportResize();
}

Rect _clipRect = Rect.zero;
late Rect _clipRect;

@override
void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false);
Expand Down
58 changes: 32 additions & 26 deletions packages/flame/lib/src/experimental/viewfinder.dart
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import 'dart:math';
import 'dart:ui';

import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';

import '../anchor.dart';
import '../components/component.dart';
import '../game/transform2d.dart';
import 'camera_component.dart';
import 'viewport.dart';

/// [Viewfinder] is a part of a [CameraComponent] system that controls which
/// part of the game world is currently visible through a viewport.
Expand All @@ -19,6 +18,9 @@ class Viewfinder extends Component {
/// Internal transform matrix used by the viewfinder.
final Transform2D _transform = Transform2D();

@internal
Matrix4 get transformMatrix => _transform.transformMatrix;

/// The game coordinates of a point that is to be positioned at the center
/// of the viewport.
Vector2 get position => -_transform.offset;
Expand All @@ -45,6 +47,22 @@ class Viewfinder extends Component {
double get angle => -_transform.angle;
set angle(double value) => _transform.angle = -value;

/// The point within the viewport that is considered the "logical center" of
/// the camera.
///
/// This anchor is relative to the viewport's bounding rect, and by default
/// is at the center of the viewport.
///
/// The "logical center" of the camera means the point within the viewport
/// where the viewfinder's focus is located at. It is at this point within
/// the viewport that the world's point [position] will be displayed.
Anchor get anchor => _anchor;
Anchor _anchor = Anchor.center;
set anchor(Anchor value) {
_anchor = value;
onViewportResize();
}

/// Reference to the parent camera.
CameraComponent get camera => parent! as CameraComponent;

Expand Down Expand Up @@ -95,36 +113,24 @@ class Viewfinder extends Component {
_initZoom();
}

/// Called by the viewport when its size changes.
@internal
void onViewportResize() {
if (parent != null) {
final viewportSize = camera.viewport.size;
_transform.position.x = viewportSize.x * (_anchor.x - 0.5);
_transform.position.y = viewportSize.y * (_anchor.y - 0.5);
}
}

@mustCallSuper
@override
void onMount() {
assert(
parent! is CameraComponent,
'Viewfinder can only be mounted to a Camera2',
'Viewfinder can only be mounted to a CameraComponent',
);
_initZoom();
}

@override
void renderTree(Canvas canvas) {}

/// Internal rendering method called by the [Viewport] (regular rendering is
/// disabled). This ensures that the viewfinder performs its rendering only
/// after the viewport applied the necessary transforms / clip mask.
@internal
void renderFromViewport(Canvas canvas) {
final world = camera.world;
if (world.isMounted &&
CameraComponent.currentCameras.length <
CameraComponent.maxCamerasDepth) {
try {
CameraComponent.currentCameras.add(camera);
canvas.transform(_transform.transformMatrix.storage);
world.renderFromCamera(canvas);
super.renderTree(canvas);
} finally {
CameraComponent.currentCameras.removeLast();
}
}
onViewportResize();
}
}
23 changes: 7 additions & 16 deletions packages/flame/lib/src/experimental/viewport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -43,17 +43,22 @@ abstract class Viewport extends Component {
"Viewport's size cannot be negative: $value",
);
_size.setFrom(value);
if (isMounted) {
camera.viewfinder.onViewportResize();
}
onViewportResize();
}

/// Reference to the parent camera.
CameraComponent get camera => parent! as CameraComponent;

/// Apply clip mask to the [canvas].
///
/// The mask must be in the viewport's local coordinate system, where the
/// center of the viewport has coordinates (0, 0). The overall size of the
/// clip mask's shape must match the [size] of the viewport.
///
/// This API must be implemented by all viewports.
@protected
void clip(Canvas canvas);

/// Override in order to perform a custom action upon resize.
Expand All @@ -68,21 +73,7 @@ abstract class Viewport extends Component {
void onMount() {
assert(
parent! is CameraComponent,
'A Viewport may only be attached to a Camera2',
'A Viewport may only be attached to a CameraComponent',
);
}

@override
void renderTree(Canvas canvas) {
final camera = parent! as CameraComponent;
canvas.save();
canvas.translate(_position.x, _position.y);
canvas.save();
clip(canvas);
camera.viewfinder.renderFromViewport(canvas);
canvas.restore();
// Render viewport's children
super.renderTree(canvas);
canvas.restore();
}
}
Loading

0 comments on commit d51dc5e

Please sign in to comment.