LCOV - code coverage report
Current view: top level - lib/src/game - camera.dart (source / functions) Hit Total Coverage
Test: lcov.info Lines: 97 107 90.7 %
Date: 2021-08-10 15:50:53 Functions: 0 0 -

          Line data    Source code
       1             : import 'dart:math' as math;
       2             : import 'dart:ui' show Rect, Canvas;
       3             : 
       4             : import '../../components.dart';
       5             : import '../../extensions.dart';
       6             : import '../../game.dart';
       7             : import 'projector.dart';
       8             : 
       9             : /// A camera translates your game coordinate system; this is useful when your
      10             : /// world is not 1:1 with your screen size.
      11             : ///
      12             : /// A camera always has a current [position], however you cannot set it
      13             : /// directly. You must use some methods to ensure that the camera moves smoothly
      14             : /// as the game runs. Smoothly here means that sudden snaps should be avoided,
      15             : /// as they feel jarring to the player.
      16             : ///
      17             : /// There are three major factors that determine the camera position:
      18             : ///
      19             : /// * Follow
      20             : /// If you want, you can call [followComponent] at the beginning of your
      21             : /// stage/world/level, and provided a [PositionComponent].
      22             : /// The camera will follow this component making sure its position is fixed
      23             : /// on the screen.
      24             : /// You can set the relative position of the screen you want the follow
      25             : /// object to stay in (normally the center), and you can even change that
      26             : /// and get a smooth transition.
      27             : ///
      28             : /// * Move
      29             : /// You can alternatively move the camera to a specific world coordinate.
      30             : /// This will set the top left of the camera and will ignore any existing follow
      31             : /// rules and move the camera smoothly until it reaches the desired destination.
      32             : ///
      33             : /// * Shake
      34             : /// Regardless of the the previous rules, you can additionally add a shake
      35             : /// effect for a brief period of time on top of the current coordinate.
      36             : /// The shake adds a random immediate delta to each tick to simulate the shake
      37             : /// effect.
      38             : ///
      39             : /// Note: in the context of the BaseGame, the camera effectively translates
      40             : /// the position where components are rendered with relation to the Viewport.
      41             : /// Components marked as `isHud = true` are always rendered in screen
      42             : /// coordinates, bypassing the camera altogether.
      43             : class Camera extends Projector {
      44             :   static const defaultSpeed = 50.0; // in pixels/s
      45             : 
      46             :   /// This must be set by the Game as soon as the Camera is created.
      47             :   ///
      48             :   /// Do not change this reference.
      49             :   late BaseGame gameRef;
      50             : 
      51             :   /// If set, this bypasses follow and moves the camera to a specific point
      52             :   /// in the world.
      53             :   ///
      54             :   /// You can use this if you are not using follow but have a few different
      55             :   /// camera positions or if you are using follow but you want to highlight a
      56             :   /// spot in the world during an animation.
      57             :   Vector2? _currentCameraDelta;
      58             :   Vector2? _targetCameraDelta;
      59             : 
      60             :   /// Remaining time in seconds for the camera shake.
      61             :   double _shakeTimer = 0.0;
      62             : 
      63             :   /// The intensity of the current shake action.
      64             :   double _shakeIntensity = 0.0;
      65             : 
      66             :   /// The matrix used for scaling and translating the canvas
      67             :   final Matrix4 _transform = Matrix4.identity();
      68             : 
      69             :   // Configurable parameters
      70             : 
      71             :   double speed = defaultSpeed;
      72             :   double defaultShakeIntensity = 75.0; // in pixels
      73             :   double defaultShakeDuration = 0.3; // in seconds
      74             : 
      75             :   /// This is the current position of the camera, ie the world coordinate that
      76             :   /// is rendered on the top left of the screen (origin of the screen space).
      77             :   ///
      78             :   /// Zero means no translation is applied.
      79             :   /// You can't change this directly; the camera will handle all ongoing
      80             :   /// movements so they smoothly transition.
      81             :   /// If you want to immediately snap the camera to a new place, you can do:
      82             :   /// ```
      83             :   ///   camera.snapTo(newPosition);
      84             :   /// ```
      85           9 :   Vector2 get position => _internalPosition.clone();
      86             : 
      87             :   /// Do not change this directly since it bypasses [onPositionUpdate]
      88             :   final Vector2 _internalPosition = Vector2.zero();
      89             : 
      90          12 :   Vector2 get _position => _internalPosition;
      91          22 :   set _position(Vector2 position) {
      92          44 :     _internalPosition.setFrom(position);
      93          44 :     onPositionUpdate(_internalPosition);
      94             :   }
      95             : 
      96             :   /// If set, the camera will "follow" this vector, making sure that this
      97             :   /// vector is always rendered in a fixed position in the screen, by
      98             :   /// immediately moving the camera to "focus" on the where the vector is.
      99             :   ///
     100             :   /// You might want to set it to the player component by using the
     101             :   /// [followComponent] method.
     102             :   /// Note that this is not smooth because the movement of the followed vector
     103             :   /// is assumed to be smooth.
     104             :   Vector2? follow;
     105             : 
     106             :   /// Where in the screen the follow object should be.
     107             :   ///
     108             :   /// This is a fractional value relating to the screen size.
     109             :   /// Changing this will smoothly move the camera to the new position
     110             :   /// (unless you use the followObject method that immediately sets
     111             :   /// up the camera for the new parameters).
     112           0 :   Vector2 get relativeOffset => _currentRelativeOffset;
     113             : 
     114             :   final Vector2 _currentRelativeOffset = Vector2.zero();
     115             :   final Vector2 _targetRelativeOffset = Vector2.zero();
     116             : 
     117             :   /// If set, this determines boundaries for the camera movement.
     118             :   ///
     119             :   /// The camera will never move such that a region outside the world boundaries
     120             :   /// is shown, meaning it will stop following when the object gets close to the
     121             :   /// edges.
     122             :   ///
     123             :   /// Changing this value can immediately snap the camera if it is a wrong
     124             :   /// position, but other than that it's just prevent movement so should not
     125             :   /// add any non-smooth movement.
     126             :   Rect? worldBounds;
     127             : 
     128             :   /// If set, the camera will zoom by this ratio. This can be greater than 1
     129             :   /// (zoom in) or smaller (zoom out), but should always be greater than zero.
     130             :   ///
     131             :   /// Note: do not confuse this with the zoom applied by the viewport. The
     132             :   /// viewport applies a (normally) fixed zoom to adapt multiple screens into
     133             :   /// one aspect ratio. The zoom might be different per dimension depending
     134             :   /// on the Viewport implementation. Also, if used with the default
     135             :   /// BaseGame implementation, it will apply to all components.
     136             :   /// The zoom from the camera is only for components that respect camera,
     137             :   /// and is applied after the viewport is set. It exists to be used if there
     138             :   /// is any kind of user configurable camera on your game.
     139             :   double zoom = 1.0;
     140             : 
     141          24 :   Camera();
     142             : 
     143             :   /// Use this method to transform the canvas using the current rules provided
     144             :   /// by this camera object.
     145             :   ///
     146             :   /// If you are using BaseGame, this will be done for you for all non-HUD
     147             :   /// components.
     148             :   /// When using this method you are responsible for saving/restoring canvas
     149             :   /// state to avoid leakage.
     150           3 :   void apply(Canvas canvas) {
     151          15 :     canvas.transform(_transformMatrix(position, zoom).storage);
     152             :   }
     153             : 
     154           3 :   Matrix4 _transformMatrix(Vector2 position, double zoom) {
     155          12 :     final translateX = -_position.x * zoom;
     156          12 :     final translateY = -_position.y * zoom;
     157           9 :     if (_transform.m11 == zoom &&
     158           9 :         _transform.m22 == zoom &&
     159           9 :         _transform.m33 == zoom &&
     160           9 :         _transform.m41 == translateX &&
     161           9 :         _transform.m42 == translateY) {
     162           3 :       return _transform;
     163             :     }
     164           2 :     _transform.setIdentity();
     165           2 :     _transform.translate(translateX, translateY);
     166           2 :     _transform.scale(zoom);
     167           1 :     return _transform;
     168             :   }
     169             : 
     170             :   /// This smoothly updates the camera for an amount of time [dt].
     171             :   ///
     172             :   /// This should be called by the Game class during the update cycle.
     173          22 :   void update(double dt) {
     174          44 :     final ds = speed * dt;
     175          22 :     final shake = _shakeDelta();
     176             : 
     177          66 :     _currentRelativeOffset.moveToTarget(_targetRelativeOffset, ds);
     178          26 :     if (_targetCameraDelta != null && _currentCameraDelta != null) {
     179          12 :       _currentCameraDelta?.moveToTarget(_targetCameraDelta!, ds);
     180             :     }
     181          66 :     _position = _target()..add(shake);
     182             : 
     183          22 :     if (shaking) {
     184           2 :       _shakeTimer -= dt;
     185           2 :       if (_shakeTimer < 0.0) {
     186           1 :         _shakeTimer = 0.0;
     187             :       }
     188             :     }
     189             :   }
     190             : 
     191             :   /// Use this to immediately "snap" the camera to where it should be right
     192             :   /// now. This bypasses any currently smooth transitions and might be janky,
     193             :   /// but can be used to setup after a new world transition for example.
     194           4 :   void snap() {
     195           8 :     if (_targetCameraDelta != null && _currentCameraDelta != null) {
     196          12 :       _currentCameraDelta!.setFrom(_targetCameraDelta!);
     197             :     }
     198          12 :     _currentRelativeOffset.setFrom(_targetRelativeOffset);
     199           4 :     update(0);
     200             :   }
     201             : 
     202             :   // Coordinates
     203             : 
     204           5 :   @override
     205             :   Vector2 unprojectVector(Vector2 screenCoordinates) {
     206          20 :     return _position + (screenCoordinates / zoom);
     207             :   }
     208             : 
     209           1 :   @override
     210             :   Vector2 projectVector(Vector2 worldCoordinates) {
     211           4 :     return (worldCoordinates - _position) * zoom;
     212             :   }
     213             : 
     214           1 :   @override
     215             :   Vector2 unscaleVector(Vector2 screenCoordinates) {
     216           2 :     return screenCoordinates / zoom;
     217             :   }
     218             : 
     219           1 :   @override
     220             :   Vector2 scaleVector(Vector2 worldCoordinates) {
     221           2 :     return worldCoordinates * zoom;
     222             :   }
     223             : 
     224             :   /// Takes coordinates in the screen space and returns their counter-part in
     225             :   /// the world space.
     226           0 :   Vector2 screenToWorld(Vector2 screenCoordinates) {
     227           0 :     return unprojectVector(screenCoordinates);
     228             :   }
     229             : 
     230             :   /// Takes coordinates in the world space and returns their counter-part in
     231             :   /// the screen space.
     232           0 :   Vector2 worldToScreen(Vector2 worldCoordinates) {
     233           0 :     return projectVector(worldCoordinates);
     234             :   }
     235             : 
     236             :   /// This is the (current) absolute target of the camera, i.e., the
     237             :   /// coordinate that should with `relativeOffset` taken into consideration but
     238             :   /// regardless of world boundaries or shake.
     239          22 :   Vector2 absoluteTarget() {
     240          66 :     return _currentCameraDelta ?? follow ?? Vector2.zero();
     241             :   }
     242             : 
     243             :   // Follow
     244             : 
     245             :   /// Immediately snaps the camera to start following the [component].
     246             :   ///
     247             :   /// This means that the camera will move so that the position vector of the
     248             :   /// component is in a fixed position on the screen.
     249             :   /// That position is determined by a fraction of screen size defined by
     250             :   /// [relativeOffset] (default to the center).
     251             :   /// [worldBounds] can be optionally set to add boundaries to how far the
     252             :   /// camera is allowed to move.
     253             :   /// The component is "grabbed" by its anchor (default top left).
     254             :   /// So for example if you want the center of the object to be at the fixed
     255             :   /// position, set the components anchor to center.
     256           1 :   void followComponent(
     257             :     PositionComponent component, {
     258             :     Anchor relativeOffset = Anchor.center,
     259             :     Rect? worldBounds,
     260             :   }) {
     261           1 :     followVector2(
     262           1 :       component.position,
     263             :       relativeOffset: relativeOffset,
     264             :       worldBounds: worldBounds,
     265             :     );
     266             :   }
     267             : 
     268             :   /// Immediately snaps the camera to start following [vector2].
     269             :   ///
     270             :   /// This means that the camera will move so that the position vector is in a
     271             :   /// fixed position on the screen.
     272             :   /// That position is determined by a fraction of screen size defined by
     273             :   /// [relativeOffset] (default to the center).
     274             :   /// [worldBounds] can be optionally set to add boundaries to how far the
     275             :   /// camera is allowed to move.
     276           1 :   void followVector2(
     277             :     Vector2 vector2, {
     278             :     Anchor relativeOffset = Anchor.center,
     279             :     Rect? worldBounds,
     280             :   }) {
     281           1 :     follow = vector2;
     282           1 :     this.worldBounds = worldBounds;
     283           3 :     _targetRelativeOffset.setFrom(relativeOffset.toVector2());
     284           3 :     _currentRelativeOffset.setFrom(_targetRelativeOffset);
     285             :   }
     286             : 
     287             :   /// This will trigger a smooth transition to a new relative offset.
     288             :   ///
     289             :   /// You can use this for example to change camera modes in your game, maybe
     290             :   /// you have two different options for the player to choose or your have a
     291             :   /// "dialog" camera that puts the player in a better place to show the
     292             :   /// dialog UI.
     293           2 :   void setRelativeOffset(Anchor newRelativeOffset) {
     294           6 :     _targetRelativeOffset.setFrom(newRelativeOffset.toVector2());
     295             :   }
     296             : 
     297          22 :   Vector2 _screenDelta() {
     298         110 :     return gameRef.size.clone()..multiply(_currentRelativeOffset);
     299             :   }
     300             : 
     301          22 :   Vector2 _target() {
     302          22 :     final target = absoluteTarget();
     303          44 :     final attemptedTarget = target - _screenDelta();
     304             : 
     305          22 :     final bounds = worldBounds;
     306             :     if (bounds != null) {
     307           7 :       if (bounds.width > gameRef.size.x * zoom) {
     308           1 :         final cameraLeftEdge = attemptedTarget.x;
     309           5 :         final cameraRightEdge = attemptedTarget.x + gameRef.size.x;
     310           2 :         if (cameraLeftEdge < bounds.left) {
     311           2 :           attemptedTarget.x = bounds.left;
     312           2 :         } else if (cameraRightEdge > bounds.right) {
     313           6 :           attemptedTarget.x = bounds.right - gameRef.size.x;
     314             :         }
     315             :       } else {
     316           7 :         attemptedTarget.x = (gameRef.size.x - bounds.width) / 2;
     317             :       }
     318             : 
     319           7 :       if (bounds.height > gameRef.size.y * zoom) {
     320           1 :         final cameraTopEdge = attemptedTarget.y;
     321           5 :         final cameraBottomEdge = attemptedTarget.y + gameRef.size.y;
     322           2 :         if (cameraTopEdge < bounds.top) {
     323           2 :           attemptedTarget.y = bounds.top;
     324           2 :         } else if (cameraBottomEdge > bounds.bottom) {
     325           6 :           attemptedTarget.y = bounds.bottom - gameRef.size.y;
     326             :         }
     327             :       } else {
     328           7 :         attemptedTarget.y = (gameRef.size.y - bounds.height) / 2;
     329             :       }
     330             :     }
     331             : 
     332             :     return attemptedTarget;
     333             :   }
     334             : 
     335             :   // Movement
     336             : 
     337             :   /// Moves the camera by a given [displacement] (delta). This is the same as
     338             :   /// [moveTo] but instead of providing an absolute end position, you can
     339             :   /// provide a desired translation vector.
     340           0 :   void translateBy(Vector2 displacement) {
     341           0 :     moveTo(absoluteTarget() + displacement);
     342             :   }
     343             : 
     344             :   /// Applies an ad-hoc movement to the camera towards the target, bypassing
     345             :   /// follow. Once it arrives the camera will not move until [resetMovement]
     346             :   /// is called.
     347             :   ///
     348             :   /// The camera will be smoothly transitioned to this position.
     349             :   /// This will replace any previous targets.
     350           4 :   void moveTo(Vector2 position) {
     351          16 :     _currentCameraDelta = _position + _screenDelta();
     352           8 :     _targetCameraDelta = position.clone();
     353             :   }
     354             : 
     355             :   /// Instantly moves the camera to the target, bypassing follow.
     356             :   /// This will replace any previous targets.
     357           3 :   void snapTo(Vector2 position) {
     358           3 :     moveTo(position);
     359           3 :     snap();
     360             :   }
     361             : 
     362             :   /// Smoothly resets any moveTo targets.
     363           0 :   void resetMovement() {
     364           0 :     _currentCameraDelta = null;
     365           0 :     _targetCameraDelta = null;
     366             :   }
     367             : 
     368             :   // Shake
     369             : 
     370             :   /// Applies a shaking effect to the camera for [duration] seconds and with
     371             :   /// [intensity] expressed in pixels.
     372           1 :   void shake({double? duration, double? intensity}) {
     373           2 :     _shakeTimer += duration ?? defaultShakeDuration;
     374           2 :     _shakeIntensity = intensity ?? defaultShakeIntensity;
     375             :   }
     376             : 
     377             :   /// Whether the camera is currently shaking or not.
     378          66 :   bool get shaking => _shakeTimer > 0.0;
     379             : 
     380             :   /// Buffer to re-use for the shake delta.
     381             :   final _shakeBuffer = Vector2.zero();
     382             : 
     383             :   /// The random number generator to use for shaking
     384             :   final _shakeRng = math.Random();
     385             : 
     386             :   /// Generates one value between [-1, 1] * [_shakeIntensity] used once for each
     387             :   /// of the axis in the shake delta.
     388           7 :   double _shakeValue() => (_shakeRng.nextDouble() - 0.5) * 2 * _shakeIntensity;
     389             : 
     390             :   /// Generates a random [Vector2] of displacement applied to the camera.
     391             :   /// This will be a random [Vector2] every tick causing a shakiness effect.
     392          22 :   Vector2 _shakeDelta() {
     393          22 :     if (shaking) {
     394           4 :       _shakeBuffer.setValues(_shakeValue(), _shakeValue());
     395          44 :     } else if (!_shakeBuffer.isZero()) {
     396           2 :       _shakeBuffer.setZero();
     397             :     }
     398          22 :     return _shakeBuffer;
     399             :   }
     400             : 
     401             :   /// If you need updated on when the position of the camera is updated you
     402             :   /// can override this.
     403          22 :   void onPositionUpdate(Vector2 position) {}
     404             : }

Generated by: LCOV version 1.15