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: Camera as a component #1355

Merged
merged 35 commits into from
Mar 7, 2022
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
a2b667c
wip on new camera system
st-pasha Feb 4, 2022
0388b09
wip on Viewfinder
st-pasha Feb 4, 2022
2efc82f
wip on Camera2 and Viewfinder
st-pasha Feb 4, 2022
173ded7
Merge branch 'main' into ps/camera2
st-pasha Feb 4, 2022
9f9b926
extend Camera
st-pasha Feb 4, 2022
25f8d5e
initial camera2 example
st-pasha Feb 4, 2022
3c46367
Use World in camera2example
st-pasha Feb 4, 2022
e8f39ec
added magnifying glass
st-pasha Feb 5, 2022
370bd89
added description
st-pasha Feb 5, 2022
17c0ffc
expanded docs for camera2
st-pasha Feb 5, 2022
3b55ab5
adjust docs for Viewport
st-pasha Feb 5, 2022
6e39515
docs for Viewfinder
st-pasha Feb 5, 2022
5284e50
format
st-pasha Feb 5, 2022
c9978fd
docs for world
st-pasha Feb 5, 2022
d6c3df2
format
st-pasha Feb 5, 2022
ad0e237
Apply suggestions from code review
st-pasha Feb 9, 2022
0932007
feat: Added NoiseEffectController (#1356)
st-pasha Feb 6, 2022
040f2f1
fix: remove vector_math dependency (#1361)
christopherfujino Feb 9, 2022
5b3e293
minor
st-pasha Feb 9, 2022
9640e0d
Merge branch 'main' into ps/camera2
st-pasha Feb 9, 2022
a0f5725
Merge branch 'main' into ps/camera2
st-pasha Feb 10, 2022
84989d4
Merge branch 'main' into ps/camera2
st-pasha Feb 12, 2022
ec75cc7
Merge branch 'main' into ps/camera2
st-pasha Feb 23, 2022
3e77fb4
use offset.toVector2()
st-pasha Feb 23, 2022
2fe89f4
added docs; renamed handleResize -> onViewportResize
st-pasha Feb 23, 2022
44bb5e9
Merge branch 'main' into ps/camera2
st-pasha Feb 25, 2022
6e47c47
Merge branch 'main' into ps/camera2
st-pasha Mar 4, 2022
28a0ac0
rename Camera2 -> CameraComponent
st-pasha Mar 4, 2022
916c3a5
Create export experimental.dart file
st-pasha Mar 4, 2022
c8927cf
rename Camera2Example -> CameraComponentExample
st-pasha Mar 4, 2022
101732e
Added doc for the experimental.dart file
st-pasha Mar 4, 2022
567181e
fix re-adding components with children
st-pasha Mar 4, 2022
199b8e0
Added support for admonitions in the docs
st-pasha Mar 4, 2022
b21102a
Documentation for CameraComponent
st-pasha Mar 4, 2022
d2affe3
format
st-pasha Mar 4, 2022
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
522 changes: 522 additions & 0 deletions examples/lib/stories/camera_and_viewport/camera2_example.dart

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:dashbook/dashbook.dart';
import 'package:flame/game.dart';

import '../../commons/commons.dart';
import 'camera2_example.dart';
import 'coordinate_systems_example.dart';
import 'fixed_resolution_example.dart';
import 'follow_component_example.dart';
Expand Down Expand Up @@ -59,5 +60,11 @@ void addCameraAndViewportStories(Dashbook dashbook) {
(context) => const CoordinateSystemsWidget(),
codeLink: baseLink('camera_and_viewport/coordinate_systems_example.dart'),
info: CoordinateSystemsExample.description,
)
..add(
'Camera 2',
(context) => GameWidget(game: Camera2Example()),
codeLink: baseLink('camera_and_viewport/camera2_example.dart'),
info: Camera2Example.description,
);
}
92 changes: 92 additions & 0 deletions packages/flame/lib/src/experimental/camera.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import 'package:meta/meta.dart';

import '../components/component.dart';
import 'max_viewport.dart';
import 'viewfinder.dart';
import 'viewport.dart';
import 'world.dart';

/// [Camera2] 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 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.
///
/// Since [Camera2] is a [Component], it is possible to attach other components
/// to it. In particular, adding components directly to the camera is equivalent
/// to adding them to the camera's parent. Components added to the viewport will
/// be affected by the viewport's position, but not by its clip mask. Such
/// components will be rendered on top of the viewport. Components added to the
/// viewfinder will be rendered as if they were part of the world. That is, they
/// will be affected both by the viewport and the viewfinder.
class Camera2 extends Component {
st-pasha marked this conversation as resolved.
Show resolved Hide resolved
Camera2({
required this.world,
Viewport? viewport,
Viewfinder? viewfinder,
}) : viewport = viewport ?? MaxViewport(),
viewfinder = viewfinder ?? Viewfinder();

/// The [viewport] is the "window" through which the game world is observed.
///
/// Imagine that the world is covered with an infinite sheet of paper, but
/// there is a hole in it. That hole is the viewport: through that aperture
/// the world can be observed. The viewport's size is equal to or smaller
/// than the size of the game canvas. If it is smaller, then the viewport's
/// position specifies where exactly it is placed on the canvas.
final Viewport viewport;

/// The [viewfinder] controls which part of the world is seen through the
/// viewport.
///
/// Thus, viewfinder's `position` is the world point which is seen at the
/// center of the viewport. In addition, viewfinder controls the zoom level
/// (i.e. how much of the world is seen through the viewport), and,
/// optionally, rotation.
final Viewfinder viewfinder;

/// Special component that is designed to be the root of a game world.
///
/// Multiple cameras can observe the same [world] simultaneously, and the
/// world may itself contain cameras that look into other worlds, or even into
/// 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.
World world;

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

/// A camera that currently performs rendering.
///
/// This variable is set to `this` when we begin rendering the world through
/// this particular camera, and reset back to `null` at the end. This variable
/// is not set when rendering components that are attached to the viewport.
static Camera2? get currentCamera {
return currentCameras.isEmpty ? null : currentCameras[0];
}

/// Stack of all current cameras in the render tree.
static final List<Camera2> currentCameras = [];

/// Maximum number of nested cameras that will be rendered.
///
/// This variable helps prevent infinite recursion when a camera is set to
/// look at the world that contains that camera.
static int maxCamerasDepth = 4;
}
23 changes: 23 additions & 0 deletions packages/flame/lib/src/experimental/circular_viewport.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'dart:ui';
import 'package:vector_math/vector_math_64.dart';

import 'viewport.dart';

/// A fixed-size viewport in the shape of a circle.
class CircularViewport extends Viewport {
CircularViewport(double radius) {
size = Vector2.all(2 * radius);
}

Path _clipPath = Path();

@override
void clip(Canvas canvas) => canvas.clipPath(_clipPath, doAntiAlias: false);

@override
void handleResize() {
final x = size.x / 2;
final y = size.y / 2;
_clipPath = Path()..addOval(Rect.fromLTRB(-x, -y, x, y));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import 'dart:ui';

import 'package:vector_math/vector_math_64.dart';

import 'viewport.dart';

class FixedAspectRatioViewport extends Viewport {
FixedAspectRatioViewport({required this.aspectRatio})
: assert(aspectRatio > 0);

final double aspectRatio;
Rect _clipRect = Rect.zero;

@override
void onGameResize(Vector2 canvasSize) {
super.onGameResize(canvasSize);
position = canvasSize / 2;
size = canvasSize;
}

@override
void clip(Canvas canvas) => canvas.clipRect(_clipRect);

@override
void handleResize() {
final desiredWidth = size.y * aspectRatio;
if (desiredWidth > size.x) {
size.y = size.x / aspectRatio;
} else {
size.x = desiredWidth;
}

final x = size.x / 2;
final y = size.y / 2;
_clipRect = Rect.fromLTRB(-x, -y, x, y);
}
}
27 changes: 27 additions & 0 deletions packages/flame/lib/src/experimental/fixed_size_viewport.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import 'dart:ui';

import 'package:vector_math/vector_math_64.dart';

import 'viewport.dart';

/// A rectangular viewport with fixed dimensions.
///
/// You can change the size of this viewport at runtime, but it will not
/// auto-resize when its parent changes size.
class FixedSizeViewport extends Viewport {
FixedSizeViewport(double width, double height) {
size = Vector2(width, height);
}

Rect _clipRect = Rect.zero;

@override
void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false);

@override
void handleResize() {
final x = size.x / 2;
final y = size.y / 2;
_clipRect = Rect.fromLTRB(-x, -y, x, y);
}
}
23 changes: 23 additions & 0 deletions packages/flame/lib/src/experimental/max_viewport.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import 'dart:ui';

import 'package:vector_math/vector_math_64.dart';

import 'viewport.dart';

/// The default viewport, which is as big as the game canvas allows.
///
/// This viewport does not perform any clipping.
class MaxViewport extends Viewport {
@override
void onGameResize(Vector2 gameSize) {
super.onGameResize(gameSize);
size = gameSize;
position = gameSize / 2;
}

@override
void clip(Canvas canvas) {}

@override
void handleResize() {}
}
126 changes: 126 additions & 0 deletions packages/flame/lib/src/experimental/viewfinder.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import 'dart:math';
import 'dart:ui';

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

import '../components/component.dart';
import '../game/transform2d.dart';
import 'camera.dart';
import 'viewport.dart';

/// [Viewfinder] is a part of a [Camera2] system that controls which part of
/// the game world is currently visible through a viewport.
///
/// The viewfinder contains the game point that is currently at the
/// "cross-hairs" of the viewport ([position]), the [zoom] level, and the
/// [angle] of rotation of the camera.
class Viewfinder extends Component {
st-pasha marked this conversation as resolved.
Show resolved Hide resolved
/// Internal transform matrix used by the viewfinder.
final Transform2D _transform = Transform2D();

/// The game coordinates of a point that is to be positioned at the center
/// of the viewport.
Vector2 get position => -_transform.position;
set position(Vector2 value) => _transform.position = -value;

/// Zoom level of the game.
///
/// The default zoom value of 1 means that the world coordinates are in 1:1
/// correspondence with the pixels on the screen. Zoom levels higher than 1
/// make the world appear closer: each unit of game coordinate systems maps
/// to [zoom] pixels on the screen. Conversely, when [zoom] is less than 1,
/// the game world will appear further away and smaller in size.
///
/// See also: [visibleGameSize] for setting the zoom level dynamically.
double get zoom => _transform.scale.x;
set zoom(double value) {
assert(value > 0, 'zoom level must be positive: $value');
_transform.scale = Vector2.all(value);
}

/// Rotation angle of the game world, in radians.
///
/// The rotation is around the axis that is perpendicular to the screen.
double get angle => -_transform.angle;
set angle(double value) => _transform.angle = -value;

/// Reference to the parent camera.
Camera2 get camera => parent! as Camera2;
st-pasha marked this conversation as resolved.
Show resolved Hide resolved

/// How much of a game world ought to be visible through the viewport.
///
/// When this property is non-null, the viewfinder will automatically select
/// the maximum zoom level such that a rectangle of size [visibleGameSize]
/// (in game coordinates) is visible through the viewport. If you want a
/// certain dimension to be unconstrained, set it to zero.
///
/// For example, if `visibleGameSize` is set to `[100.0, 0.0]`, the zoom level
/// will be chosen such that 100 game units will be visible across the width
/// of the viewport. Likewise, setting `visibleGameSize` to `[5.0, 10.0]`
/// will ensure that 5 or more game units are visible across the width of the
/// viewport, and 10 or more game units across the height.
///
/// This property is an alternative way to set the [zoom] level for the
/// viewfinder. It is persistent too: if the game size changes, the zoom
/// will be recalculated to fit the constraint.
Vector2? get visibleGameSize => _visibleGameSize;
Vector2? _visibleGameSize;
set visibleGameSize(Vector2? value) {
if (value == null || (value.x == 0 && value.y == 0)) {
_visibleGameSize = null;
} else {
assert(
value.x >= 0 && value.y >= 0,
'visibleGameSize cannot be negative: $value',
);
_visibleGameSize = value;
_initZoom();
}
}

/// Set [zoom] level based on the [_visibleGameSize].
void _initZoom() {
if (isMounted && _visibleGameSize != null) {
final viewportSize = camera.viewport.size;
final zoomX = viewportSize.x / _visibleGameSize!.x;
final zoomY = viewportSize.y / _visibleGameSize!.y;
zoom = min(zoomX, zoomY);
}
}

@override
void onGameResize(Vector2 size) {
super.onGameResize(size);
_initZoom();
}

@mustCallSuper
@override
void onMount() {
assert(parent! is Camera2, 'Viewfinder can only be mounted to a Camera2');
_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 &&
Camera2.currentCameras.length < Camera2.maxCamerasDepth) {
try {
Camera2.currentCameras.add(camera);
canvas.transform(_transform.transformMatrix.storage);
world.renderFromCamera(canvas);
super.renderTree(canvas);
} finally {
Camera2.currentCameras.removeLast();
}
}
}
}
Loading