diff --git a/doc/flame/camera_component.md b/doc/flame/camera_component.md index 3efefe35f3..0ac65912eb 100644 --- a/doc/flame/camera_component.md +++ b/doc/flame/camera_component.md @@ -21,21 +21,14 @@ With this mindset, we can now understand how camera-as-a-component works. First, there is the [](#world) class, which contains all components that are inside your game world. The `World` component can be mounted anywhere, for -example at the root of your game class. +example at the root of your game class, like the built-in `World` is. -Then, a [](#cameracomponent) class that "looks at" the `World`. The -`CameraComponent` has a `Viewport` and a `Viewfinder` inside, allowing both the -flexibility of rendering the world at any place on the screen, and also control -the viewing location and angle. - -If you add children to the `Viewport` they will appear as static HUDs in -front of the world and if you add children to the `Viewfinder` they will appear -statically in front of the viewport. - -To add static components behind the world you can add them to the `backdrop` -component, or replace the `backdrop` component. This is for example useful if -you want to have a static `ParallaxComponent` beneath a world that you can move -around it. +Then, a [](#cameracomponent) class that "looks at" the [](#world). The +`CameraComponent` has a [](#viewport) and a [](#viewfinder) inside, allowing +both the flexibility of rendering the world at any place on the screen, and +also control the viewing location and angle. The `CameraComponent` also +contains a [](#backdrop) component which is statically rendered below the +world. ## World @@ -152,12 +145,17 @@ The following viewports are available: - `MaxViewport` (default) -- this viewport expands to the maximum size allowed by the game, i.e. it will be equal to the size of the game canvas. +- `FixedResolutionViewport` -- keeps the resolution and aspect ratio fixed, with black bars on the + sides if it doesn't match the aspect ratio. - `FixedSizeViewport` -- a simple rectangular viewport with predefined size. - `FixedAspectRatioViewport` -- a rectangular viewport which expands to fit into the game canvas, but preserving its aspect ratio. - `CircularViewport` -- a viewport in the shape of a circle, fixed size. +If you add children to the `Viewport` they will appear as static HUDs in front of the world. + + ## Viewfinder This part of the camera is responsible for knowing which location in the @@ -171,9 +169,33 @@ main character who is displayed not in the center of the screen but closer to the lower-left corner. This off-center position would be the "logical center" of the camera, controlled by the viewfinder's `anchor`. -Components added to the `Viewfinder` as children will be rendered as if they -were part of the world (but on top). It is more useful to add behavioral -components to the viewfinder, for example [](effects.md) or other controllers. +If you add children to the `Viewfinder` they will appear will appear in front +of the world, but behind the viewport and with the same transformations as are +applied to the world, so these components are not static. + +You can also add behavioral components as children to the viewfinder, for +example [](effects.md) or other controllers. If you for example would add a +`ScaleEffect` you would be able to achieve a smooth zoom in your game. + + +## Backdrop + +To add static components behind the world you can add them to the `backdrop` +component, or replace the `backdrop` component. This is for example useful if +you want to have a static `ParallaxComponent` beneath a world that you can move +around it. + +Example: + +```dart +camera.backdrop.add(MyStaticBackground()); +``` + +or + +```dart +camera.backdrop = MyStaticBackground(); +``` ## Camera controls diff --git a/examples/.metadata b/examples/.metadata index 732ba6d37c..cef2b7a680 100644 --- a/examples/.metadata +++ b/examples/.metadata @@ -1,11 +1,11 @@ # This file tracks properties of this Flutter project. # Used by Flutter tool to assess capabilities and perform upgrades etc. # -# This file should be version controlled. +# This file should be version controlled and should not be manually edited. version: - revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - channel: stable + revision: "367f9ea16bfae1ca451b9cc27c1366870b187ae2" + channel: "stable" project_type: app @@ -13,11 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - - platform: android - create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + - platform: linux + create_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 + base_revision: 367f9ea16bfae1ca451b9cc27c1366870b187ae2 # User provided section 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 d1e41d7fe0..bd84ebc495 100644 --- a/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart +++ b/examples/lib/stories/camera_and_viewport/camera_and_viewport.dart @@ -45,13 +45,8 @@ void addCameraAndViewportStories(Dashbook dashbook) { ..add( 'Fixed Resolution viewport', (context) { - return GameWidget( - game: FixedResolutionExample( - viewportResolution: Vector2( - context.numberProperty('viewport width', 600), - context.numberProperty('viewport height', 1024), - ), - ), + return const GameWidget.controlled( + gameFactory: FixedResolutionExample.new, ); }, codeLink: baseLink('camera_and_viewport/fixed_resolution_example.dart'), diff --git a/examples/lib/stories/camera_and_viewport/fixed_resolution_example.dart b/examples/lib/stories/camera_and_viewport/fixed_resolution_example.dart index d01075390d..886a834fe2 100644 --- a/examples/lib/stories/camera_and_viewport/fixed_resolution_example.dart +++ b/examples/lib/stories/camera_and_viewport/fixed_resolution_example.dart @@ -1,9 +1,10 @@ -import 'dart:ui'; - import 'package:flame/components.dart'; +import 'package:flame/events.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame/palette.dart'; +import 'package:flame/text.dart'; +import 'package:flutter/material.dart'; class FixedResolutionExample extends FlameGame with ScrollDetector, ScaleDetector { @@ -12,32 +13,84 @@ class FixedResolutionExample extends FlameGame It is useful when you want the visible part of the game to be the same on all devices no matter the actual screen size of the device. Resize the window or change device orientation to see the difference. + + If you tap once you will set the zoom to 2 and if you tap again it goes back + to 1, so that you can test how it works with a zoom level. '''; - FixedResolutionExample({ - required Vector2 viewportResolution, - }) : super( - camera: CameraComponent.withFixedResolution( - width: viewportResolution.x, - height: viewportResolution.y, - ), + FixedResolutionExample() + : super( + camera: CameraComponent.withFixedResolution(width: 600, height: 1024), + world: FixedResolutionWorld(), ); @override Future onLoad() async { - final flameSprite = await loadSprite('layers/player.png'); + final textRenderer = TextPaint( + style: TextStyle(fontSize: 25, color: BasicPalette.black.color), + ); + camera.viewport.add( + TextButton( + text: 'Viewport\ncomponent', + position: Vector2.all(10), + textRenderer: textRenderer, + ), + ); + camera.viewfinder.add( + TextButton( + text: 'Viewfinder\ncomponent', + textRenderer: textRenderer, + position: Vector2(0, 200), + anchor: Anchor.center, + ), + ); + camera.viewport.add( + TextButton( + text: 'Viewport\ncomponent', + position: camera.viewport.virtualSize - Vector2.all(10), + textRenderer: textRenderer, + anchor: Anchor.bottomRight, + ), + ); + } +} + +class FixedResolutionWorld extends World + with HasGameReference, TapCallbacks, DoubleTapCallbacks { + final red = BasicPalette.red.paint(); + + @override + Future onLoad() async { + final flameSprite = await game.loadSprite('layers/player.png'); - world.add(Background()); - world.add( + add(Background()); + add( SpriteComponent( sprite: flameSprite, size: Vector2(149, 211), )..anchor = Anchor.center, ); } + + @override + void onTapDown(TapDownEvent event) { + add( + CircleComponent( + radius: 2, + position: event.localPosition, + paint: red, + ), + ); + } + + @override + void onDoubleTapDown(DoubleTapDownEvent event) { + final currentZoom = game.camera.viewfinder.zoom; + game.camera.viewfinder.zoom = currentZoom > 1 ? 1 : 2; + } } -class Background extends PositionComponent with HasGameRef { +class Background extends PositionComponent { @override int priority = -1; @@ -57,3 +110,32 @@ class Background extends PositionComponent with HasGameRef { canvas.drawRect(hugeRect, white); } } + +class TextButton extends ButtonComponent { + TextButton({ + required String text, + required super.position, + super.anchor, + TextRenderer? textRenderer, + }) : super( + button: RectangleComponent( + size: Vector2(200, 100), + paint: Paint() + ..color = Colors.orange + ..strokeWidth = 2 + ..style = PaintingStyle.stroke, + ), + buttonDown: RectangleComponent( + size: Vector2(200, 100), + paint: Paint()..color = BasicPalette.orange.color.withOpacity(0.5), + ), + children: [ + TextComponent( + text: text, + textRenderer: textRenderer, + position: Vector2(100, 50), + anchor: Anchor.center, + ), + ], + ); +} diff --git a/packages/flame/lib/effects.dart b/packages/flame/lib/effects.dart index 01a6a64452..ac4ec21afa 100644 --- a/packages/flame/lib/effects.dart +++ b/packages/flame/lib/effects.dart @@ -35,6 +35,7 @@ export 'src/effects/provider_interfaces.dart' ScaleProvider, SizeProvider, ReadOnlyPositionProvider, + ReadOnlyScaleProvider, ReadOnlySizeProvider, OpacityProvider; export 'src/effects/remove_effect.dart'; diff --git a/packages/flame/lib/src/camera/camera_component.dart b/packages/flame/lib/src/camera/camera_component.dart index 456ae989e6..440398a129 100644 --- a/packages/flame/lib/src/camera/camera_component.dart +++ b/packages/flame/lib/src/camera/camera_component.dart @@ -1,10 +1,9 @@ -import 'dart:ui'; - +import 'package:flame/extensions.dart'; import 'package:flame/src/camera/behaviors/bounded_position_behavior.dart'; import 'package:flame/src/camera/behaviors/follow_behavior.dart'; import 'package:flame/src/camera/viewfinder.dart'; import 'package:flame/src/camera/viewport.dart'; -import 'package:flame/src/camera/viewports/fixed_aspect_ratio_viewport.dart'; +import 'package:flame/src/camera/viewports/fixed_resolution_viewport.dart'; import 'package:flame/src/camera/viewports/max_viewport.dart'; import 'package:flame/src/camera/world.dart'; import 'package:flame/src/components/core/component.dart'; @@ -16,7 +15,6 @@ import 'package:flame/src/effects/move_to_effect.dart'; import 'package:flame/src/effects/provider_interfaces.dart'; import 'package:flame/src/experimental/geometry/shapes/shape.dart'; import 'package:flame/src/game/flame_game.dart'; -import 'package:vector_math/vector_math_64.dart'; /// [CameraComponent] is a component through which a [World] is observed. /// @@ -70,15 +68,16 @@ class CameraComponent extends Component { factory CameraComponent.withFixedResolution({ required double width, required double height, + Viewfinder? viewfinder, World? world, Component? backdrop, List? hudComponents, }) { return CameraComponent( world: world, - viewport: FixedAspectRatioViewport(aspectRatio: width / height) + viewport: FixedResolutionViewport(resolution: Vector2(width, height)) ..addAll(hudComponents ?? []), - viewfinder: Viewfinder()..visibleGameSize = Vector2(width, height), + viewfinder: viewfinder ?? Viewfinder(), backdrop: backdrop, ); } @@ -175,25 +174,29 @@ class CameraComponent extends Component { viewport.position.x - viewport.anchor.x * viewport.size.x, viewport.position.y - viewport.anchor.y * viewport.size.y, ); - backdrop.renderTree(canvas); // Render the world through the viewport if ((world?.isMounted ?? false) && currentCameras.length < maxCamerasDepth) { canvas.save(); viewport.clip(canvas); + viewport.transformCanvas(canvas); + backdrop.renderTree(canvas); + canvas.save(); try { currentCameras.add(this); - canvas.transform(viewfinder.transform.transformMatrix.storage); + canvas.transform2D(viewfinder.transform); world!.renderFromCamera(canvas); + // Render the viewfinder elements, which will be in front of the world, + // but with the same transforms applied to them. + viewfinder.renderTree(canvas); } finally { currentCameras.removeLast(); } canvas.restore(); + // Render the viewport elements, which will be in front of the world. + viewport.renderTree(canvas); + canvas.restore(); } - // Render the viewport elements, which will be in front of the world. - viewport.renderTree(canvas); - // Render the viewfinder elements, which will be in front of the viewport. - viewfinder.renderTree(canvas); canvas.restore(); } diff --git a/packages/flame/lib/src/camera/viewfinder.dart b/packages/flame/lib/src/camera/viewfinder.dart index 0c77580fe4..a380a4c28f 100644 --- a/packages/flame/lib/src/camera/viewfinder.dart +++ b/packages/flame/lib/src/camera/viewfinder.dart @@ -1,13 +1,13 @@ import 'dart:math'; -import 'dart:ui'; +import 'package:flame/extensions.dart'; import 'package:flame/src/anchor.dart'; import 'package:flame/src/camera/camera_component.dart'; import 'package:flame/src/components/core/component.dart'; +import 'package:flame/src/components/mixins/parent_is_a.dart'; import 'package:flame/src/effects/provider_interfaces.dart'; import 'package:flame/src/game/transform2d.dart'; import 'package:meta/meta.dart'; -import 'package:vector_math/vector_math_64.dart'; /// [Viewfinder] is a part of a [CameraComponent] system that controls which /// part of the game world is currently visible through a viewport. @@ -19,6 +19,7 @@ import 'package:vector_math/vector_math_64.dart'; /// If you add children to the [Viewfinder] they will appear like HUDs i.e. /// statically in front of the world. class Viewfinder extends Component + with ParentIsA implements AnchorProvider, AngleProvider, PositionProvider, ScaleProvider { /// Internal transform matrix used by the viewfinder. final Transform2D _transform = Transform2D(); @@ -33,7 +34,7 @@ class Viewfinder extends Component @override set position(Vector2 value) { _transform.offset = -value; - _visibleRect = null; + visibleRect = null; } /// Zoom level of the game. @@ -49,7 +50,7 @@ class Viewfinder extends Component set zoom(double value) { assert(value > 0, 'zoom level must be positive: $value'); _transform.scale = Vector2.all(value); - _visibleRect = null; + visibleRect = null; } /// Rotation angle of the game world, in radians. @@ -60,7 +61,7 @@ class Viewfinder extends Component @override set angle(double value) { _transform.angle = -value; - _visibleRect = null; + visibleRect = null; } /// The point within the viewport that is considered the "logical center" of @@ -82,7 +83,7 @@ class Viewfinder extends Component } /// Reference to the parent camera. - CameraComponent get camera => parent! as CameraComponent; + CameraComponent get camera => parent; /// Convert a point from the global coordinate system to the viewfinder's /// coordinate system. @@ -92,7 +93,7 @@ class Viewfinder extends Component /// /// Opposite of [localToGlobal]. Vector2 globalToLocal(Vector2 point, {Vector2? output}) { - return _transform.globalToLocal(point, output: output); + return transform.globalToLocal(point, output: output); } /// Convert a point from the viewfinder's coordinate system to the global @@ -103,7 +104,7 @@ class Viewfinder extends Component /// /// Opposite of [globalToLocal]. Vector2 localToGlobal(Vector2 point, {Vector2? output}) { - return _transform.localToGlobal(point, output: output); + return transform.localToGlobal(point, output: output); } /// How much of a game world ought to be visible through the viewport. @@ -122,6 +123,9 @@ class Viewfinder extends Component /// 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. + /// + /// If you set the [visibleGameSize] you will remove any fixed resolution + /// constraints that you might have previously put. Vector2? get visibleGameSize => _visibleGameSize; Vector2? _visibleGameSize; set visibleGameSize(Vector2? value) { @@ -133,25 +137,30 @@ class Viewfinder extends Component 'visibleGameSize cannot be negative: $value', ); _visibleGameSize = value; - _initZoom(); + _updateZoom(); } } /// See [CameraComponent.visibleWorldRect]. @internal - Rect get visibleWorldRect => _visibleRect ??= _computeVisibleRect(); - Rect? _visibleRect; - Rect _computeVisibleRect() { + Rect get visibleWorldRect => visibleRect ??= computeVisibleRect(); + @internal + Rect? visibleRect; + @protected + Rect computeVisibleRect() { final viewportSize = camera.viewport.size; - final topLeft = _transform.globalToLocal(Vector2.zero()); - final bottomRight = _transform.globalToLocal(viewportSize); + final currentTransform = transform; + final topLeft = currentTransform.globalToLocal(Vector2.zero()); + final bottomRight = currentTransform.globalToLocal(viewportSize); var minX = min(topLeft.x, bottomRight.x); var minY = min(topLeft.y, bottomRight.y); var maxX = max(topLeft.x, bottomRight.x); var maxY = max(topLeft.y, bottomRight.y); if (angle != 0) { - final topRight = _transform.globalToLocal(Vector2(viewportSize.x, 0)); - final bottomLeft = _transform.globalToLocal(Vector2(0, viewportSize.y)); + final topRight = + currentTransform.globalToLocal(Vector2(viewportSize.x, 0)); + final bottomLeft = + currentTransform.globalToLocal(Vector2(0, viewportSize.y)); minX = min(minX, min(topRight.x, bottomLeft.x)); minY = min(minY, min(topRight.y, bottomLeft.y)); maxX = max(maxX, max(topRight.x, bottomLeft.x)); @@ -161,8 +170,8 @@ class Viewfinder extends Component } /// Set [zoom] level based on the [_visibleGameSize]. - void _initZoom() { - if (parent != null && _visibleGameSize != null) { + void _updateZoom() { + if (_visibleGameSize != null) { final viewportSize = camera.viewport.size; final zoomX = viewportSize.x / _visibleGameSize!.x; final zoomY = viewportSize.y / _visibleGameSize!.y; @@ -172,19 +181,17 @@ class Viewfinder extends Component @override void onGameResize(Vector2 size) { - _initZoom(); + _updateZoom(); super.onGameResize(size); } /// Called by the viewport when its size changes. @internal void onViewportResize() { - if (parent != null) { - final viewportSize = camera.viewport.size; - _transform.position.x = viewportSize.x * _anchor.x; - _transform.position.y = viewportSize.y * _anchor.y; - _visibleRect = null; - } + final viewportSize = camera.viewport.virtualSize; + _transform.position.x = viewportSize.x * _anchor.x; + _transform.position.y = viewportSize.y * _anchor.y; + visibleRect = null; } @mustCallSuper @@ -198,16 +205,13 @@ class Viewfinder extends Component @mustCallSuper @override void onMount() { + super.onMount(); updateTransform(); } @internal void updateTransform() { - assert( - parent! is CameraComponent, - 'Viewfinder can only be mounted to a CameraComponent', - ); - _initZoom(); + _updateZoom(); onViewportResize(); } @@ -224,6 +228,6 @@ class Viewfinder extends Component ); assert(value.x > 0, 'Zoom must be positive: ${value.x}'); _transform.scale = value; - _visibleRect = null; + visibleRect = null; } } diff --git a/packages/flame/lib/src/camera/viewport.dart b/packages/flame/lib/src/camera/viewport.dart index 248eef53f0..51d966fcd1 100644 --- a/packages/flame/lib/src/camera/viewport.dart +++ b/packages/flame/lib/src/camera/viewport.dart @@ -4,6 +4,7 @@ import 'package:flame/game.dart'; import 'package:flame/src/anchor.dart'; import 'package:flame/src/camera/camera_component.dart'; import 'package:flame/src/components/core/component.dart'; +import 'package:flame/src/components/mixins/parent_is_a.dart'; import 'package:flame/src/effects/provider_interfaces.dart'; import 'package:meta/meta.dart'; @@ -21,6 +22,7 @@ import 'package:meta/meta.dart'; /// A viewport establishes its own local coordinate system, with the origin at /// the top left corner of the viewport's bounding box. abstract class Viewport extends Component + with ParentIsA implements AnchorProvider, PositionProvider, SizeProvider { Viewport({super.children}); @@ -64,6 +66,12 @@ abstract class Viewport extends Component return _size; } + /// In most cases [virtualSize] is the same as [size], but in the cases when + /// the viewport is emulating a different size, this is the size of the + /// emulated viewport, for example the resolution for the + /// [FixedResolutionViewport]. + Vector2 get virtualSize => size; + @override set size(Vector2 value) { assert( @@ -72,7 +80,7 @@ abstract class Viewport extends Component ); _size.setFrom(value); _isInitialized = true; - if (parent != null) { + if (isLoaded) { camera.viewfinder.onViewportResize(); } onViewportResize(); @@ -82,7 +90,7 @@ abstract class Viewport extends Component } /// Reference to the parent camera. - CameraComponent get camera => parent! as CameraComponent; + CameraComponent get camera => parent; /// Apply clip mask to the [canvas]. /// @@ -113,15 +121,6 @@ abstract class Viewport extends Component @protected void onViewportResize(); - @mustCallSuper - @override - void onMount() { - assert( - parent! is CameraComponent, - 'A Viewport may only be attached to a CameraComponent', - ); - } - /// Converts a point from the global coordinate system to the local /// coordinate system of the viewport. /// @@ -147,4 +146,6 @@ abstract class Viewport extends Component final y = point.y + position.y - anchor.y * size.y; return (output?..setValues(x, y)) ?? Vector2(x, y); } + + void transformCanvas(Canvas canvas) {} } diff --git a/packages/flame/lib/src/camera/viewports/fixed_resolution_viewport.dart b/packages/flame/lib/src/camera/viewports/fixed_resolution_viewport.dart new file mode 100644 index 0000000000..d37d4ff94e --- /dev/null +++ b/packages/flame/lib/src/camera/viewports/fixed_resolution_viewport.dart @@ -0,0 +1,72 @@ +import 'dart:math'; + +import 'package:flame/camera.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/src/effects/provider_interfaces.dart'; +import 'package:flame/src/game/transform2d.dart'; +import 'package:meta/meta.dart'; + +/// [FixedAspectRatioViewport] is a rectangular viewport which auto-expands to +/// take as much space as possible within the canvas, while maintaining a fixed +/// aspect ratio. +/// +/// This viewport will automatically adjust its size and position when the +/// game canvas changes in size. At the same time, manually changing the size +/// of this viewport is not supported. +class FixedResolutionViewport extends FixedAspectRatioViewport + implements ReadOnlyScaleProvider { + FixedResolutionViewport({ + required this.resolution, + super.children, + }) : super(aspectRatio: resolution.x / resolution.y); + + /// The resolution that the viewport should adhere to. + final Vector2 resolution; + + @override + Vector2 get virtualSize => resolution; + + @internal + final Transform2D transform = Transform2D(); + + @override + Vector2 get scale => transform.scale; + + final Vector2 _scaleVector = Vector2.zero(); + + @override + bool containsLocalPoint(Vector2 point) { + final x = point.x; + final y = point.y; + return x >= 0 && y >= 0 && x <= virtualSize.x && y <= virtualSize.y; + } + + @override + void onViewportResize() { + super.onViewportResize(); + final scaleX = size.x / resolution.x; + final scaleY = size.y / resolution.y; + _scaleVector.setAll(min(scaleX, scaleY)); + transform.scale = _scaleVector; + camera.viewfinder.visibleRect = null; + } + + @override + Vector2 globalToLocal(Vector2 point, {Vector2? output}) { + final viewportPoint = super.globalToLocal(point, output: output); + return transform.globalToLocal(viewportPoint, output: output); + } + + @override + Vector2 localToGlobal(Vector2 point, {Vector2? output}) { + final viewportPoint = transform.localToGlobal(point, output: output); + return super.localToGlobal(viewportPoint, output: output); + } + + @override + void transformCanvas(Canvas canvas) { + canvas.translate(size.x / 2, size.y / 2); + canvas.transform2D(transform); + canvas.translate(-(size.x / 2) / scale.x, -(size.y / 2) / scale.y); + } +} diff --git a/packages/flame/lib/src/effects/provider_interfaces.dart b/packages/flame/lib/src/effects/provider_interfaces.dart index fd67547517..ea3a171a80 100644 --- a/packages/flame/lib/src/effects/provider_interfaces.dart +++ b/packages/flame/lib/src/effects/provider_interfaces.dart @@ -33,9 +33,14 @@ class PositionProviderImpl implements PositionProvider { set position(Vector2 value) => _setter!(value); } -/// Interface for a component that can be affected by scale effects. -abstract class ScaleProvider { +/// Interface for a class that has [scale] property which can be read but not +/// modified. +abstract class ReadOnlyScaleProvider { Vector2 get scale; +} + +/// Interface for a component that can be affected by scale effects. +abstract class ScaleProvider extends ReadOnlyScaleProvider { set scale(Vector2 value); } diff --git a/packages/flame/lib/src/extensions/canvas.dart b/packages/flame/lib/src/extensions/canvas.dart index 96867dde5c..b9e739169e 100644 --- a/packages/flame/lib/src/extensions/canvas.dart +++ b/packages/flame/lib/src/extensions/canvas.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:flame/palette.dart'; import 'package:flame/src/extensions/vector2.dart'; +import 'package:flame/src/game/transform2d.dart'; export 'dart:ui' show Canvas; @@ -55,4 +56,9 @@ extension CanvasExtension on Canvas { fn(this); restore(); } + + /// Use the [Transform2D] object to [transform] the canvas. + void transform2D(Transform2D transform2D) { + transform(transform2D.transformMatrix.storage); + } } diff --git a/packages/flame/lib/src/extensions/vector2.dart b/packages/flame/lib/src/extensions/vector2.dart index 6bd695e2b0..a7265e18c9 100644 --- a/packages/flame/lib/src/extensions/vector2.dart +++ b/packages/flame/lib/src/extensions/vector2.dart @@ -115,6 +115,9 @@ extension Vector2Extension on Vector2 { y = y.clamp(min.y, max.y); } + /// Sets both x and y to [value]. + void setAll(double value) => setValues(value, value); + /// Project this onto [other]. /// /// [other] needs to have a length > 0; diff --git a/packages/flame/test/_goldens/camera_component_fixed_resolution_order_test.png b/packages/flame/test/_goldens/camera_component_fixed_resolution_order_test.png new file mode 100644 index 0000000000..d2c889e10b Binary files /dev/null and b/packages/flame/test/_goldens/camera_component_fixed_resolution_order_test.png differ diff --git a/packages/flame/test/_goldens/camera_component_fixed_resolution_order_zoom_test.png b/packages/flame/test/_goldens/camera_component_fixed_resolution_order_zoom_test.png new file mode 100644 index 0000000000..3f4b6296f1 Binary files /dev/null and b/packages/flame/test/_goldens/camera_component_fixed_resolution_order_zoom_test.png differ diff --git a/packages/flame/test/_goldens/camera_component_order_test.png b/packages/flame/test/_goldens/camera_component_order_test.png index 39f68ce9e7..d3b265f721 100644 Binary files a/packages/flame/test/_goldens/camera_component_order_test.png and b/packages/flame/test/_goldens/camera_component_order_test.png differ diff --git a/packages/flame/test/_goldens/camera_component_prescale_test.png b/packages/flame/test/_goldens/camera_component_prescale_test.png new file mode 100644 index 0000000000..240ae84515 Binary files /dev/null and b/packages/flame/test/_goldens/camera_component_prescale_test.png differ diff --git a/packages/flame/test/_goldens/camera_component_prescale_zoom_test.png b/packages/flame/test/_goldens/camera_component_prescale_zoom_test.png new file mode 100644 index 0000000000..002878b624 Binary files /dev/null and b/packages/flame/test/_goldens/camera_component_prescale_zoom_test.png differ diff --git a/packages/flame/test/_goldens/circular_viewport_test2.png b/packages/flame/test/_goldens/circular_viewport_test2.png index 05c2905c85..2298750465 100644 Binary files a/packages/flame/test/_goldens/circular_viewport_test2.png and b/packages/flame/test/_goldens/circular_viewport_test2.png differ diff --git a/packages/flame/test/_goldens/circular_viewport_test3.png b/packages/flame/test/_goldens/circular_viewport_test3.png index 05c2905c85..2298750465 100644 Binary files a/packages/flame/test/_goldens/circular_viewport_test3.png and b/packages/flame/test/_goldens/circular_viewport_test3.png differ diff --git a/packages/flame/test/camera/camera_component_test.dart b/packages/flame/test/camera/camera_component_test.dart index ac14b48dbc..6fb31862f5 100644 --- a/packages/flame/test/camera/camera_component_test.dart +++ b/packages/flame/test/camera/camera_component_test.dart @@ -325,7 +325,7 @@ void main() { camera.viewfinder.add( CrossHair( size: Vector2.all(20), - position: camera.viewport.size / 2 + Vector2(-6, 0), + position: Vector2(-2, 4), color: Colors.white, ), ); @@ -345,6 +345,90 @@ void main() { ); }); + testGolden( + 'Correct scale of rendering', + (game) async { + final world = World(); + final resolution = Vector2(40, 60); + final camera = CameraComponent.withFixedResolution( + world: world, + width: resolution.x, + height: resolution.y, + ); + game.addAll([world, camera]); + camera.viewfinder.position = Vector2.all(4); + camera.backdrop.add( + CrossHair( + size: Vector2.all(28), + position: resolution / 2 + Vector2.all(4), + color: Colors.teal, + ), + ); + camera.viewfinder.add( + CrossHair( + size: Vector2.all(20), + position: Vector2(0, 2), + color: Colors.white, + ), + ); + world.add( + CrossHair(size: Vector2.all(14), color: Colors.green), + ); + camera.viewport.add( + CrossHair( + size: Vector2.all(8), + position: resolution / 2 + Vector2(2, -2), + color: Colors.red, + ), + ); + }, + goldenFile: '../_goldens/camera_component_fixed_resolution_order_test.png', + size: Vector2(50, 50), + ); + + testGolden( + 'Correct scale of rendering after zoom', + (game) async { + final world = World(); + final resolution = Vector2(40, 60); + final camera = CameraComponent.withFixedResolution( + world: world, + width: resolution.x, + height: resolution.y, + ); + game.addAll([world, camera]); + camera.viewfinder.position = Vector2.all(4); + camera.viewfinder.zoom = 1.5; + camera.backdrop.add( + CrossHair( + size: Vector2.all(28), + position: resolution / 2 + Vector2.all(4), + color: Colors.teal, + ), + ); + camera.viewfinder.add( + CrossHair( + size: Vector2.all(20), + position: Vector2(-2, 4), + color: Colors.white, + ), + ); + world.add( + CrossHair(size: Vector2.all(14), color: Colors.green), + ); + camera.viewport.add( + CrossHair( + size: Vector2.all(8), + position: resolution / 2 + Vector2(4, -4), + color: Colors.red, + ), + ); + }, + goldenFile: + '../_goldens/camera_component_fixed_resolution_order_zoom_test.png', + size: Vector2(50, 50), + ); + group('CameraComponent.canSee', () { testWithFlameGame('null world', (game) async { final player = PositionComponent();