From a2b667cfe21b35ccadbd632ee8973b23e472f060 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Feb 2022 02:57:12 -0800 Subject: [PATCH 01/28] wip on new camera system --- .../flame/lib/src/experimental/camera.dart | 6 ++ .../src/experimental/circular_viewport.dart | 24 ++++++++ .../fixed_aspect_ratio_viewport.dart | 37 ++++++++++++ .../src/experimental/fixed_size_viewport.dart | 28 +++++++++ .../lib/src/experimental/max_viewport.dart | 17 ++++++ .../lib/src/experimental/viewfinder.dart | 14 +++++ .../flame/lib/src/experimental/viewport.dart | 60 +++++++++++++++++++ 7 files changed, 186 insertions(+) create mode 100644 packages/flame/lib/src/experimental/camera.dart create mode 100644 packages/flame/lib/src/experimental/circular_viewport.dart create mode 100644 packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart create mode 100644 packages/flame/lib/src/experimental/fixed_size_viewport.dart create mode 100644 packages/flame/lib/src/experimental/max_viewport.dart create mode 100644 packages/flame/lib/src/experimental/viewfinder.dart create mode 100644 packages/flame/lib/src/experimental/viewport.dart diff --git a/packages/flame/lib/src/experimental/camera.dart b/packages/flame/lib/src/experimental/camera.dart new file mode 100644 index 00000000000..ed22c709c0a --- /dev/null +++ b/packages/flame/lib/src/experimental/camera.dart @@ -0,0 +1,6 @@ + +import '../components/component.dart'; + +class Camera extends Component { + +} diff --git a/packages/flame/lib/src/experimental/circular_viewport.dart b/packages/flame/lib/src/experimental/circular_viewport.dart new file mode 100644 index 00000000000..a382385d390 --- /dev/null +++ b/packages/flame/lib/src/experimental/circular_viewport.dart @@ -0,0 +1,24 @@ + +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 handleResize() { + final x = size.x / 2; + final y = size.y / 2; + _clipPath = Path()..addOval(Rect.fromLTRB(-x, -y, x, y)); + } + + @override + void clip(Canvas canvas) => canvas.clipPath(_clipPath, doAntiAlias: false); +} diff --git a/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart new file mode 100644 index 00000000000..e31b55c1225 --- /dev/null +++ b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart @@ -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 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); + } + + @override + void clip(Canvas canvas) => canvas.clipRect(_clipRect); +} diff --git a/packages/flame/lib/src/experimental/fixed_size_viewport.dart b/packages/flame/lib/src/experimental/fixed_size_viewport.dart new file mode 100644 index 00000000000..efed81cb61d --- /dev/null +++ b/packages/flame/lib/src/experimental/fixed_size_viewport.dart @@ -0,0 +1,28 @@ + +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 handleResize() { + final x = size.x / 2; + final y = size.y / 2; + _clipRect = Rect.fromLTRB(-x, -y, x, y); + } + + @override + void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false); +} diff --git a/packages/flame/lib/src/experimental/max_viewport.dart b/packages/flame/lib/src/experimental/max_viewport.dart new file mode 100644 index 00000000000..05cea00d0d3 --- /dev/null +++ b/packages/flame/lib/src/experimental/max_viewport.dart @@ -0,0 +1,17 @@ +import 'dart:ui'; + +import 'package:vector_math/vector_math_64.dart'; + +import 'viewport.dart'; + +class MaxViewport extends Viewport { + @override + void onGameResize(Vector2 gameSize) { + super.onGameResize(gameSize); + size = gameSize; + position = gameSize / 2; + } + + @override + void clip(Canvas canvas) {} +} diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart new file mode 100644 index 00000000000..330a9019ae6 --- /dev/null +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -0,0 +1,14 @@ + +import 'dart:ui'; + +import '../components/component.dart'; + +class Viewfinder extends Component { + + @override + void renderTree(Canvas canvas) {} + + void renderFromViewport(Canvas canvas) { + + } +} diff --git a/packages/flame/lib/src/experimental/viewport.dart b/packages/flame/lib/src/experimental/viewport.dart new file mode 100644 index 00000000000..51e553f19fe --- /dev/null +++ b/packages/flame/lib/src/experimental/viewport.dart @@ -0,0 +1,60 @@ + +import 'dart:ui'; + +import 'package:meta/meta.dart'; +import 'package:vector_math/vector_math_64.dart'; + +import '../components/component.dart'; +import 'viewfinder.dart'; + +abstract class Viewport extends Component { + /// Position of the viewport's center in the parent's coordinate frame. + Vector2 get position => _position; + final Vector2 _position = Vector2.zero(); + set position(Vector2 value) { + _position.setFrom(value); + } + + /// Size of the viewport, i.e. width and height. + /// + /// This property represents the bounding box of the viewport. If the viewport + /// is rectangular in shape, then [size] describes the dimensions of that + /// rectangle. If the viewport has any other shape (for example, circular), + /// then [size] describes the dimensions of the bounding box of the viewport. + /// + /// Changing the size at runtime triggers the [handleResize] event. + Vector2 get size => _size; + final Vector2 _size = Vector2.zero(); + set size(Vector2 value) { + _size.setFrom(value); + handleResize(); + } + + late Viewfinder _viewfinder; + + @internal + void setViewfinder(Viewfinder vf) => _viewfinder = vf; + + @override + void renderTree(Canvas canvas) { + canvas.save(); + canvas.translate(_position.x, _position.y); + canvas.save(); + clip(canvas); + _viewfinder.renderFromViewport(canvas); + canvas.restore(); + // Render viewport's children + super.renderTree(canvas); + canvas.restore(); + } + + @protected + void clip(Canvas canvas); + + /// Override in order to perform a custom action upon resize. + /// + /// A typical use-case would be to adjust the viewport's clip mask to match + /// the new size. + @protected + void handleResize() {} +} From 0388b09c82a252c31c24a2a4bb0750ed8fa992b9 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Feb 2022 03:06:41 -0800 Subject: [PATCH 02/28] wip on Viewfinder --- .../flame/lib/src/experimental/viewfinder.dart | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index 330a9019ae6..ccc57e72ca6 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -1,10 +1,24 @@ import 'dart:ui'; +import 'package:vector_math/vector_math_64.dart'; + import '../components/component.dart'; +import '../game/transform2d.dart'; class Viewfinder extends Component { + final Transform2D _transform = Transform2D(); + + Vector2 get position => _transform.position; + set position(Vector2 value) => _transform.position = value; + + double get zoom => _transform.scale.x; + set zoom(double value) => _transform.scale = Vector2.all(value); + + double get angle => _transform.angle; + set angle(double value) => _transform.angle; + @override void renderTree(Canvas canvas) {} From 2efc82f6cb8d6c259548c675d40d6ea8e90c377b Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Feb 2022 03:43:55 -0800 Subject: [PATCH 03/28] wip on Camera2 and Viewfinder --- .../flame/lib/src/experimental/camera.dart | 18 ++++++++-- .../lib/src/experimental/viewfinder.dart | 35 ++++++++++++++++++- .../flame/lib/src/experimental/viewport.dart | 8 ++--- 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/packages/flame/lib/src/experimental/camera.dart b/packages/flame/lib/src/experimental/camera.dart index ed22c709c0a..e4a0e7ffa47 100644 --- a/packages/flame/lib/src/experimental/camera.dart +++ b/packages/flame/lib/src/experimental/camera.dart @@ -1,6 +1,20 @@ - import '../components/component.dart'; +import 'max_viewport.dart'; +import 'viewfinder.dart'; +import 'viewport.dart'; + +class Camera2 extends Component { + Camera2({ + Viewport? viewport, + }) : viewport = viewport ?? MaxViewport(), + viewfinder = Viewfinder(); -class Camera extends Component { + @override + Future onLoad() async { + await add(viewport); + await add(viewfinder); + } + final Viewport viewport; + final Viewfinder viewfinder; } diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index ccc57e72ca6..d9f686128c2 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -5,6 +5,7 @@ import 'package:vector_math/vector_math_64.dart'; import '../components/component.dart'; import '../game/transform2d.dart'; +import 'camera.dart'; class Viewfinder extends Component { @@ -19,10 +20,42 @@ class Viewfinder extends Component { double get angle => _transform.angle; set angle(double value) => _transform.angle; + double? get visibleGameWidth => _visibleGameWidth; + double? _visibleGameWidth; + set visibleGameWidth(double? value) { + _visibleGameWidth = value; + _visibleGameHeight = null; + _initZoom(); + } + + double? get visibleGameHeight => _visibleGameHeight; + double? _visibleGameHeight; + set visibleGameHeight(double? value) { + _visibleGameWidth = null; + _visibleGameHeight = value; + _initZoom(); + } + + void _initZoom() { + if (isMounted) { + if (_visibleGameWidth != null) { + zoom = (parent! as Camera2).viewport.size.x / _visibleGameWidth!; + } + if (_visibleGameHeight != null) { + zoom = (parent! as Camera2).viewport.size.y / _visibleGameHeight!; + } + } + } + + @override + void onMount() { + _initZoom(); + } + @override void renderTree(Canvas canvas) {} void renderFromViewport(Canvas canvas) { - + canvas.transform(_transform.transformMatrix.storage); } } diff --git a/packages/flame/lib/src/experimental/viewport.dart b/packages/flame/lib/src/experimental/viewport.dart index 51e553f19fe..0c9af0f5268 100644 --- a/packages/flame/lib/src/experimental/viewport.dart +++ b/packages/flame/lib/src/experimental/viewport.dart @@ -5,6 +5,7 @@ import 'package:meta/meta.dart'; import 'package:vector_math/vector_math_64.dart'; import '../components/component.dart'; +import 'camera.dart'; import 'viewfinder.dart'; abstract class Viewport extends Component { @@ -30,18 +31,13 @@ abstract class Viewport extends Component { handleResize(); } - late Viewfinder _viewfinder; - - @internal - void setViewfinder(Viewfinder vf) => _viewfinder = vf; - @override void renderTree(Canvas canvas) { canvas.save(); canvas.translate(_position.x, _position.y); canvas.save(); clip(canvas); - _viewfinder.renderFromViewport(canvas); + (parent! as Camera2).viewfinder.renderFromViewport(canvas); canvas.restore(); // Render viewport's children super.renderTree(canvas); From 9f9b926b1a570c24841d9f55028a81074082255d Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Feb 2022 13:14:29 -0800 Subject: [PATCH 04/28] extend Camera --- .../flame/lib/src/experimental/camera.dart | 46 ++++++++++++++++++- .../lib/src/experimental/viewfinder.dart | 20 ++++++-- .../flame/lib/src/experimental/viewport.dart | 3 +- .../flame/lib/src/experimental/world.dart | 20 ++++++++ 4 files changed, 82 insertions(+), 7 deletions(-) create mode 100644 packages/flame/lib/src/experimental/world.dart diff --git a/packages/flame/lib/src/experimental/camera.dart b/packages/flame/lib/src/experimental/camera.dart index e4a0e7ffa47..036009dc08d 100644 --- a/packages/flame/lib/src/experimental/camera.dart +++ b/packages/flame/lib/src/experimental/camera.dart @@ -2,12 +2,39 @@ 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 exists somewhere else in the game tree. +/// +/// 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. +/// +/// 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. +/// +/// The [world] is a special component that is designed to be the root of a +/// game world. Multiple cameras can observe the world simultaneously, and the +/// world may itself contain cameras that look into other worlds, or even the +/// same world. class Camera2 extends Component { Camera2({ + required this.world, Viewport? viewport, + Viewfinder? viewfinder, }) : viewport = viewport ?? MaxViewport(), - viewfinder = Viewfinder(); + viewfinder = viewfinder ?? Viewfinder(); @override Future onLoad() async { @@ -17,4 +44,21 @@ class Camera2 extends Component { final Viewport viewport; final Viewfinder viewfinder; + World world; + + /// 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]; + } + static final List 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; } diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index d9f686128c2..11370e53ee4 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -1,4 +1,3 @@ - import 'dart:ui'; import 'package:vector_math/vector_math_64.dart'; @@ -8,7 +7,6 @@ import '../game/transform2d.dart'; import 'camera.dart'; class Viewfinder extends Component { - final Transform2D _transform = Transform2D(); Vector2 get position => _transform.position; @@ -36,13 +34,15 @@ class Viewfinder extends Component { _initZoom(); } + Camera2 get camera => parent! as Camera2; + void _initZoom() { if (isMounted) { if (_visibleGameWidth != null) { - zoom = (parent! as Camera2).viewport.size.x / _visibleGameWidth!; + zoom = camera.viewport.size.x / _visibleGameWidth!; } if (_visibleGameHeight != null) { - zoom = (parent! as Camera2).viewport.size.y / _visibleGameHeight!; + zoom = camera.viewport.size.y / _visibleGameHeight!; } } } @@ -56,6 +56,16 @@ class Viewfinder extends Component { void renderTree(Canvas canvas) {} void renderFromViewport(Canvas canvas) { - canvas.transform(_transform.transformMatrix.storage); + final world = camera.world; + if (world.isMounted && + Camera2.currentCameras.length < Camera2.maxCamerasDepth) { + try { + Camera2.currentCameras.add(camera); + canvas.transform(_transform.transformMatrix.storage); + world.renderTree(canvas); + } finally { + Camera2.currentCameras.removeLast(); + } + } } } diff --git a/packages/flame/lib/src/experimental/viewport.dart b/packages/flame/lib/src/experimental/viewport.dart index 0c9af0f5268..c270ed0e279 100644 --- a/packages/flame/lib/src/experimental/viewport.dart +++ b/packages/flame/lib/src/experimental/viewport.dart @@ -33,11 +33,12 @@ abstract class Viewport extends Component { @override void renderTree(Canvas canvas) { + final camera = parent! as Camera2; canvas.save(); canvas.translate(_position.x, _position.y); canvas.save(); clip(canvas); - (parent! as Camera2).viewfinder.renderFromViewport(canvas); + camera.viewfinder.renderFromViewport(canvas); canvas.restore(); // Render viewport's children super.renderTree(canvas); diff --git a/packages/flame/lib/src/experimental/world.dart b/packages/flame/lib/src/experimental/world.dart new file mode 100644 index 00000000000..f07e75211db --- /dev/null +++ b/packages/flame/lib/src/experimental/world.dart @@ -0,0 +1,20 @@ + +import 'dart:ui'; + +import 'package:meta/meta.dart'; + +import '../components/component.dart'; +import 'camera.dart'; + +class World extends Component { + // World may only be rendered through a camera, so regular [renderTree] is + // disabled. + @override + void renderTree(Canvas canvas) {} + + @internal + void renderFromCamera(Canvas canvas) { + assert(Camera2.currentCamera != null); + super.renderTree(canvas); + } +} From 25f8d5e7503b0bd5f285c03ee03b8ea06f215f09 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Feb 2022 13:48:28 -0800 Subject: [PATCH 05/28] initial camera2 example --- .../stories/camera_and_viewport/camera2.dart | 377 ++++++++++++++++++ 1 file changed, 377 insertions(+) create mode 100644 examples/lib/stories/camera_and_viewport/camera2.dart diff --git a/examples/lib/stories/camera_and_viewport/camera2.dart b/examples/lib/stories/camera_and_viewport/camera2.dart new file mode 100644 index 00000000000..09ce9d0453e --- /dev/null +++ b/examples/lib/stories/camera_and_viewport/camera2.dart @@ -0,0 +1,377 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flutter/widgets.dart'; + +Future main() async { + runApp(GameWidget(game: Camera2Example())); +} + +class Camera2Example extends FlameGame { + @override + Color backgroundColor() => const Color(0xFFffffff); + + @override + Future onLoad() async { + final random = Random(); + final curve = DragonCurve(); + add(curve); + + const baseColor = HSVColor.fromAHSV(1, 38.5, 0.63, 0.68); + for (var i = 0; i < 20; i++) { + add( + Ant() + ..color = baseColor.withHue(random.nextDouble() * 360).toColor() + ..scale = Vector2.all(.4) + ..setTravelPath(curve.path), + ); + } + final center = curve.boundingRect().center; + camera.snapTo(Vector2(center.dx, center.dy)); + camera.setRelativeOffset(Anchor.center); + } +} + +class DragonCurve extends PositionComponent { + DragonCurve() { + initPath(); + } + + late final Paint borderPaint; + late final Paint mainPaint; + late final Path dragon; + late List path; + static const cellSize = 20.0; + static const notchSize = 4.0; + + void initPath() { + path = [ + Vector2(0, cellSize - notchSize), + Vector2(0, notchSize), + ]; + final endPoint = Vector2(0, cellSize); + final transform = Transform2D()..angleDegrees = -90; + for (var i = 0; i < 8; i++) { + path += List.from(path.reversed.map(transform.localToGlobal)); + final pivot = transform.localToGlobal(endPoint); + transform + ..position = pivot + ..offset = -pivot; + } + } + + Rect boundingRect() { + var minX = double.infinity; + var minY = double.infinity; + var maxX = -double.infinity; + var maxY = -double.infinity; + for (final point in path) { + minX = min(minX, point.x); + minY = min(minY, point.y); + maxX = max(maxX, point.x); + maxY = max(maxY, point.y); + } + return Rect.fromLTRB(minX, minY, maxX, maxY); + } + + @override + void onMount() { + borderPaint = Paint() + ..color = const Color(0xFF041D1F) + ..style = PaintingStyle.stroke + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 0.3) + ..strokeWidth = 4; + mainPaint = Paint() + ..color = const Color(0xffefe79c) + ..style = PaintingStyle.stroke + ..strokeWidth = 3.6; + + dragon = Path()..moveTo(path[0].x, path[0].y); + for (final p in path) { + dragon.lineTo(p.x, p.y); + } + } + + @override + void render(Canvas canvas) { + super.render(canvas); + canvas.drawPath(dragon, borderPaint); + canvas.drawPath(dragon, mainPaint); + } +} + +class Ant extends PositionComponent { + Ant() : random = Random() { + size = Vector2(2, 5); + anchor = const Anchor(0.5, 0.4); + } + + late final Color color; + final Random random; + static const black = Color(0xFF000000); + static const tau = Transform2D.tau; + late final Paint bodyPaint; + late final Paint eyesPaint; + late final Paint legsPaint; + late final Paint facePaint; + late final Paint borderPaint; + late final Path head; + late final Path body; + late final Path pincers; + late final Path eyes; + late final Path antennae; + late final List legs; + Vector2 destinationPosition = Vector2.zero(); + double destinationAngle = 0; + double movementTime = 0; + double rotationTime = 0; + double stepTime = 0; + double movementSpeed = 3; // mm/s + double rotationSpeed = 3; // angle/s + double probabilityToChangeDirection = 0.02; + bool moveLeftSide = false; + List targetLegsPositions = List.generate(6, (_) => Vector2.zero()); + List travelPath = []; + int travelPathNodeIndex = 0; + int travelDirection = 1; // +1 or -1 + + bool legIsMoving(int i) => moveLeftSide == (i < 3); + + void setTravelPath(List path) { + travelPath = path; + travelPathNodeIndex = random.nextInt(path.length - 1); + travelDirection = 1; + position = travelPath[travelPathNodeIndex]; + destinationPosition = travelPath[travelPathNodeIndex + travelDirection]; + angle = -(destinationPosition - position).angleToSigned(Vector2(0, -1)); + destinationAngle = angle; + } + + @override + void onMount() { + super.onMount(); + bodyPaint = Paint()..color = color; + eyesPaint = Paint()..color = black; + borderPaint = Paint() + ..color = Color.lerp(color, black, 0.6)! + ..style = PaintingStyle.stroke + ..strokeWidth = 0.06; + legsPaint = Paint() + ..color = Color.lerp(color, black, 0.4)! + ..style = PaintingStyle.stroke + ..strokeWidth = 0.2; + facePaint = Paint() + ..color = Color.lerp(color, black, 0.5)! + ..style = PaintingStyle.stroke + ..strokeWidth = 0.05; + head = Path() + ..moveTo(0, -0.3) + ..cubicTo(-0.5, -0.3, -0.7, -0.6, -0.7, -1) + ..cubicTo(-0.7, -1.3, -0.3, -2, 0, -2) + ..cubicTo(0.3, -2, 0.7, -1.3, 0.7, -1) + ..cubicTo(0.7, -0.6, 0.5, -0.3, 0, -0.3) + ..close(); + body = Path() + ..moveTo(0, -0.3) + ..cubicTo(0.2, -0.3, 0.4, -0.2, 0.4, 0.2) + ..cubicTo(0.4, 0.4, 0.25, 1, 0, 1) + ..cubicTo(0.6, 1, 0.9, 1.4, 0.9, 1.8) + ..cubicTo(0.9, 2.6, 0.35, 3.1, 0, 3.1) + ..cubicTo(-0.35, 3.1, -0.9, 2.6, -0.9, 1.8) + ..cubicTo(-0.9, 1.4, -0.6, 1, 0, 1) + ..cubicTo(-0.25, 1, -0.4, 0.4, -0.4, 0.2) + ..cubicTo(-0.4, -0.2, -0.2, -0.3, 0, -0.3) + ..close(); + pincers = Path() + ..moveTo(0.15, -2.15) + ..cubicTo(0.5, -1.5, -0.5, -1.5, -0.15, -2.15) + ..cubicTo(-0.3, -1.8, 0.3, -1.8, 0.15, -2.15) + ..close(); + antennae = Path() + ..moveTo(0, -1.7) + ..lineTo(-0.7, -1.9) + ..lineTo(-1, -2.5) + ..lineTo(-1.5, -2.6) + ..moveTo(0, -1.7) + ..lineTo(0.7, -1.9) + ..lineTo(1, -2.5) + ..lineTo(1.5, -2.6); + eyes = Path() + ..moveTo(-0.5, -1.1) + ..cubicTo(-0.95, -1.1, -0.6, -1.8, -0.3, -1.8) + ..cubicTo(0, -1.8, 0, -1.1, -0.5, -1.1) + ..moveTo(0.5, -1.1) + ..cubicTo(0.95, -1.1, 0.6, -1.8, 0.3, -1.8) + ..cubicTo(0, -1.8, 0, -1.1, 0.5, -1.1) + ..close(); + legs = [ + InsectLeg(-0.3, 0.4, -2.6, 0.6, 1.1, 1.1, 0.5, true), + InsectLeg(-0.2, 0.7, -2.3, 2.6, 1.5, 1.5, 0.6, true), + InsectLeg(0.3, 0, 1.7, -2.3, 1.5, 1.3, 0.6, true), + InsectLeg(0.3, 0.4, 2.6, 0.6, 1.1, 1.1, 0.5, false), + InsectLeg(0.2, 0.7, 2.3, 2.6, 1.5, 1.5, 0.6, false), + InsectLeg(-0.3, 0, -1.7, -2.3, 1.5, 1.3, 0.6, false), + ]; + } + + @override + void update(double dt) { + super.update(dt); + if (movementTime <= 0 && rotationTime <= 0) { + planNextMove(); + } + if (stepTime <= 0) { + planNextStep(); + } + final feetPositions = [for (final leg in legs) positionOf(leg.foot)]; + final fMove = movementTime > 0 ? min(dt / movementTime, 1) : 0; + final fRot = rotationTime > 0 ? min(dt / rotationTime, 1) : 0; + final deltaX = (destinationPosition.x - position.x) * fMove; + final deltaY = (destinationPosition.y - position.y) * fMove; + final deltaA = (destinationAngle - angle) * fRot; + position += Vector2(deltaX, deltaY); + angle += deltaA; + movementTime -= dt; + rotationTime -= dt; + for (var i = 0; i < 6; i++) { + var newFootPosition = feetPositions[i]; + if (legIsMoving(i)) { + final fStep = min(dt / stepTime, 1.0); + final targetPosition = targetLegsPositions[i]; + newFootPosition += (targetPosition - newFootPosition) * fStep; + } + legs[i].placeFoot(toLocal(newFootPosition)); + } + stepTime -= dt; + } + + void planNextStep() { + moveLeftSide = !moveLeftSide; + stepTime = 0.1; + final f = min(stepTime * 1.6 / movementTime, 1.0); + final deltaX = (destinationPosition.x - position.x) * f; + final deltaY = (destinationPosition.y - position.y) * f; + final deltaA = (destinationAngle - angle) * f; + position += Vector2(deltaX, deltaY); + angle += deltaA; + for (var i = 0; i < 6; i++) { + if (legIsMoving(i)) { + targetLegsPositions[i].setFrom( + positionOf(Vector2(legs[i].x1, legs[i].y1)), + ); + } + } + position -= Vector2(deltaX, deltaY); + angle -= deltaA; + } + + void planNextMove() { + if (travelPathNodeIndex == 0) { + travelDirection = 1; + } else if (travelPathNodeIndex == travelPath.length - 1) { + travelDirection = -1; + } else if (random.nextDouble() < probabilityToChangeDirection) { + travelDirection = -travelDirection; + } + final nextIndex = travelPathNodeIndex + travelDirection; + assert(nextIndex >= 0 && nextIndex < travelPath.length); + final nextPosition = travelPath[nextIndex]; + var nextAngle = + angle = -(nextPosition - position).angleToSigned(Vector2(0, -1)); + if (nextAngle - angle > tau / 2) { + nextAngle -= tau; + } + if (nextAngle - angle < -tau / 2) { + nextAngle += tau; + } + if ((nextAngle - angle).abs() > 1) { + destinationPosition = position; + destinationAngle = nextAngle; + } else { + destinationPosition = nextPosition; + destinationAngle = nextAngle; + travelPathNodeIndex = nextIndex; + } + rotationTime = (destinationAngle - angle) / rotationSpeed; + movementTime = (destinationPosition - position).length / movementSpeed; + } + + @override + void render(Canvas canvas) { + super.render(canvas); + canvas + ..save() + ..translate(1, 2) + ..drawPath(pincers, facePaint) + ..drawPath(antennae, facePaint) + ..drawPath(head, bodyPaint) + ..drawPath(head, borderPaint) + ..drawPath(eyes, eyesPaint) + ..drawPath(legs[0].path, legsPaint) + ..drawPath(legs[1].path, legsPaint) + ..drawPath(legs[2].path, legsPaint) + ..drawPath(legs[3].path, legsPaint) + ..drawPath(legs[4].path, legsPaint) + ..drawPath(legs[5].path, legsPaint) + ..drawPath(body, bodyPaint) + ..drawPath(body, borderPaint) + ..restore(); + } +} + +class InsectLeg { + InsectLeg( + this.x0, + this.y0, + this.x1, + this.y1, + this.l1, + this.l2, + this.l3, + this.bendDirection, + ) : r = l3 / 2, + dir = bendDirection ? -1 : 1, + path = Path(), + foot = Vector2.zero() { + final ok = placeFoot(Vector2(x1, y1)); + assert(ok); + } + + final double x0, y0; + final double x1, y1; + final double l1, l2, l3, r; + final bool bendDirection; + final double dir; + final Path path; + final Vector2 foot; + + bool placeFoot(Vector2 pos) { + final rr = distance(pos.x, pos.y, x0, y0); + if (rr < r) { + return false; + } + final d = rr - r; + final z = (d * d + l1 * l1 - l2 * l2) / (2 * d); + if (z > l1) { + return false; + } + final h = sqrt(l1 * l1 - z * z); + final xv = (pos.x - x0) / rr; + final yv = (pos.y - y0) / rr; + path + ..reset() + ..moveTo(x0, y0) + ..lineTo(x0 + xv * z + dir * yv * h, y0 + yv * z - dir * xv * h) + ..lineTo(x0 + xv * (rr - r), y0 + yv * (rr - r)) + ..lineTo(x0 + xv * (rr + r), y0 + yv * (rr + r)); + foot.setFrom(pos); + return true; + } +} + +double distance(num x0, num y0, num x1, num y1) { + final dx = x1 - x0; + final dy = y1 - y0; + return sqrt(dx * dx + dy * dy); +} From 3c46367e145452dc1640f826e039815d7daf4b80 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Feb 2022 14:27:37 -0800 Subject: [PATCH 06/28] Use World in camera2example --- .../stories/camera_and_viewport/camera2.dart | 29 +++++++++++++------ .../lib/src/experimental/viewfinder.dart | 10 +++---- .../lib/src/game/game_widget/game_widget.dart | 1 + 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/examples/lib/stories/camera_and_viewport/camera2.dart b/examples/lib/stories/camera_and_viewport/camera2.dart index 09ce9d0453e..6e4cd2dfd9f 100644 --- a/examples/lib/stories/camera_and_viewport/camera2.dart +++ b/examples/lib/stories/camera_and_viewport/camera2.dart @@ -2,6 +2,8 @@ import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; +import 'package:flame/src/experimental/camera.dart'; // ignore: implementation_imports +import 'package:flame/src/experimental/world.dart'; // ignore: implementation_imports import 'package:flutter/widgets.dart'; Future main() async { @@ -12,11 +14,25 @@ class Camera2Example extends FlameGame { @override Color backgroundColor() => const Color(0xFFffffff); + @override + Future onLoad() async { + final world = AntWorld(); + await add(world); + final camera = Camera2(world: world); + await add(camera); + final center = world.curve.boundingRect().center; + camera.viewfinder.position = Vector2(center.dx, center.dy); + } +} + +class AntWorld extends World { + late final DragonCurve curve; + @override Future onLoad() async { final random = Random(); - final curve = DragonCurve(); - add(curve); + curve = DragonCurve(); + await add(curve); const baseColor = HSVColor.fromAHSV(1, 38.5, 0.63, 0.68); for (var i = 0; i < 20; i++) { @@ -27,9 +43,6 @@ class Camera2Example extends FlameGame { ..setTravelPath(curve.path), ); } - final center = curve.boundingRect().center; - camera.snapTo(Vector2(center.dx, center.dy)); - camera.setRelativeOffset(Anchor.center); } } @@ -76,7 +89,7 @@ class DragonCurve extends PositionComponent { } @override - void onMount() { + Future onLoad() async { borderPaint = Paint() ..color = const Color(0xFF041D1F) ..style = PaintingStyle.stroke @@ -95,7 +108,6 @@ class DragonCurve extends PositionComponent { @override void render(Canvas canvas) { - super.render(canvas); canvas.drawPath(dragon, borderPaint); canvas.drawPath(dragon, mainPaint); } @@ -149,8 +161,7 @@ class Ant extends PositionComponent { } @override - void onMount() { - super.onMount(); + Future onLoad() async { bodyPaint = Paint()..color = color; eyesPaint = Paint()..color = black; borderPaint = Paint() diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index 11370e53ee4..99dafd4f526 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -9,14 +9,14 @@ import 'camera.dart'; class Viewfinder extends Component { final Transform2D _transform = Transform2D(); - Vector2 get position => _transform.position; - set position(Vector2 value) => _transform.position = value; + Vector2 get position => -_transform.position; + set position(Vector2 value) => _transform.position = -value; double get zoom => _transform.scale.x; set zoom(double value) => _transform.scale = Vector2.all(value); - double get angle => _transform.angle; - set angle(double value) => _transform.angle; + double get angle => -_transform.angle; + set angle(double value) => _transform.angle = -value; double? get visibleGameWidth => _visibleGameWidth; double? _visibleGameWidth; @@ -62,7 +62,7 @@ class Viewfinder extends Component { try { Camera2.currentCameras.add(camera); canvas.transform(_transform.transformMatrix.storage); - world.renderTree(canvas); + world.renderFromCamera(canvas); } finally { Camera2.currentCameras.removeLast(); } diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index 38b00faa9ed..389ee439d7d 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -323,6 +323,7 @@ class _GameWidgetState extends State> { if (snapshot.hasError) { final errorBuilder = widget.errorBuilder; if (errorBuilder == null) { + print(snapshot.stackTrace); throw snapshot.error!; } else { return errorBuilder(context, snapshot.error!); From e8f39ec786e6a66dcb101552cff8d55bc1729158 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Feb 2022 17:58:00 -0800 Subject: [PATCH 07/28] added magnifying glass --- .../{camera2.dart => camera2_example.dart} | 147 +++++++++++++++++- .../flame/lib/src/experimental/camera.dart | 3 + 2 files changed, 142 insertions(+), 8 deletions(-) rename examples/lib/stories/camera_and_viewport/{camera2.dart => camera2_example.dart} (71%) diff --git a/examples/lib/stories/camera_and_viewport/camera2.dart b/examples/lib/stories/camera_and_viewport/camera2_example.dart similarity index 71% rename from examples/lib/stories/camera_and_viewport/camera2.dart rename to examples/lib/stories/camera_and_viewport/camera2_example.dart index 6e4cd2dfd9f..81511e4aa80 100644 --- a/examples/lib/stories/camera_and_viewport/camera2.dart +++ b/examples/lib/stories/camera_and_viewport/camera2_example.dart @@ -2,15 +2,22 @@ import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; +import 'package:flame/input.dart'; import 'package:flame/src/experimental/camera.dart'; // ignore: implementation_imports +import 'package:flame/src/experimental/circular_viewport.dart'; // ignore: implementation_imports import 'package:flame/src/experimental/world.dart'; // ignore: implementation_imports -import 'package:flutter/widgets.dart'; +import 'package:flutter/widgets.dart' hide Draggable; Future main() async { runApp(GameWidget(game: Camera2Example())); } -class Camera2Example extends FlameGame { +class Camera2Example extends FlameGame with PanDetector { + late final Camera2 magnifyingGlass; + late final Vector2 center; + static const zoom = 10.0; + static const radius = 130.0; + @override Color backgroundColor() => const Color(0xFFffffff); @@ -20,19 +27,139 @@ class Camera2Example extends FlameGame { await add(world); final camera = Camera2(world: world); await add(camera); - final center = world.curve.boundingRect().center; - camera.viewfinder.position = Vector2(center.dx, center.dy); + final offset = world.curve.boundingRect().center; + print(world.curve.boundingRect()); + center = Vector2(offset.dx, offset.dy); + camera.viewfinder.position = Vector2(center.x, center.y); + + magnifyingGlass = Camera2(world: world, viewport: CircularViewport(radius)); + magnifyingGlass.viewport.add(Bezel(radius)); + magnifyingGlass.viewfinder.zoom = zoom; + } + + @override + bool onPanStart(DragStartInfo info) { + _updateMagnifyingGlassPosition(info.eventPosition.widget); + add(magnifyingGlass); + return false; + } + + @override + bool onPanUpdate(DragUpdateInfo info) { + _updateMagnifyingGlassPosition(info.eventPosition.widget); + return false; + } + + @override + bool onPanEnd(DragEndInfo info) { + onPanCancel(); + return false; + } + + @override + bool onPanCancel() { + remove(magnifyingGlass); + return false; + } + + void _updateMagnifyingGlassPosition(Vector2 point) { + // shifts the original [point] by 1.4142*radius, which happens to be in the + // middle of a handle + final handlePoint = point - Vector2.all(radius); + magnifyingGlass.viewport.position = handlePoint; + magnifyingGlass.viewfinder.position = + (handlePoint - canvasSize / 2 + center) * zoom; + } +} + +class Bezel extends Component { + Bezel(this.radius); + + final double radius; + late final Path rim; + late final Path rimBorder; + late final Path handle; + late final Path connector; + late final Path specularHighlight; + static const rimWidth = 20.0; + static const handleWidth = 40.0; + static const handleLength = 100.0; + late final Paint glassPaint; + late final Paint rimPaint; + late final Paint rimBorderPaint; + late final Paint handlePaint; + late final Paint connectorPaint; + late final Paint specularPaint; + + @override + Future onLoad() async { + rim = Path()..addOval(Rect.fromLTRB(-radius, -radius, radius, radius)); + final outer = radius + rimWidth / 2; + final inner = radius - rimWidth / 2; + rimBorder = Path() + ..addOval(Rect.fromLTRB(-outer, -outer, outer, outer)) + ..addOval(Rect.fromLTRB(-inner, -inner, inner, inner)); + handle = (Path() + ..addRRect( + RRect.fromLTRBR( + radius, + -handleWidth / 2, + handleLength + radius, + handleWidth / 2, + const Radius.circular(5.0), + ), + )) + .transform((Matrix4.identity()..rotateZ(pi / 4)).storage); + connector = (Path() + ..addArc(Rect.fromLTRB(-outer, -outer, outer, outer), -0.22, 0.44)) + .transform((Matrix4.identity()..rotateZ(pi / 4)).storage); + specularHighlight = (Path() + ..addOval(Rect.fromLTWH(-radius * 0.8, -8, 16, radius * 0.3))) + .transform((Matrix4.identity()..rotateZ(pi / 4)).storage); + + glassPaint = Paint()..color = const Color(0x1400ffae); + rimBorderPaint = Paint() + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke + ..color = const Color(0xff61382a); + rimPaint = Paint() + ..strokeWidth = rimWidth + ..style = PaintingStyle.stroke + ..color = const Color(0xffffdf70); + connectorPaint = Paint() + ..strokeWidth = 20.0 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round + ..color = const Color(0xff654510); + handlePaint = Paint()..color = const Color(0xffdbbf9f); + specularPaint = Paint() + ..color = const Color(0xccffffff) + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 2); + } + + @override + void render(Canvas canvas) { + canvas.drawPath(rim, glassPaint); + canvas.drawPath(specularHighlight, specularPaint); + canvas.drawPath(handle, handlePaint); + canvas.drawPath(handle, rimBorderPaint); + canvas.drawPath(connector, connectorPaint); + canvas.drawPath(rim, rimPaint); + canvas.drawPath(rimBorder, rimBorderPaint); } } class AntWorld extends World { late final DragonCurve curve; + late final Rect bgRect; + final Paint bgPaint = Paint()..color = const Color(0xffffffff); @override Future onLoad() async { final random = Random(); curve = DragonCurve(); await add(curve); + bgRect = curve.boundingRect().inflate(100); const baseColor = HSVColor.fromAHSV(1, 38.5, 0.63, 0.68); for (var i = 0; i < 20; i++) { @@ -44,13 +171,16 @@ class AntWorld extends World { ); } } -} -class DragonCurve extends PositionComponent { - DragonCurve() { - initPath(); + @override + void render(Canvas canvas) { + // Render white backdrop, to prevent the world in the magnifying glass from + // being "see-through" + canvas.drawRect(bgRect, bgPaint); } +} +class DragonCurve extends PositionComponent { late final Paint borderPaint; late final Paint mainPaint; late final Path dragon; @@ -90,6 +220,7 @@ class DragonCurve extends PositionComponent { @override Future onLoad() async { + initPath(); borderPaint = Paint() ..color = const Color(0xFF041D1F) ..style = PaintingStyle.stroke diff --git a/packages/flame/lib/src/experimental/camera.dart b/packages/flame/lib/src/experimental/camera.dart index 036009dc08d..839bd8fd5d1 100644 --- a/packages/flame/lib/src/experimental/camera.dart +++ b/packages/flame/lib/src/experimental/camera.dart @@ -1,3 +1,5 @@ +import 'package:meta/meta.dart'; + import '../components/component.dart'; import 'max_viewport.dart'; import 'viewfinder.dart'; @@ -36,6 +38,7 @@ class Camera2 extends Component { }) : viewport = viewport ?? MaxViewport(), viewfinder = viewfinder ?? Viewfinder(); + @mustCallSuper @override Future onLoad() async { await add(viewport); From 370bd89c19c0adc3ba1d64ca5e2dbd1b39e68556 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Feb 2022 18:13:01 -0800 Subject: [PATCH 08/28] added description --- .../camera_and_viewport/camera2_example.dart | 17 +++++++++++------ .../camera_and_viewport.dart | 7 +++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/examples/lib/stories/camera_and_viewport/camera2_example.dart b/examples/lib/stories/camera_and_viewport/camera2_example.dart index 81511e4aa80..dd322369003 100644 --- a/examples/lib/stories/camera_and_viewport/camera2_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera2_example.dart @@ -6,20 +6,25 @@ import 'package:flame/input.dart'; import 'package:flame/src/experimental/camera.dart'; // ignore: implementation_imports import 'package:flame/src/experimental/circular_viewport.dart'; // ignore: implementation_imports import 'package:flame/src/experimental/world.dart'; // ignore: implementation_imports -import 'package:flutter/widgets.dart' hide Draggable; +import 'package:flutter/painting.dart'; -Future main() async { - runApp(GameWidget(game: Camera2Example())); -} class Camera2Example extends FlameGame with PanDetector { + static const description = ''' + This example shows how a camera can be dynamically added into a game via + the Camera2 component. + + Click and hold the mouse to bring up a magnifying glass, then have a better + look at the world underneath! + '''; + late final Camera2 magnifyingGlass; late final Vector2 center; static const zoom = 10.0; static const radius = 130.0; @override - Color backgroundColor() => const Color(0xFFffffff); + Color backgroundColor() => const Color(0xFFeeeeee); @override Future onLoad() async { @@ -152,7 +157,7 @@ class Bezel extends Component { class AntWorld extends World { late final DragonCurve curve; late final Rect bgRect; - final Paint bgPaint = Paint()..color = const Color(0xffffffff); + final Paint bgPaint = Paint()..color = const Color(0xffeeeeee); @override Future onLoad() async { diff --git a/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart b/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart index 7c99d48388a..b3b5f7d7038 100644 --- a/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart +++ b/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart @@ -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'; @@ -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, ); } From 17c0ffc3fcaf004582eb1c009111d858fc6bb2ed Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Feb 2022 23:43:15 -0800 Subject: [PATCH 09/28] expanded docs for camera2 --- .../camera_and_viewport/camera2_example.dart | 1 - .../flame/lib/src/experimental/camera.dart | 67 +++++++++++++------ .../lib/src/experimental/viewfinder.dart | 1 + 3 files changed, 47 insertions(+), 22 deletions(-) diff --git a/examples/lib/stories/camera_and_viewport/camera2_example.dart b/examples/lib/stories/camera_and_viewport/camera2_example.dart index dd322369003..33a7b689dfa 100644 --- a/examples/lib/stories/camera_and_viewport/camera2_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera2_example.dart @@ -33,7 +33,6 @@ class Camera2Example extends FlameGame with PanDetector { final camera = Camera2(world: world); await add(camera); final offset = world.curve.boundingRect().center; - print(world.curve.boundingRect()); center = Vector2(offset.dx, offset.dy); camera.viewfinder.position = Vector2(center.x, center.y); diff --git a/packages/flame/lib/src/experimental/camera.dart b/packages/flame/lib/src/experimental/camera.dart index 839bd8fd5d1..90ab53574cf 100644 --- a/packages/flame/lib/src/experimental/camera.dart +++ b/packages/flame/lib/src/experimental/camera.dart @@ -11,25 +11,22 @@ import 'world.dart'; /// 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 exists somewhere else in the game tree. +/// world, which may exist anywhere in the game tree. /// -/// 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. +/// 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 [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. -/// -/// The [world] is a special component that is designed to be the root of a -/// game world. Multiple cameras can observe the world simultaneously, and the -/// world may itself contain cameras that look into other worlds, or even the -/// same world. +/// 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 { Camera2({ required this.world, @@ -38,6 +35,36 @@ class Camera2 extends Component { }) : 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 onLoad() async { @@ -45,10 +72,6 @@ class Camera2 extends Component { await add(viewfinder); } - final Viewport viewport; - final Viewfinder viewfinder; - World world; - /// A camera that currently performs rendering. /// /// This variable is set to `this` when we begin rendering the world through @@ -57,6 +80,8 @@ class Camera2 extends Component { static Camera2? get currentCamera { return currentCameras.isEmpty? null : currentCameras[0]; } + + /// Stack of all current cameras in the render tree. static final List currentCameras = []; /// Maximum number of nested cameras that will be rendered. diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index 99dafd4f526..29e83775c6a 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -63,6 +63,7 @@ class Viewfinder extends Component { Camera2.currentCameras.add(camera); canvas.transform(_transform.transformMatrix.storage); world.renderFromCamera(canvas); + super.renderTree(canvas); } finally { Camera2.currentCameras.removeLast(); } From 3b55ab59fa29835414698658955891e1388f68cf Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Sat, 5 Feb 2022 00:05:17 -0800 Subject: [PATCH 10/28] adjust docs for Viewport --- .../src/experimental/circular_viewport.dart | 6 +- .../fixed_aspect_ratio_viewport.dart | 6 +- .../src/experimental/fixed_size_viewport.dart | 6 +- .../lib/src/experimental/max_viewport.dart | 3 + .../flame/lib/src/experimental/viewport.dart | 61 ++++++++++++++----- 5 files changed, 57 insertions(+), 25 deletions(-) diff --git a/packages/flame/lib/src/experimental/circular_viewport.dart b/packages/flame/lib/src/experimental/circular_viewport.dart index a382385d390..32ef1437d0f 100644 --- a/packages/flame/lib/src/experimental/circular_viewport.dart +++ b/packages/flame/lib/src/experimental/circular_viewport.dart @@ -12,13 +12,13 @@ class CircularViewport extends Viewport { 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)); } - - @override - void clip(Canvas canvas) => canvas.clipPath(_clipPath, doAntiAlias: false); } diff --git a/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart index e31b55c1225..74486dc495f 100644 --- a/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart +++ b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart @@ -18,6 +18,9 @@ class FixedAspectRatioViewport extends Viewport { size = canvasSize; } + @override + void clip(Canvas canvas) => canvas.clipRect(_clipRect); + @override void handleResize() { final desiredWidth = size.y * aspectRatio; @@ -31,7 +34,4 @@ class FixedAspectRatioViewport extends Viewport { final y = size.y / 2; _clipRect = Rect.fromLTRB(-x, -y, x, y); } - - @override - void clip(Canvas canvas) => canvas.clipRect(_clipRect); } diff --git a/packages/flame/lib/src/experimental/fixed_size_viewport.dart b/packages/flame/lib/src/experimental/fixed_size_viewport.dart index efed81cb61d..71059dd54b8 100644 --- a/packages/flame/lib/src/experimental/fixed_size_viewport.dart +++ b/packages/flame/lib/src/experimental/fixed_size_viewport.dart @@ -16,13 +16,13 @@ class FixedSizeViewport extends Viewport { 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); } - - @override - void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false); } diff --git a/packages/flame/lib/src/experimental/max_viewport.dart b/packages/flame/lib/src/experimental/max_viewport.dart index 05cea00d0d3..50f9b1e9cda 100644 --- a/packages/flame/lib/src/experimental/max_viewport.dart +++ b/packages/flame/lib/src/experimental/max_viewport.dart @@ -14,4 +14,7 @@ class MaxViewport extends Viewport { @override void clip(Canvas canvas) {} + + @override + void handleResize() {} } diff --git a/packages/flame/lib/src/experimental/viewport.dart b/packages/flame/lib/src/experimental/viewport.dart index c270ed0e279..c18e10d9e87 100644 --- a/packages/flame/lib/src/experimental/viewport.dart +++ b/packages/flame/lib/src/experimental/viewport.dart @@ -1,4 +1,3 @@ - import 'dart:ui'; import 'package:meta/meta.dart'; @@ -6,31 +5,70 @@ import 'package:vector_math/vector_math_64.dart'; import '../components/component.dart'; import 'camera.dart'; -import 'viewfinder.dart'; +/// [Viewport] is a part of a [Camera2] system. +/// +/// The viewport describes a "window" through which the underlying game world +/// is observed. At the same time, the viewport is agnostic of the game world, +/// and only contain properties that describe the "window". These properties +/// are: the window's size, shape, and position on the screen. +/// +/// There are several implementations of [Viewport], which differ by their +/// shape, and also by their behavior in response to changes to the canvas size. +/// Users may also create their own implementations. abstract class Viewport extends Component { /// Position of the viewport's center in the parent's coordinate frame. + /// + /// Changing this position will move the viewport around the screen, but will + /// not affect which portion of the game world is visible. Thus, the game + /// world will appear as a static picture inside the viewport. Vector2 get position => _position; final Vector2 _position = Vector2.zero(); - set position(Vector2 value) { - _position.setFrom(value); - } + set position(Vector2 value) => _position.setFrom(value); - /// Size of the viewport, i.e. width and height. + /// Size of the viewport, i.e. the width and the height. /// /// This property represents the bounding box of the viewport. If the viewport /// is rectangular in shape, then [size] describes the dimensions of that /// rectangle. If the viewport has any other shape (for example, circular), /// then [size] describes the dimensions of the bounding box of the viewport. /// - /// Changing the size at runtime triggers the [handleResize] event. + /// Changing the size at runtime triggers the [handleResize] event. The size + /// cannot be negative. Vector2 get size => _size; final Vector2 _size = Vector2.zero(); set size(Vector2 value) { + assert( + value.x >= 0 && value.y >= 0, + "Viewport's size cannot be negative: $value", + ); _size.setFrom(value); handleResize(); } + /// 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. + /// + /// A typical use-case would be to adjust the viewport's clip mask to match + /// the new size. + @protected + void handleResize(); + + @mustCallSuper + @override + void onMount() { + assert(parent! is Camera2, 'A Viewport may only be attached to a Camera2'); + } + @override void renderTree(Canvas canvas) { final camera = parent! as Camera2; @@ -45,13 +83,4 @@ abstract class Viewport extends Component { canvas.restore(); } - @protected - void clip(Canvas canvas); - - /// Override in order to perform a custom action upon resize. - /// - /// A typical use-case would be to adjust the viewport's clip mask to match - /// the new size. - @protected - void handleResize() {} } From 6e39515c4a949697a60fb65d9975074adf0b6af8 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Sat, 5 Feb 2022 01:09:49 -0800 Subject: [PATCH 11/28] docs for Viewfinder --- .../lib/src/experimental/max_viewport.dart | 3 + .../lib/src/experimental/viewfinder.dart | 100 ++++++++++++++---- .../flame/lib/src/experimental/viewport.dart | 1 - 3 files changed, 80 insertions(+), 24 deletions(-) diff --git a/packages/flame/lib/src/experimental/max_viewport.dart b/packages/flame/lib/src/experimental/max_viewport.dart index 50f9b1e9cda..597d24a6d1b 100644 --- a/packages/flame/lib/src/experimental/max_viewport.dart +++ b/packages/flame/lib/src/experimental/max_viewport.dart @@ -4,6 +4,9 @@ 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) { diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index 29e83775c6a..77629a20d98 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -1,60 +1,114 @@ +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 { + /// 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) => _transform.scale = Vector2.all(value); + 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; - double? get visibleGameWidth => _visibleGameWidth; - double? _visibleGameWidth; - set visibleGameWidth(double? value) { - _visibleGameWidth = value; - _visibleGameHeight = null; - _initZoom(); - } + /// Reference to the parent camera. + Camera2 get camera => parent! as Camera2; - double? get visibleGameHeight => _visibleGameHeight; - double? _visibleGameHeight; - set visibleGameHeight(double? value) { - _visibleGameWidth = null; - _visibleGameHeight = value; - _initZoom(); + /// 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(); + } } - Camera2 get camera => parent! as Camera2; - + /// Set [zoom] level based on the [_visibleGameSize]. void _initZoom() { - if (isMounted) { - if (_visibleGameWidth != null) { - zoom = camera.viewport.size.x / _visibleGameWidth!; - } - if (_visibleGameHeight != null) { - zoom = camera.viewport.size.y / _visibleGameHeight!; - } + 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(); + } + @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 && diff --git a/packages/flame/lib/src/experimental/viewport.dart b/packages/flame/lib/src/experimental/viewport.dart index c18e10d9e87..9f404c3f809 100644 --- a/packages/flame/lib/src/experimental/viewport.dart +++ b/packages/flame/lib/src/experimental/viewport.dart @@ -82,5 +82,4 @@ abstract class Viewport extends Component { super.renderTree(canvas); canvas.restore(); } - } From 5284e50a20a2ba09008efcba2371ce33c2fb9c2d Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Sat, 5 Feb 2022 01:10:33 -0800 Subject: [PATCH 12/28] format --- packages/flame/lib/src/experimental/camera.dart | 6 +++--- packages/flame/lib/src/experimental/circular_viewport.dart | 1 - .../lib/src/experimental/fixed_aspect_ratio_viewport.dart | 2 +- .../flame/lib/src/experimental/fixed_size_viewport.dart | 1 - packages/flame/lib/src/experimental/world.dart | 1 - 5 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/flame/lib/src/experimental/camera.dart b/packages/flame/lib/src/experimental/camera.dart index 90ab53574cf..621f33f345b 100644 --- a/packages/flame/lib/src/experimental/camera.dart +++ b/packages/flame/lib/src/experimental/camera.dart @@ -32,8 +32,8 @@ class Camera2 extends Component { required this.world, Viewport? viewport, Viewfinder? viewfinder, - }) : viewport = viewport ?? MaxViewport(), - viewfinder = viewfinder ?? Viewfinder(); + }) : viewport = viewport ?? MaxViewport(), + viewfinder = viewfinder ?? Viewfinder(); /// The [viewport] is the "window" through which the game world is observed. /// @@ -78,7 +78,7 @@ class Camera2 extends Component { /// 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]; + return currentCameras.isEmpty ? null : currentCameras[0]; } /// Stack of all current cameras in the render tree. diff --git a/packages/flame/lib/src/experimental/circular_viewport.dart b/packages/flame/lib/src/experimental/circular_viewport.dart index 32ef1437d0f..a8fedda01e8 100644 --- a/packages/flame/lib/src/experimental/circular_viewport.dart +++ b/packages/flame/lib/src/experimental/circular_viewport.dart @@ -1,4 +1,3 @@ - import 'dart:ui'; import 'package:vector_math/vector_math_64.dart'; diff --git a/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart index 74486dc495f..72042e08ad3 100644 --- a/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart +++ b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart @@ -6,7 +6,7 @@ import 'viewport.dart'; class FixedAspectRatioViewport extends Viewport { FixedAspectRatioViewport({required this.aspectRatio}) - : assert(aspectRatio > 0); + : assert(aspectRatio > 0); final double aspectRatio; Rect _clipRect = Rect.zero; diff --git a/packages/flame/lib/src/experimental/fixed_size_viewport.dart b/packages/flame/lib/src/experimental/fixed_size_viewport.dart index 71059dd54b8..d230c0b6155 100644 --- a/packages/flame/lib/src/experimental/fixed_size_viewport.dart +++ b/packages/flame/lib/src/experimental/fixed_size_viewport.dart @@ -1,4 +1,3 @@ - import 'dart:ui'; import 'package:vector_math/vector_math_64.dart'; diff --git a/packages/flame/lib/src/experimental/world.dart b/packages/flame/lib/src/experimental/world.dart index f07e75211db..8ba8162abce 100644 --- a/packages/flame/lib/src/experimental/world.dart +++ b/packages/flame/lib/src/experimental/world.dart @@ -1,4 +1,3 @@ - import 'dart:ui'; import 'package:meta/meta.dart'; From c9978fdd4b424ae3ea197d1471fe6dcf6fe1b136 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Sat, 5 Feb 2022 01:19:49 -0800 Subject: [PATCH 13/28] docs for world --- packages/flame/lib/src/experimental/world.dart | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/flame/lib/src/experimental/world.dart b/packages/flame/lib/src/experimental/world.dart index 8ba8162abce..e556463234c 100644 --- a/packages/flame/lib/src/experimental/world.dart +++ b/packages/flame/lib/src/experimental/world.dart @@ -5,12 +5,16 @@ import 'package:meta/meta.dart'; import '../components/component.dart'; import 'camera.dart'; +/// The root component for all game world elements. +/// +/// The primary feature of this component is that it disables regular rendering, +/// and allows itself to be rendered through a [Camera2] only. The updates +/// proceed through the world tree normally. class World extends Component { - // World may only be rendered through a camera, so regular [renderTree] is - // disabled. @override void renderTree(Canvas canvas) {} + /// Internal rendering method invoked by the [Camera2]. @internal void renderFromCamera(Canvas canvas) { assert(Camera2.currentCamera != null); From d6c3df21335dbb16896cf9fbf419b048b07754ca Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Sat, 5 Feb 2022 01:44:08 -0800 Subject: [PATCH 14/28] format --- examples/lib/stories/camera_and_viewport/camera2_example.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/lib/stories/camera_and_viewport/camera2_example.dart b/examples/lib/stories/camera_and_viewport/camera2_example.dart index 33a7b689dfa..8398b6ed999 100644 --- a/examples/lib/stories/camera_and_viewport/camera2_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera2_example.dart @@ -8,7 +8,6 @@ import 'package:flame/src/experimental/circular_viewport.dart'; // ignore: imple import 'package:flame/src/experimental/world.dart'; // ignore: implementation_imports import 'package:flutter/painting.dart'; - class Camera2Example extends FlameGame with PanDetector { static const description = ''' This example shows how a camera can be dynamically added into a game via From ad0e2378713173efa417931709c17b33de91bde9 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Wed, 9 Feb 2022 09:05:30 -0800 Subject: [PATCH 15/28] Apply suggestions from code review Co-authored-by: Luan Nico --- packages/flame/lib/src/experimental/max_viewport.dart | 2 +- packages/flame/lib/src/experimental/viewfinder.dart | 1 - packages/flame/lib/src/game/game_widget/game_widget.dart | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/flame/lib/src/experimental/max_viewport.dart b/packages/flame/lib/src/experimental/max_viewport.dart index 597d24a6d1b..6af055d698e 100644 --- a/packages/flame/lib/src/experimental/max_viewport.dart +++ b/packages/flame/lib/src/experimental/max_viewport.dart @@ -6,7 +6,7 @@ import 'viewport.dart'; /// The default viewport, which is as big as the game canvas allows. /// -/// This viewport does not perform any clipping. (?) +/// This viewport does not perform any clipping. class MaxViewport extends Viewport { @override void onGameResize(Vector2 gameSize) { diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index 77629a20d98..9d7257ca934 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -15,7 +15,6 @@ import 'viewport.dart'; /// 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 { /// Internal transform matrix used by the viewfinder. final Transform2D _transform = Transform2D(); diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart index 389ee439d7d..38b00faa9ed 100644 --- a/packages/flame/lib/src/game/game_widget/game_widget.dart +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -323,7 +323,6 @@ class _GameWidgetState extends State> { if (snapshot.hasError) { final errorBuilder = widget.errorBuilder; if (errorBuilder == null) { - print(snapshot.stackTrace); throw snapshot.error!; } else { return errorBuilder(context, snapshot.error!); From 093200765083748e37b6e4c807e92cce358fa0db Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Sun, 6 Feb 2022 04:43:12 -0800 Subject: [PATCH 16/28] feat: Added NoiseEffectController (#1356) --- doc/effects.md | 11 ++++ .../stories/effects/move_effect_example.dart | 29 +++++++++ packages/flame/lib/effects.dart | 1 + .../controllers/noise_effect_controller.dart | 42 +++++++++++++ .../noise_effect_controller_test.dart | 59 +++++++++++++++++++ 5 files changed, 142 insertions(+) create mode 100644 packages/flame/lib/src/effects/controllers/noise_effect_controller.dart create mode 100644 packages/flame/test/effects/controllers/noise_effect_controller_test.dart diff --git a/doc/effects.md b/doc/effects.md index 7ef51508cb8..56dd0a9cb6b 100644 --- a/doc/effects.md +++ b/doc/effects.md @@ -62,6 +62,7 @@ There are multiple effect controllers provided by the Flame framework as well: - [`SequenceEffectController`](#sequenceeffectcontroller) - [`SpeedEffectController`](#speedeffectcontroller) - [`DelayedEffectController`](#delayedeffectcontroller) +- [`NoiseEffectController`](#noiseffectcontroller) - [`RandomEffectController`](#randomeffectcontroller) - [`SineEffectController`](#sineeffectcontroller) - [`ZigzagEffectController`](#zigzageffectcontroller) @@ -506,6 +507,16 @@ final ec = DelayedEffectController(LinearEffectController(1), delay: 5); ``` +### `NoiseEffectController` + +This effect controller exhibits noisy behavior, i.e. it oscillates randomly around zero. Such effect +controller can be used to implement a variety of shake effects. + +```dart +final ec = NoiseEffectController(duration: 0.6, frequency: 10); +``` + + ### `RandomEffectController` This controller wraps another controller and makes its duration random. The actual value for the diff --git a/examples/lib/stories/effects/move_effect_example.dart b/examples/lib/stories/effects/move_effect_example.dart index 6c4284f6f84..dfea65302cf 100644 --- a/examples/lib/stories/effects/move_effect_example.dart +++ b/examples/lib/stories/effects/move_effect_example.dart @@ -14,6 +14,8 @@ class MoveEffectExample extends FlameGame { The middle green square has a combination of two movement effects: a `MoveEffect.to` and a `MoveEffect.by` which forces it to periodically jump. + The purple square executes a sequence of shake effects. + At the bottom there are 60 more components which demonstrate movement along an arbitrary path using `MoveEffect.along`. '''; @@ -31,7 +33,9 @@ class MoveEffectExample extends FlameGame { ..style = PaintingStyle.stroke ..strokeWidth = 5.0 ..color = Colors.greenAccent; + final paint3 = Paint()..color = const Color(0xffb372dc); + // Red square, moving back and forth add( RectangleComponent.square( position: Vector2(20, 50), @@ -49,6 +53,8 @@ class MoveEffectExample extends FlameGame { ), ), ); + + // Green square, moving and jumping add( RectangleComponent.square( position: Vector2(20, 150), @@ -80,6 +86,29 @@ class MoveEffectExample extends FlameGame { ), ); + add( + RectangleComponent.square( + size: 15, + position: Vector2(40, 240), + paint: paint3, + )..add( + SequenceEffect( + [ + MoveEffect.by( + Vector2(5, 0), + NoiseEffectController(duration: 1, frequency: 20), + ), + MoveEffect.by(Vector2.zero(), LinearEffectController(2)), + MoveEffect.by( + Vector2(0, 10), + NoiseEffectController(duration: 1, frequency: 10), + ), + ], + infinite: true, + ), + ), + ); + final path1 = Path()..moveTo(200, 250); for (var i = 1; i <= 5; i++) { final x = 200 + 100 * sin(i * tau * 2 / 5); diff --git a/packages/flame/lib/effects.dart b/packages/flame/lib/effects.dart index 6c95ff56fac..ef93f3e3715 100644 --- a/packages/flame/lib/effects.dart +++ b/packages/flame/lib/effects.dart @@ -6,6 +6,7 @@ export 'src/effects/controllers/duration_effect_controller.dart'; export 'src/effects/controllers/effect_controller.dart'; export 'src/effects/controllers/infinite_effect_controller.dart'; export 'src/effects/controllers/linear_effect_controller.dart'; +export 'src/effects/controllers/noise_effect_controller.dart'; export 'src/effects/controllers/pause_effect_controller.dart'; export 'src/effects/controllers/random_effect_controller.dart'; export 'src/effects/controllers/repeated_effect_controller.dart'; diff --git a/packages/flame/lib/src/effects/controllers/noise_effect_controller.dart b/packages/flame/lib/src/effects/controllers/noise_effect_controller.dart new file mode 100644 index 00000000000..e8308f7afc2 --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/noise_effect_controller.dart @@ -0,0 +1,42 @@ +import 'dart:math'; + +import 'package:flutter/animation.dart' show Curve, Curves; +import 'package:vector_math/vector_math_64.dart'; +import 'duration_effect_controller.dart'; + +/// Effect controller that oscillates around 0 following a noise curve. +/// +/// The [frequency] parameter controls smoothness/jerkiness of the oscillations. +/// It is roughly proportional to the total number of swings for the duration +/// of the effect. +/// +/// The [taperingCurve] describes how the effect fades out over time. The +/// curve that you supply will be flipped along the X axis, so that the effect +/// starts at full force, and gradually reduces to zero towards the end. +/// +/// This effect controller can be used to implement various shake effects. For +/// example, putting into a `MoveEffect.by` will create a shake motion, where +/// the magnitude and the direction of shaking is controlled by the effect's +/// `offset`. +class NoiseEffectController extends DurationEffectController { + NoiseEffectController({ + required double duration, + required this.frequency, + this.taperingCurve = Curves.easeInOutCubic, + Random? random, + }) : assert(duration > 0, 'duration must be positive'), + assert(frequency > 0, 'frequency parameter must be positive'), + noise = SimplexNoise(random), + super(duration); + + final double frequency; + final Curve taperingCurve; + final SimplexNoise noise; + + @override + double get progress { + final x = timer / duration; + final amplitude = taperingCurve.transform(1 - x); + return noise.noise2D(x * frequency, 0) * amplitude; + } +} diff --git a/packages/flame/test/effects/controllers/noise_effect_controller_test.dart b/packages/flame/test/effects/controllers/noise_effect_controller_test.dart new file mode 100644 index 00000000000..dd5c2e501f0 --- /dev/null +++ b/packages/flame/test/effects/controllers/noise_effect_controller_test.dart @@ -0,0 +1,59 @@ +import 'dart:math'; + +import 'package:flame/effects.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/animation.dart'; +import 'package:test/test.dart'; + +void main() { + group('NoiseEffectController', () { + test('general properties', () { + final ec = NoiseEffectController(duration: 1, frequency: 12); + expect(ec.duration, 1.0); + expect(ec.frequency, 12.0); + expect(ec.taperingCurve, Curves.easeInOutCubic); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, 0); + expect(ec.isRandom, false); + }); + + test('progression', () { + final random = Random(567890); + final ec = NoiseEffectController( + duration: 1, + frequency: 3, + random: random, + ); + final observed = []; + for (var t = 0.0; t < 1.0; t += 0.1) { + observed.add(ec.progress); + ec.advance(0.1); + } + expect(observed, [ + 0.0, + -0.4852269950897251, + 0.7905631204866628, + 0.25384428741054194, + 0.06718741964100555, + 0.08011164287850409, + -0.008746065536907871, + -0.07181264736289301, + -0.014005001721806985, + 0.00985567863632108, + -0.000015661267181374608, + ]); + }); + + test('errors', () { + expect( + () => NoiseEffectController(duration: 0, frequency: 1), + failsAssert('duration must be positive'), + ); + expect( + () => NoiseEffectController(duration: 1, frequency: 0), + failsAssert('frequency parameter must be positive'), + ); + }); + }); +} From 040f2f191237b547732d742a848b749320ec061f Mon Sep 17 00:00:00 2001 From: Christopher Fujino Date: Wed, 9 Feb 2022 05:50:24 -0800 Subject: [PATCH 17/28] fix: remove vector_math dependency (#1361) * remove vector_math dependency * remove vector_math from flame_test --- packages/flame/pubspec.yaml | 1 - packages/flame_test/pubspec.yaml | 1 - 2 files changed, 2 deletions(-) diff --git a/packages/flame/pubspec.yaml b/packages/flame/pubspec.yaml index 0de01a23390..92109336439 100644 --- a/packages/flame/pubspec.yaml +++ b/packages/flame/pubspec.yaml @@ -13,7 +13,6 @@ dependencies: meta: ^1.7.0 collection: ^1.15.0 ordered_set: ^5.0.0 - vector_math: '>=2.1.0 <3.0.0' dev_dependencies: flutter_test: diff --git a/packages/flame_test/pubspec.yaml b/packages/flame_test/pubspec.yaml index fc33e3b59df..2a125b62892 100644 --- a/packages/flame_test/pubspec.yaml +++ b/packages/flame_test/pubspec.yaml @@ -15,7 +15,6 @@ dependencies: sdk: flutter meta: ^1.7.0 test: ^1.17.10 - vector_math: '>=2.1.0 <3.0.0' dev_dependencies: dartdoc: ^4.1.0 From 5b3e2939413fdbc572b917f4ed9443a0025c5e71 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Wed, 9 Feb 2022 09:05:48 -0800 Subject: [PATCH 18/28] minor --- packages/flame/lib/src/experimental/viewfinder.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index 9d7257ca934..dfbbfcc38e7 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -95,6 +95,7 @@ class Viewfinder extends Component { _initZoom(); } + @mustCallSuper @override void onMount() { assert(parent! is Camera2, 'Viewfinder can only be mounted to a Camera2'); From 3e77fb4dc64ee7f4de03cf7e62eafde7e3e53d28 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Tue, 22 Feb 2022 18:46:54 -0800 Subject: [PATCH 19/28] use offset.toVector2() --- examples/lib/stories/camera_and_viewport/camera2_example.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/examples/lib/stories/camera_and_viewport/camera2_example.dart b/examples/lib/stories/camera_and_viewport/camera2_example.dart index 8398b6ed999..2dbb1758a25 100644 --- a/examples/lib/stories/camera_and_viewport/camera2_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera2_example.dart @@ -1,6 +1,7 @@ import 'dart:math'; import 'package:flame/components.dart'; +import 'package:flame/extensions.dart' show OffsetExtension; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame/src/experimental/camera.dart'; // ignore: implementation_imports @@ -32,7 +33,7 @@ class Camera2Example extends FlameGame with PanDetector { final camera = Camera2(world: world); await add(camera); final offset = world.curve.boundingRect().center; - center = Vector2(offset.dx, offset.dy); + center = offset.toVector2(); camera.viewfinder.position = Vector2(center.x, center.y); magnifyingGlass = Camera2(world: world, viewport: CircularViewport(radius)); From 2fe89f49ad85060840f02f35b8cad14a86789bee Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Tue, 22 Feb 2022 19:10:02 -0800 Subject: [PATCH 20/28] added docs; renamed handleResize -> onViewportResize --- .../camera_and_viewport/camera2_example.dart | 21 ++++++++++++++----- .../src/experimental/circular_viewport.dart | 2 +- .../fixed_aspect_ratio_viewport.dart | 2 +- .../src/experimental/fixed_size_viewport.dart | 2 +- .../lib/src/experimental/max_viewport.dart | 2 +- .../flame/lib/src/experimental/viewport.dart | 4 ++-- 6 files changed, 22 insertions(+), 11 deletions(-) diff --git a/examples/lib/stories/camera_and_viewport/camera2_example.dart b/examples/lib/stories/camera_and_viewport/camera2_example.dart index 2dbb1758a25..c67b3b29566 100644 --- a/examples/lib/stories/camera_and_viewport/camera2_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera2_example.dart @@ -475,24 +475,35 @@ class InsectLeg { this.l1, this.l2, this.l3, - this.bendDirection, - ) : r = l3 / 2, - dir = bendDirection ? -1 : 1, + bool bendDirection, + ) : dir = bendDirection ? -1 : 1, path = Path(), foot = Vector2.zero() { final ok = placeFoot(Vector2(x1, y1)); assert(ok); } + /// Place where the leg is attached to the body final double x0, y0; + + /// Place on the ground where the ant needs to place its foot final double x1, y1; - final double l1, l2, l3, r; - final bool bendDirection; + + /// Lengths of the 3 segments of the leg: [l1] is nearest to the body, [l2] + /// is the middle part, and [l3] is the "foot". + final double l1, l2, l3; + + /// +1 if the leg bends "forward", or -1 if backwards final double dir; + + /// The leg is drawn as a simple [path] polyline consisting of 3 segments. final Path path; + + /// This vector stores the position of the foot; it's equal to (x1, y1). final Vector2 foot; bool placeFoot(Vector2 pos) { + final r = l3 / 2; final rr = distance(pos.x, pos.y, x0, y0); if (rr < r) { return false; diff --git a/packages/flame/lib/src/experimental/circular_viewport.dart b/packages/flame/lib/src/experimental/circular_viewport.dart index a8fedda01e8..40fea9e3082 100644 --- a/packages/flame/lib/src/experimental/circular_viewport.dart +++ b/packages/flame/lib/src/experimental/circular_viewport.dart @@ -15,7 +15,7 @@ class CircularViewport extends Viewport { void clip(Canvas canvas) => canvas.clipPath(_clipPath, doAntiAlias: false); @override - void handleResize() { + void onViewportResize() { final x = size.x / 2; final y = size.y / 2; _clipPath = Path()..addOval(Rect.fromLTRB(-x, -y, x, y)); diff --git a/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart index 72042e08ad3..6cda0521442 100644 --- a/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart +++ b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart @@ -22,7 +22,7 @@ class FixedAspectRatioViewport extends Viewport { void clip(Canvas canvas) => canvas.clipRect(_clipRect); @override - void handleResize() { + void onViewportResize() { final desiredWidth = size.y * aspectRatio; if (desiredWidth > size.x) { size.y = size.x / aspectRatio; diff --git a/packages/flame/lib/src/experimental/fixed_size_viewport.dart b/packages/flame/lib/src/experimental/fixed_size_viewport.dart index d230c0b6155..5b2aa08a878 100644 --- a/packages/flame/lib/src/experimental/fixed_size_viewport.dart +++ b/packages/flame/lib/src/experimental/fixed_size_viewport.dart @@ -19,7 +19,7 @@ class FixedSizeViewport extends Viewport { void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false); @override - void handleResize() { + void onViewportResize() { final x = size.x / 2; final y = size.y / 2; _clipRect = Rect.fromLTRB(-x, -y, x, y); diff --git a/packages/flame/lib/src/experimental/max_viewport.dart b/packages/flame/lib/src/experimental/max_viewport.dart index 6af055d698e..8ecbe776534 100644 --- a/packages/flame/lib/src/experimental/max_viewport.dart +++ b/packages/flame/lib/src/experimental/max_viewport.dart @@ -19,5 +19,5 @@ class MaxViewport extends Viewport { void clip(Canvas canvas) {} @override - void handleResize() {} + void onViewportResize() {} } diff --git a/packages/flame/lib/src/experimental/viewport.dart b/packages/flame/lib/src/experimental/viewport.dart index 9f404c3f809..32bf2716383 100644 --- a/packages/flame/lib/src/experimental/viewport.dart +++ b/packages/flame/lib/src/experimental/viewport.dart @@ -43,7 +43,7 @@ abstract class Viewport extends Component { "Viewport's size cannot be negative: $value", ); _size.setFrom(value); - handleResize(); + onViewportResize(); } /// Apply clip mask to the [canvas]. @@ -61,7 +61,7 @@ abstract class Viewport extends Component { /// A typical use-case would be to adjust the viewport's clip mask to match /// the new size. @protected - void handleResize(); + void onViewportResize(); @mustCallSuper @override From 28a0ac07fa96fcca9f416ae781befc125b408151 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 3 Mar 2022 22:14:02 -0800 Subject: [PATCH 21/28] rename Camera2 -> CameraComponent --- .../camera_and_viewport/camera2_example.dart | 6 ++--- .../flame/lib/src/experimental/camera.dart | 24 +++++++++---------- .../lib/src/experimental/viewfinder.dart | 18 ++++++++------ .../flame/lib/src/experimental/viewport.dart | 9 ++++--- .../flame/lib/src/experimental/world.dart | 8 +++---- 5 files changed, 36 insertions(+), 29 deletions(-) diff --git a/examples/lib/stories/camera_and_viewport/camera2_example.dart b/examples/lib/stories/camera_and_viewport/camera2_example.dart index c67b3b29566..4cabb2f779e 100644 --- a/examples/lib/stories/camera_and_viewport/camera2_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera2_example.dart @@ -18,7 +18,7 @@ class Camera2Example extends FlameGame with PanDetector { look at the world underneath! '''; - late final Camera2 magnifyingGlass; + late final CameraComponent magnifyingGlass; late final Vector2 center; static const zoom = 10.0; static const radius = 130.0; @@ -30,13 +30,13 @@ class Camera2Example extends FlameGame with PanDetector { Future onLoad() async { final world = AntWorld(); await add(world); - final camera = Camera2(world: world); + final camera = CameraComponent(world: world); await add(camera); final offset = world.curve.boundingRect().center; center = offset.toVector2(); camera.viewfinder.position = Vector2(center.x, center.y); - magnifyingGlass = Camera2(world: world, viewport: CircularViewport(radius)); + magnifyingGlass = CameraComponent(world: world, viewport: CircularViewport(radius)); magnifyingGlass.viewport.add(Bezel(radius)); magnifyingGlass.viewfinder.zoom = zoom; } diff --git a/packages/flame/lib/src/experimental/camera.dart b/packages/flame/lib/src/experimental/camera.dart index 621f33f345b..3e814e93345 100644 --- a/packages/flame/lib/src/experimental/camera.dart +++ b/packages/flame/lib/src/experimental/camera.dart @@ -6,7 +6,7 @@ import 'viewfinder.dart'; import 'viewport.dart'; import 'world.dart'; -/// [Camera2] is a component through which a [World] is observed. +/// [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 @@ -20,15 +20,15 @@ import 'world.dart'; /// 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 { - Camera2({ +/// Since [CameraComponent] 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 CameraComponent extends Component { + CameraComponent({ required this.world, Viewport? viewport, Viewfinder? viewfinder, @@ -77,12 +77,12 @@ class Camera2 extends Component { /// 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 { + static CameraComponent? get currentCamera { return currentCameras.isEmpty ? null : currentCameras[0]; } /// Stack of all current cameras in the render tree. - static final List currentCameras = []; + static final List currentCameras = []; /// Maximum number of nested cameras that will be rendered. /// diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index dfbbfcc38e7..e0c0f5b0b96 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -9,8 +9,8 @@ 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. +/// [Viewfinder] is a part of a [CameraComponent] 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 @@ -46,7 +46,7 @@ class Viewfinder extends Component { set angle(double value) => _transform.angle = -value; /// Reference to the parent camera. - Camera2 get camera => parent! as Camera2; + CameraComponent get camera => parent! as CameraComponent; /// How much of a game world ought to be visible through the viewport. /// @@ -98,7 +98,10 @@ class Viewfinder extends Component { @mustCallSuper @override void onMount() { - assert(parent! is Camera2, 'Viewfinder can only be mounted to a Camera2'); + assert( + parent! is CameraComponent, + 'Viewfinder can only be mounted to a Camera2', + ); _initZoom(); } @@ -112,14 +115,15 @@ class Viewfinder extends Component { void renderFromViewport(Canvas canvas) { final world = camera.world; if (world.isMounted && - Camera2.currentCameras.length < Camera2.maxCamerasDepth) { + CameraComponent.currentCameras.length < + CameraComponent.maxCamerasDepth) { try { - Camera2.currentCameras.add(camera); + CameraComponent.currentCameras.add(camera); canvas.transform(_transform.transformMatrix.storage); world.renderFromCamera(canvas); super.renderTree(canvas); } finally { - Camera2.currentCameras.removeLast(); + CameraComponent.currentCameras.removeLast(); } } } diff --git a/packages/flame/lib/src/experimental/viewport.dart b/packages/flame/lib/src/experimental/viewport.dart index 32bf2716383..c641646b5a9 100644 --- a/packages/flame/lib/src/experimental/viewport.dart +++ b/packages/flame/lib/src/experimental/viewport.dart @@ -6,7 +6,7 @@ import 'package:vector_math/vector_math_64.dart'; import '../components/component.dart'; import 'camera.dart'; -/// [Viewport] is a part of a [Camera2] system. +/// [Viewport] is a part of a [CameraComponent] system. /// /// The viewport describes a "window" through which the underlying game world /// is observed. At the same time, the viewport is agnostic of the game world, @@ -66,12 +66,15 @@ abstract class Viewport extends Component { @mustCallSuper @override void onMount() { - assert(parent! is Camera2, 'A Viewport may only be attached to a Camera2'); + assert( + parent! is CameraComponent, + 'A Viewport may only be attached to a Camera2', + ); } @override void renderTree(Canvas canvas) { - final camera = parent! as Camera2; + final camera = parent! as CameraComponent; canvas.save(); canvas.translate(_position.x, _position.y); canvas.save(); diff --git a/packages/flame/lib/src/experimental/world.dart b/packages/flame/lib/src/experimental/world.dart index e556463234c..9d4fb481212 100644 --- a/packages/flame/lib/src/experimental/world.dart +++ b/packages/flame/lib/src/experimental/world.dart @@ -8,16 +8,16 @@ import 'camera.dart'; /// The root component for all game world elements. /// /// The primary feature of this component is that it disables regular rendering, -/// and allows itself to be rendered through a [Camera2] only. The updates -/// proceed through the world tree normally. +/// and allows itself to be rendered through a [CameraComponent] only. The +/// updates proceed through the world tree normally. class World extends Component { @override void renderTree(Canvas canvas) {} - /// Internal rendering method invoked by the [Camera2]. + /// Internal rendering method invoked by the [CameraComponent]. @internal void renderFromCamera(Canvas canvas) { - assert(Camera2.currentCamera != null); + assert(CameraComponent.currentCamera != null); super.renderTree(canvas); } } From 916c3a5fb366f2fd8161ec318f63d61a7e749087 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 3 Mar 2022 22:20:44 -0800 Subject: [PATCH 22/28] Create export experimental.dart file --- .../lib/stories/camera_and_viewport/camera2_example.dart | 2 +- packages/flame/lib/experimental.dart | 9 +++++++++ .../experimental/{camera.dart => camera_component.dart} | 0 packages/flame/lib/src/experimental/viewfinder.dart | 2 +- packages/flame/lib/src/experimental/viewport.dart | 2 +- packages/flame/lib/src/experimental/world.dart | 2 +- 6 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 packages/flame/lib/experimental.dart rename packages/flame/lib/src/experimental/{camera.dart => camera_component.dart} (100%) diff --git a/examples/lib/stories/camera_and_viewport/camera2_example.dart b/examples/lib/stories/camera_and_viewport/camera2_example.dart index 4cabb2f779e..8d0e1fdc660 100644 --- a/examples/lib/stories/camera_and_viewport/camera2_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera2_example.dart @@ -4,7 +4,7 @@ import 'package:flame/components.dart'; import 'package:flame/extensions.dart' show OffsetExtension; import 'package:flame/game.dart'; import 'package:flame/input.dart'; -import 'package:flame/src/experimental/camera.dart'; // ignore: implementation_imports +import 'package:flame/src/experimental/camera_component.dart'; // ignore: implementation_imports import 'package:flame/src/experimental/circular_viewport.dart'; // ignore: implementation_imports import 'package:flame/src/experimental/world.dart'; // ignore: implementation_imports import 'package:flutter/painting.dart'; diff --git a/packages/flame/lib/experimental.dart b/packages/flame/lib/experimental.dart new file mode 100644 index 00000000000..f60cf3a35b5 --- /dev/null +++ b/packages/flame/lib/experimental.dart @@ -0,0 +1,9 @@ +export 'src/experimental/camera_component.dart' show CameraComponent; +export 'src/experimental/circular_viewport.dart' show CircularViewport; +export 'src/experimental/fixed_aspect_ratio_viewport.dart' + show FixedAspectRatioViewport; +export 'src/experimental/fixed_size_viewport.dart' show FixedSizeViewport; +export 'src/experimental/max_viewport.dart' show MaxViewport; +export 'src/experimental/viewfinder.dart' show Viewfinder; +export 'src/experimental/viewport.dart' show Viewport; +export 'src/experimental/world.dart' show World; diff --git a/packages/flame/lib/src/experimental/camera.dart b/packages/flame/lib/src/experimental/camera_component.dart similarity index 100% rename from packages/flame/lib/src/experimental/camera.dart rename to packages/flame/lib/src/experimental/camera_component.dart diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index e0c0f5b0b96..6b8b5ef7cd5 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -6,7 +6,7 @@ import 'package:vector_math/vector_math_64.dart'; import '../components/component.dart'; import '../game/transform2d.dart'; -import 'camera.dart'; +import 'camera_component.dart'; import 'viewport.dart'; /// [Viewfinder] is a part of a [CameraComponent] system that controls which diff --git a/packages/flame/lib/src/experimental/viewport.dart b/packages/flame/lib/src/experimental/viewport.dart index c641646b5a9..9d0e7101a9c 100644 --- a/packages/flame/lib/src/experimental/viewport.dart +++ b/packages/flame/lib/src/experimental/viewport.dart @@ -4,7 +4,7 @@ import 'package:meta/meta.dart'; import 'package:vector_math/vector_math_64.dart'; import '../components/component.dart'; -import 'camera.dart'; +import 'camera_component.dart'; /// [Viewport] is a part of a [CameraComponent] system. /// diff --git a/packages/flame/lib/src/experimental/world.dart b/packages/flame/lib/src/experimental/world.dart index 9d4fb481212..801b3390ef5 100644 --- a/packages/flame/lib/src/experimental/world.dart +++ b/packages/flame/lib/src/experimental/world.dart @@ -3,7 +3,7 @@ import 'dart:ui'; import 'package:meta/meta.dart'; import '../components/component.dart'; -import 'camera.dart'; +import 'camera_component.dart'; /// The root component for all game world elements. /// From c8927cfc56e847c2142977f2f6008f4700e414e8 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 3 Mar 2022 22:26:52 -0800 Subject: [PATCH 23/28] rename Camera2Example -> CameraComponentExample --- .../camera_and_viewport/camera_and_viewport.dart | 10 +++++----- ...2_example.dart => camera_component_example.dart} | 13 ++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) rename examples/lib/stories/camera_and_viewport/{camera2_example.dart => camera_component_example.dart} (97%) diff --git a/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart b/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart index b3b5f7d7038..25fe9d7e95b 100644 --- a/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart +++ b/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart @@ -2,7 +2,7 @@ import 'package:dashbook/dashbook.dart'; import 'package:flame/game.dart'; import '../../commons/commons.dart'; -import 'camera2_example.dart'; +import 'camera_component_example.dart'; import 'coordinate_systems_example.dart'; import 'fixed_resolution_example.dart'; import 'follow_component_example.dart'; @@ -62,9 +62,9 @@ void addCameraAndViewportStories(Dashbook dashbook) { info: CoordinateSystemsExample.description, ) ..add( - 'Camera 2', - (context) => GameWidget(game: Camera2Example()), - codeLink: baseLink('camera_and_viewport/camera2_example.dart'), - info: Camera2Example.description, + 'CameraComponent', + (context) => GameWidget(game: CameraComponentExample()), + codeLink: baseLink('camera_and_viewport/camera_component_example.dart'), + info: CameraComponentExample.description, ); } diff --git a/examples/lib/stories/camera_and_viewport/camera2_example.dart b/examples/lib/stories/camera_and_viewport/camera_component_example.dart similarity index 97% rename from examples/lib/stories/camera_and_viewport/camera2_example.dart rename to examples/lib/stories/camera_and_viewport/camera_component_example.dart index 8d0e1fdc660..fcad4c49a48 100644 --- a/examples/lib/stories/camera_and_viewport/camera2_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera_component_example.dart @@ -1,18 +1,16 @@ import 'dart:math'; import 'package:flame/components.dart'; +import 'package:flame/experimental.dart'; import 'package:flame/extensions.dart' show OffsetExtension; import 'package:flame/game.dart'; import 'package:flame/input.dart'; -import 'package:flame/src/experimental/camera_component.dart'; // ignore: implementation_imports -import 'package:flame/src/experimental/circular_viewport.dart'; // ignore: implementation_imports -import 'package:flame/src/experimental/world.dart'; // ignore: implementation_imports import 'package:flutter/painting.dart'; -class Camera2Example extends FlameGame with PanDetector { +class CameraComponentExample extends FlameGame with PanDetector { static const description = ''' - This example shows how a camera can be dynamically added into a game via - the Camera2 component. + This example shows how a camera can be dynamically added into a game using + a CameraComponent. Click and hold the mouse to bring up a magnifying glass, then have a better look at the world underneath! @@ -36,7 +34,8 @@ class Camera2Example extends FlameGame with PanDetector { center = offset.toVector2(); camera.viewfinder.position = Vector2(center.x, center.y); - magnifyingGlass = CameraComponent(world: world, viewport: CircularViewport(radius)); + magnifyingGlass = + CameraComponent(world: world, viewport: CircularViewport(radius)); magnifyingGlass.viewport.add(Bezel(radius)); magnifyingGlass.viewfinder.zoom = zoom; } From 101732e0c89386fc8ab5058e6127f9c2ce77862a Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 3 Mar 2022 22:36:09 -0800 Subject: [PATCH 24/28] Added doc for the experimental.dart file --- packages/flame/lib/experimental.dart | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/flame/lib/experimental.dart b/packages/flame/lib/experimental.dart index f60cf3a35b5..4b4fe0a1f4f 100644 --- a/packages/flame/lib/experimental.dart +++ b/packages/flame/lib/experimental.dart @@ -1,3 +1,15 @@ +/// Classes and components in this sub-module are considered experimental, +/// that is, their API may still be incomplete and subject to change at a more +/// rapid pace than the rest of the Flame code. +/// +/// However, do not feel discouraged to use this functionality: on the contrary, +/// consider this as a way to help the Flame community by beta-testing new +/// components. +/// +/// After the components lived here for some time, and when we gain more +/// confidence in their robustness, they will be moved out into the main Flame +/// library. + export 'src/experimental/camera_component.dart' show CameraComponent; export 'src/experimental/circular_viewport.dart' show CircularViewport; export 'src/experimental/fixed_aspect_ratio_viewport.dart' From 567181eb356ca589efb91991312d980594597aa6 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Thu, 3 Mar 2022 23:00:41 -0800 Subject: [PATCH 25/28] fix re-adding components with children --- .../flame/lib/src/components/component.dart | 12 ++++--- .../components/component_lifecycle_test.dart | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/packages/flame/lib/src/components/component.dart b/packages/flame/lib/src/components/component.dart index 2b7906218aa..4dfdf2705d3 100644 --- a/packages/flame/lib/src/components/component.dart +++ b/packages/flame/lib/src/components/component.dart @@ -394,9 +394,9 @@ class Component { /// and later re-mounted. For these components we need to run [onGameResize] /// (since they haven't passed through [add]), but we don't have to add them /// to the parent's children because they are already there. - @internal - void mount({bool existingChild = false}) { - assert(_parent!.isMounted); + void _mount({Component? parent, bool existingChild = false}) { + _parent ??= parent; + assert(_parent != null && _parent!.isMounted); assert(_state == LifecycleState.loaded || _state == LifecycleState.removed); if (existingChild || _state == LifecycleState.removed) { onGameResize(findGame()!.canvasSize); @@ -407,7 +407,9 @@ class Component { _parent!.children.add(this); } if (_children != null) { - _children!.forEach((child) => child.mount(existingChild: true)); + _children!.forEach( + (child) => child._mount(parent: this, existingChild: true), + ); } _lifecycleManager?.processChildrenQueue(); } @@ -584,7 +586,7 @@ class _LifecycleManager { final child = _children.first; assert(child.parent!.isMounted); if (child.isLoaded) { - child.mount(); + child._mount(); _children.removeFirst(); } else if (child._state == LifecycleState.loading) { break; diff --git a/packages/flame/test/components/component_lifecycle_test.dart b/packages/flame/test/components/component_lifecycle_test.dart index 63738bd8a03..2ddad7c1582 100644 --- a/packages/flame/test/components/component_lifecycle_test.dart +++ b/packages/flame/test/components/component_lifecycle_test.dart @@ -141,6 +141,38 @@ void main() { ); }); }); + + testWithFlameGame( + 'Remove and re-add component with children', + (game) async { + final parent = _MyComponent('parent'); + final child = _MyComponent('child')..addToParent(parent); + await game.add(parent); + await game.ready(); + + expect(parent.isMounted, true); + expect(child.isMounted, true); + expect(parent.parent, game); + expect(child.parent, parent); + + parent.removeFromParent(); + game.update(0); // needed until 1385 is merged + await game.ready(); + + expect(parent.isMounted, false); + expect(child.isMounted, false); + expect(parent.parent, isNull); + expect(child.parent, isNull); + + await game.add(parent); + await game.ready(); + + expect(parent.isMounted, true); + expect(child.isMounted, true); + expect(parent.parent, game); + expect(child.parent, parent); + }, + ); }); } From 199b8e05f714ce9ceb5e40a83719750726c45f4d Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Mar 2022 00:01:45 -0800 Subject: [PATCH 26/28] Added support for admonitions in the docs --- doc/_sphinx/theme/flames.css | 64 +++++++++++++++++++++++++++++++++++ doc/flame/camera_component.md | 6 ++++ doc/flame/flame.md | 1 + 3 files changed, 71 insertions(+) create mode 100644 doc/flame/camera_component.md diff --git a/doc/_sphinx/theme/flames.css b/doc/_sphinx/theme/flames.css index c29184a72de..d3b4ac969d3 100644 --- a/doc/_sphinx/theme/flames.css +++ b/doc/_sphinx/theme/flames.css @@ -706,3 +706,67 @@ div.highlight-box button.close { font-size: 20px; margin: 0 0 0 20px; } + + +/*----------------------------------------------------------------------------* + * Admonitions + *----------------------------------------------------------------------------*/ + +div.admonition { + background: #333333; + border-left: 3px solid var(--admonition-border-color); + border-radius: 5px; + box-shadow: 1px 1px 4px black; + margin: 12pt 0; + padding: 0 0 6pt 0; + position: relative; +} + +div.admonition > p.admonition-title { + background-color: var(--admonition-title-background-color); + border-radius: 5px 5px 0 0; + color: silver; + margin: 0 0 6pt 0; + padding: 4px 12px 4px 30px; +} + +div.admonition > p.admonition-title:before { + color: var(--admonition-icon-color); + content: var(--admonition-icon); + font-family: var(--font-awesome); + left: 8px; + padding-right: 4pt; + position: absolute; +} + +div.admonition > p { + margin: 0 12px 12px 30px; +} + +div.admonition.warning { + --admonition-border-color: orange; + --admonition-icon: '\f071'; + --admonition-icon-color: gold; + --admonition-title-background-color: #503e1a; +} + +div.admonition.error { + --admonition-border-color: red; + --admonition-icon: '\f188'; + --admonition-icon-color: #ff7c7c; + --admonition-title-background-color: #460202; +} + +div.admonition.note { + --admonition-border-color: #6eb1cc; + --admonition-icon: '\f05a'; + --admonition-icon-color: #ace3fa; + --admonition-title-background-color: #235179; +} + +div.admonition.seealso { + --admonition-border-color: #54d452; + --admonition-icon: '\f064'; + --admonition-icon-color: #acfab6; + --admonition-title-background-color: #285131; +} diff --git a/doc/flame/camera_component.md b/doc/flame/camera_component.md new file mode 100644 index 00000000000..513b2f5fc49 --- /dev/null +++ b/doc/flame/camera_component.md @@ -0,0 +1,6 @@ +# Camera component + +```{note} +This document describes a new experimental API. The more traditional approach +for handling a camera is described in [](camera_and_viewport.md). +``` diff --git a/doc/flame/flame.md b/doc/flame/flame.md index dcb2ccc7ed1..440d0a2e31f 100644 --- a/doc/flame/flame.md +++ b/doc/flame/flame.md @@ -11,6 +11,7 @@ Collision detection Effects Camera & Viewport + Camera component Inputs Rendering Other From b21102adf37ef5835f0064ae45198fde55367f3b Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Mar 2022 01:15:42 -0800 Subject: [PATCH 27/28] Documentation for CameraComponent --- doc/flame/camera_component.md | 108 ++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/doc/flame/camera_component.md b/doc/flame/camera_component.md index 513b2f5fc49..e6c6b2390b8 100644 --- a/doc/flame/camera_component.md +++ b/doc/flame/camera_component.md @@ -4,3 +4,111 @@ This document describes a new experimental API. The more traditional approach for handling a camera is described in [](camera_and_viewport.md). ``` + +Camera-as-a-component is an alternative way of structuring a game, an approach +that allows more flexibility in placing the camera, or even having more than +one camera simultaneously. + +In order to understand how this approach works, imagine that your game world is +an entity that exists _somewhere_ independently from your application. Imagine +that your game is merely a window through which you can look into that world. +That you can close that window at any moment, and the game world would still be +there. Or, on the contrary, you can open multiple windows that all look at the +same world (or different worlds) at the same time. + +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. + +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. + + +## World + +This component should be used to host all other components that comprise your +game world. The main property of the `World` class is that it does not render +through traditional means -- instead, create one or more [](#cameracomponent)s +to "look at" the world. + +A game can have multiple `World` instances that can be rendered either at the +same time, or at different times. For example, if you have two worlds A and B +and a single camera, then switching that camera's target from A to B will +instantaneously switch the view to world B without having to unmount A and +then mount B. + + +## CameraComponent + +This is a component through which a `World` is rendered. It requires a +reference to a `World` instance during construction; however later the target +world can be replaced with another one. Multiple cameras can observe the same +world at the same time. + +A `CameraComponent` has two other components inside: a [](#viewport) and a +[](#viewfinder). Unlike the `World` object, the camera owns the viewport and +the viewfinder, which means those components are children of the camera. + +There is also a static property `CameraComponent.currentCamera` which is not +null only during the rendering stage, and it returns the camera object that +currently performs rendering. This is needed only for certain advanced use +cases where the rendering of a component depends on the camera settings. For +example, some components may decide to skip rendering themselves and their +children if they are outside of the camera's viewport. + + +## Viewport + +The `Viewport` is a window through which the `World` is seen. That window +has a certain size, shape, and position on the screen. There are multiple kinds +of viewports available, and you can always implement your own. + +The `Viewport` is a component, which means you can add other components to it. +These children components will be affected by the viewport's position, but not +by its clip mask. Thus, if a viewport is a "window" into the game world, then +its children are things that you can put on top of the window. + +Adding elements to the viewport is a convenient way to implement "HUD" +components. + +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. + - `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. + + +## Viewfinder + +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. + +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. + + +## Comparison to the traditional camera + +Compared to the normal [Camera](camera_and_viewport.md), the `CameraComponent` +has several advantages and drawbacks. + +Pros: + - Multiple cameras can be added to the game at the same time; + - More flexibility in choosing the placement and the size of the viewport; + - Switching camera from one world to another can happen instantaneously, + without having to unmount one world and then mount another; + - Support rotation of the world view; + - (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): + - Camera controls are not yet implemented; + - Events propagation may not always work correctly. From d2affe34e05e7c2e2174dc91c63b64ad6e6cdc77 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Fri, 4 Mar 2022 01:16:09 -0800 Subject: [PATCH 28/28] format --- packages/flame/test/components/component_lifecycle_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flame/test/components/component_lifecycle_test.dart b/packages/flame/test/components/component_lifecycle_test.dart index 2ddad7c1582..67f5fb12dd6 100644 --- a/packages/flame/test/components/component_lifecycle_test.dart +++ b/packages/flame/test/components/component_lifecycle_test.dart @@ -156,7 +156,7 @@ void main() { expect(child.parent, parent); parent.removeFromParent(); - game.update(0); // needed until 1385 is merged + game.update(0); // needed until 1385 is merged await game.ready(); expect(parent.isMounted, false);