diff --git a/examples/lib/stories/camera_and_viewport/follow_component_example.dart b/examples/lib/stories/camera_and_viewport/follow_component_example.dart index f5d9c061e..837eb048f 100644 --- a/examples/lib/stories/camera_and_viewport/follow_component_example.dart +++ b/examples/lib/stories/camera_and_viewport/follow_component_example.dart @@ -188,7 +188,7 @@ class Rock extends SpriteComponent add( ScaleEffect.by( Vector2.all(10), - StandardEffectController(duration: 0.3), + EffectController(duration: 0.3), ), ); return true; diff --git a/examples/lib/stories/collision_detection/simple_shapes_example.dart b/examples/lib/stories/collision_detection/simple_shapes_example.dart index 659f32f6c..fee4f484e 100644 --- a/examples/lib/stories/collision_detection/simple_shapes_example.dart +++ b/examples/lib/stories/collision_detection/simple_shapes_example.dart @@ -65,7 +65,7 @@ class SimpleShapesExample extends FlameGame with HasTappables { component.add( MoveEffect.to( size / 2, - StandardEffectController( + EffectController( duration: 5, reverseDuration: 5, infinite: true, @@ -75,7 +75,7 @@ class SimpleShapesExample extends FlameGame with HasTappables { component.add( RotateEffect.to( 3, - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, infinite: true, diff --git a/examples/lib/stories/effects/move_effect_example.dart b/examples/lib/stories/effects/move_effect_example.dart index 579cc3d39..a0d5f9dc9 100644 --- a/examples/lib/stories/effects/move_effect_example.dart +++ b/examples/lib/stories/effects/move_effect_example.dart @@ -40,7 +40,7 @@ class MoveEffectExample extends FlameGame { )..add( MoveEffect.to( Vector2(380, 50), - StandardEffectController( + EffectController( duration: 3, reverseDuration: 3, infinite: true, @@ -58,7 +58,7 @@ class MoveEffectExample extends FlameGame { ..add( MoveEffect.to( Vector2(380, 150), - StandardEffectController( + EffectController( duration: 3, reverseDuration: 3, infinite: true, @@ -68,7 +68,7 @@ class MoveEffectExample extends FlameGame { ..add( MoveEffect.by( Vector2(0, -50), - StandardEffectController( + EffectController( duration: 0.25, reverseDuration: 0.25, startDelay: 1, @@ -92,7 +92,7 @@ class MoveEffectExample extends FlameGame { ..add( MoveEffect.along( path1, - StandardEffectController( + EffectController( duration: 10, startDelay: i * 0.2, infinite: true, @@ -110,7 +110,7 @@ class MoveEffectExample extends FlameGame { ..add( MoveEffect.along( path2, - StandardEffectController( + EffectController( duration: 6, startDelay: i * 0.3, infinite: true, diff --git a/examples/lib/stories/effects/opacity_effect_example.dart b/examples/lib/stories/effects/opacity_effect_example.dart index c402e7b3e..b07d1a894 100644 --- a/examples/lib/stories/effects/opacity_effect_example.dart +++ b/examples/lib/stories/effects/opacity_effect_example.dart @@ -2,7 +2,6 @@ import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; - import '../../commons/ember.dart'; class OpacityEffectExample extends FlameGame with TapDetector { @@ -32,7 +31,7 @@ class OpacityEffectExample extends FlameGame with TapDetector { size: Vector2.all(100), )..add( OpacityEffect.fadeOut( - StandardEffectController( + EffectController( duration: 1.5, reverseDuration: 1.5, infinite: true, @@ -46,9 +45,9 @@ class OpacityEffectExample extends FlameGame with TapDetector { void onTap() { final opacity = sprite.paint.color.opacity; if (opacity >= 0.5) { - sprite.add(OpacityEffect.fadeOut(StandardEffectController(duration: 1))); + sprite.add(OpacityEffect.fadeOut(EffectController(duration: 1))); } else { - sprite.add(OpacityEffect.fadeIn(StandardEffectController(duration: 1))); + sprite.add(OpacityEffect.fadeIn(EffectController(duration: 1))); } } } diff --git a/examples/lib/stories/effects/rotate_effect_example.dart b/examples/lib/stories/effects/rotate_effect_example.dart index 010ecfe10..e3ece6e8f 100644 --- a/examples/lib/stories/effects/rotate_effect_example.dart +++ b/examples/lib/stories/effects/rotate_effect_example.dart @@ -25,7 +25,7 @@ class RotateEffectExample extends FlameGame { compass.rim.add( RotateEffect.by( 1.0, - StandardEffectController( + EffectController( duration: 6, reverseDuration: 3, curve: Curves.ease, @@ -37,7 +37,7 @@ class RotateEffectExample extends FlameGame { ..add( RotateEffect.to( Transform2D.tau, - StandardEffectController( + EffectController( duration: 20, infinite: true, ), @@ -46,7 +46,7 @@ class RotateEffectExample extends FlameGame { ..add( RotateEffect.by( Transform2D.tau * 0.015, - StandardEffectController( + EffectController( duration: 0.1, reverseDuration: 0.1, infinite: true, @@ -56,7 +56,7 @@ class RotateEffectExample extends FlameGame { ..add( RotateEffect.by( Transform2D.tau * 0.021, - StandardEffectController( + EffectController( duration: 0.13, reverseDuration: 0.13, infinite: true, diff --git a/examples/lib/stories/effects/scale_effect_example.dart b/examples/lib/stories/effects/scale_effect_example.dart index 2ee1fa572..cc0c679c3 100644 --- a/examples/lib/stories/effects/scale_effect_example.dart +++ b/examples/lib/stories/effects/scale_effect_example.dart @@ -42,7 +42,7 @@ class ScaleEffectExample extends FlameGame with TapDetector { square.add( ScaleEffect.to( Vector2.all(s), - StandardEffectController( + EffectController( duration: 1.5, curve: Curves.bounceInOut, ), diff --git a/examples/lib/stories/effects/size_effect_example.dart b/examples/lib/stories/effects/size_effect_example.dart index d8894d517..7029718fd 100644 --- a/examples/lib/stories/effects/size_effect_example.dart +++ b/examples/lib/stories/effects/size_effect_example.dart @@ -1,10 +1,12 @@ +import 'dart:ui'; + import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame/palette.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/animation.dart'; class SizeEffectExample extends FlameGame with TapDetector { static const String description = ''' @@ -42,10 +44,7 @@ class SizeEffectExample extends FlameGame with TapDetector { square.add( SizeEffect.to( Vector2.all(s), - StandardEffectController( - duration: 1.5, - curve: Curves.bounceInOut, - ), + EffectController(duration: 1.5, curve: Curves.bounceInOut), ), ); } diff --git a/examples/lib/stories/input/joystick_advanced_example.dart b/examples/lib/stories/input/joystick_advanced_example.dart index fb8dc4ce9..03201c6f0 100644 --- a/examples/lib/stories/input/joystick_advanced_example.dart +++ b/examples/lib/stories/input/joystick_advanced_example.dart @@ -105,7 +105,7 @@ class JoystickAdvancedExample extends FlameGame onPressed: () => player.add( RotateEffect.by( 8 * rng.nextDouble(), - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, curve: Curves.bounceOut, @@ -131,7 +131,7 @@ class JoystickAdvancedExample extends FlameGame onPressed: () => player.add( ScaleEffect.by( Vector2.all(1.5), - StandardEffectController(duration: 1.0, reverseDuration: 1.0), + EffectController(duration: 1.0, reverseDuration: 1.0), ), ), ); @@ -152,7 +152,7 @@ class JoystickAdvancedExample extends FlameGame size: Vector2(185, 50), onPressed: () => player.add( OpacityEffect.fadeOut( - StandardEffectController(duration: 0.5, reverseDuration: 0.5), + EffectController(duration: 0.5, reverseDuration: 0.5), ), ), ); diff --git a/packages/flame/CHANGELOG.md b/packages/flame/CHANGELOG.md index 98701ea2d..396814cba 100644 --- a/packages/flame/CHANGELOG.md +++ b/packages/flame/CHANGELOG.md @@ -3,10 +3,12 @@ ## [Next] - Add `ButtonComponent` backed by two `PositionComponent`s - Add `SpriteButtonComponent` backed by two `Sprite`s + - Allow more flexible construction of `EffectController`s and make them able to run back in time - Remove old effects system - Export new effects system - Introduce `updateTree` to follow the `renderTree` convention - Fix `Parallax.load` with different loading times + - Fix render order of components and add tests - `isHud` renamed to `respectCamera` ## [1.0.0-releasecandidate.18] @@ -15,7 +17,6 @@ - Fixed position calculation in `HudMarginComponent` when using a viewport - Add noClip option to `FixedResolutionViewport` - Add a few missing helpers to SpriteAnimation - - Fix render order of components and add tests ## [1.0.0-releasecandidate.17] - Added `StandardEffectController` class diff --git a/packages/flame/lib/effects.dart b/packages/flame/lib/effects.dart index 77d7c23a2..e6b1305c1 100644 --- a/packages/flame/lib/effects.dart +++ b/packages/flame/lib/effects.dart @@ -1,13 +1,21 @@ export 'src/effects/component_effect.dart'; +export 'src/effects/controllers/curved_effect_controller.dart'; +export 'src/effects/controllers/delayed_effect_controller.dart'; +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/pause_effect_controller.dart'; +export 'src/effects/controllers/repeated_effect_controller.dart'; +export 'src/effects/controllers/reverse_curved_effect_controller.dart'; +export 'src/effects/controllers/reverse_linear_effect_controller.dart'; +export 'src/effects/controllers/sequence_effect_controller.dart'; export 'src/effects/effect.dart'; -export 'src/effects/effect_controller.dart'; export 'src/effects/move_effect.dart'; export 'src/effects/opacity_effect.dart'; export 'src/effects/remove_effect.dart'; export 'src/effects/rotate_effect.dart'; export 'src/effects/scale_effect.dart'; -export 'src/effects/simple_effect_controller.dart'; export 'src/effects/size_effect.dart'; -export 'src/effects/standard_effect_controller.dart'; export 'src/effects/transform2d_effect.dart'; export 'src/extensions/vector2.dart'; diff --git a/packages/flame/lib/src/effects/component_effect.dart b/packages/flame/lib/src/effects/component_effect.dart index 5d8e10948..e19a5bc35 100644 --- a/packages/flame/lib/src/effects/component_effect.dart +++ b/packages/flame/lib/src/effects/component_effect.dart @@ -1,8 +1,8 @@ import 'package:flutter/cupertino.dart'; import '../../components.dart'; +import 'controllers/effect_controller.dart'; import 'effect.dart'; -import 'effect_controller.dart'; /// Base class for effects that target a [Component] of type [T]. /// diff --git a/packages/flame/lib/src/effects/controllers/curved_effect_controller.dart b/packages/flame/lib/src/effects/controllers/curved_effect_controller.dart new file mode 100644 index 000000000..1a324f9ed --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/curved_effect_controller.dart @@ -0,0 +1,18 @@ +import 'package:flutter/animation.dart'; + +import 'duration_effect_controller.dart'; + +/// A controller that grows non-linearly from 0 to 1 following the provided +/// [curve]. The [duration] cannot be 0. +class CurvedEffectController extends DurationEffectController { + CurvedEffectController(double duration, Curve curve) + : assert(duration > 0, 'Duration must be positive: $duration'), + _curve = curve, + super(duration); + + Curve get curve => _curve; + final Curve _curve; + + @override + double get progress => _curve.transform(timer / duration); +} diff --git a/packages/flame/lib/src/effects/controllers/delayed_effect_controller.dart b/packages/flame/lib/src/effects/controllers/delayed_effect_controller.dart new file mode 100644 index 000000000..3d6563376 --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/delayed_effect_controller.dart @@ -0,0 +1,77 @@ +import 'effect_controller.dart'; + +/// An effect controller that waits for [delay] seconds before running the +/// child controller. While waiting, the progress will be reported at 0. +class DelayedEffectController extends EffectController { + DelayedEffectController(EffectController child, {required this.delay}) + : assert(delay >= 0, 'Delay must be non-negative: $delay'), + _child = child, + _timer = 0, + super.empty(); + + final EffectController _child; + final double delay; + double _timer; + + @override + bool get isInfinite => _child.isInfinite; + + @override + bool get isRandom => _child.isRandom; + + @override + bool get started => _timer == delay; + + @override + bool get completed => started && _child.completed; + + @override + double get progress => started ? _child.progress : 0; + + @override + double? get duration { + final d = _child.duration; + return d == null ? null : d + delay; + } + + @override + double advance(double dt) { + if (_timer == delay) { + return _child.advance(dt); + } + _timer += dt; + if (_timer > delay) { + final t = _child.advance(_timer - delay); + _timer = delay; + return t; + } else { + return 0; + } + } + + @override + double recede(double dt) { + if (_timer == delay) { + _timer -= _child.recede(dt); + } else { + _timer -= dt; + } + if (_timer < 0) { + final leftoverTime = -_timer; + _timer = 0; + return leftoverTime; + } + return 0; + } + + @override + void setToStart() { + _timer = 0; + } + + @override + void setToEnd() { + _timer = delay; + _child.setToEnd(); + } +} diff --git a/packages/flame/lib/src/effects/controllers/duration_effect_controller.dart b/packages/flame/lib/src/effects/controllers/duration_effect_controller.dart new file mode 100644 index 000000000..1d198343e --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/duration_effect_controller.dart @@ -0,0 +1,66 @@ +import 'package:meta/meta.dart'; + +import 'effect_controller.dart'; + +/// Abstract class for an effect controller that has a predefined [duration]. +/// +/// This effect controller cannot be used directly, instead it serves as base +/// for some other effect controller classes. +/// +/// The primary functionality offered by this class is the [timer] property, +/// which keeps track of how much time has passed within this controller. The +/// effect controller will be considered [completed] when the timer reaches the +/// [duration] value. +abstract class DurationEffectController extends EffectController { + DurationEffectController(double duration) + : assert(duration >= 0, 'Duration cannot be negative: $duration'), + _duration = duration, + _timer = 0, + super.empty(); + + double _duration; + double _timer; + + @override + double get duration => _duration; + + set duration(double value) => _duration = value; + + @protected + double get timer => _timer; + + @override + bool get completed => _timer == _duration; + + @override + double advance(double dt) { + _timer += dt; + if (_timer > _duration) { + final leftoverTime = _timer - _duration; + _timer = _duration; + return leftoverTime; + } + return 0; + } + + @override + double recede(double dt) { + _timer -= dt; + if (_timer < 0) { + final leftoverTime = 0 - _timer; + _timer = 0; + return leftoverTime; + } + return 0; + } + + @override + void setToStart() { + _timer = 0; + } + + @override + void setToEnd() { + _timer = duration; + } +} diff --git a/packages/flame/lib/src/effects/controllers/effect_controller.dart b/packages/flame/lib/src/effects/controllers/effect_controller.dart new file mode 100644 index 000000000..95611c35f --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/effect_controller.dart @@ -0,0 +1,171 @@ +import 'package:flutter/animation.dart'; + +import 'curved_effect_controller.dart'; +import 'delayed_effect_controller.dart'; +import 'infinite_effect_controller.dart'; +import 'linear_effect_controller.dart'; +import 'pause_effect_controller.dart'; +import 'repeated_effect_controller.dart'; +import 'reverse_curved_effect_controller.dart'; +import 'reverse_linear_effect_controller.dart'; +import 'sequence_effect_controller.dart'; + +/// Base "controller" class to facilitate animation of effects. +/// +/// The purpose of an effect controller is to define how an effect or an +/// animation should progress over time. To facilitate that, this class provides +/// variable [progress], which will grow from 0.0 to 1.0. The value of 0 +/// corresponds to the beginning of an animation, and the value of 1.0 is +/// the end of the animation. +/// +/// The [progress] variable can best be thought of as a "logical time". For +/// example, if you want to animate a certain property from value A to value B, +/// then you can use [progress] to linearly interpolate between these two +/// extremes and obtain variable `x = A*(1 - progress) + B*progress`. +/// +/// The exact behavior of [progress] is determined by subclasses, but the +/// following general considerations apply: +/// - the progress can also go in negative direction (i.e. from 1 to 0); +/// - the progress may oscillate, going from 0 to 1, then back to 0, etc; +/// - the progress may change over a finite or infinite period of time; +/// - the value of 0 corresponds to the logical start of an animation; +/// - the value of 1 is either the end or the "peak" of an animation; +/// - the progress may briefly attain values outside of [0; 1] range (for +/// example if a "bouncy" easing curve is applied). +/// +/// An [EffectController] can be made to run forward in time (`advance()`), or +/// backward in time (`recede()`). +/// +/// Unlike the `dart.ui.AnimationController`, this class does not use a `Ticker` +/// to keep track of time. Instead, it must be pushed through time manually, by +/// calling the `update()` method within the game loop. +abstract class EffectController { + /// Factory function for producing common [EffectController]s. + /// + /// In the simplest case, when only `duration` is provided, this will return + /// a [LinearEffectController] that grows linearly from 0 to 1 over the period + /// of that duration. + /// + /// More generally, the produced effect controller allows to add a delay + /// before the beginning of the animation, to animate both forward and in + /// reverse, to iterate several times (or infinitely), to apply an arbitrary + /// [curve] making the effect progression non-linear, etc. + /// + /// In the most general case, the animation proceeds through the following + /// steps: + /// 1. wait for [startDelay] seconds, + /// 2. repeat the following steps [repeatCount] times (or [infinite]ly): + /// a. progress from 0 to 1 over the [duration] seconds, + /// b. wait for [atMaxDuration] seconds, + /// c. progress from 1 to 0 over the [reverseDuration] seconds, + /// d. wait for [atMinDuration] seconds. + /// + /// Setting parameter [alternate] to true is another way to create a + /// controller whose [reverseDuration] is the same as the forward [duration]. + /// + /// If the animation is finite and there are no "backward" or "atMin" stages + /// then the animation will complete at `progress == 1`, otherwise it will + /// complete at `progress == 0`. + factory EffectController({ + required double duration, + Curve curve = Curves.linear, + double? reverseDuration, + Curve? reverseCurve, + bool infinite = false, + bool alternate = false, + int? repeatCount, + double startDelay = 0.0, + double atMaxDuration = 0.0, + double atMinDuration = 0.0, + }) { + final isLinear = curve == Curves.linear; + final hasReverse = alternate || (reverseDuration != null); + final reverseIsLinear = + reverseCurve == Curves.linear || ((reverseCurve == null) && isLinear); + final items = [ + if (isLinear) LinearEffectController(duration), + if (!isLinear) CurvedEffectController(duration, curve), + if (atMaxDuration != 0) PauseEffectController(atMaxDuration, progress: 1), + if (hasReverse && reverseIsLinear) + ReverseLinearEffectController(reverseDuration ?? duration), + if (hasReverse && !reverseIsLinear) + ReverseCurvedEffectController( + reverseDuration ?? duration, + reverseCurve ?? curve.flipped, + ), + if (atMinDuration != 0) PauseEffectController(atMinDuration, progress: 0), + ]; + assert(items.isNotEmpty); + var controller = + items.length == 1 ? items[0] : SequenceEffectController(items); + if (infinite) { + assert( + repeatCount == null, + 'An infinite animation cannot have a repeat count', + ); + controller = InfiniteEffectController(controller); + } + if (repeatCount != null && repeatCount != 1) { + assert(repeatCount > 0, 'repeatCount must be positive'); + controller = RepeatedEffectController(controller, repeatCount); + } + if (startDelay != 0) { + controller = DelayedEffectController(controller, delay: startDelay); + } + return controller; + } + + EffectController.empty(); + + /// Will the effect continue to run forever (never completes)? + bool get isInfinite => false; + + /// Is the effect's duration random or fixed? + bool get isRandom => false; + + /// Total duration of the effect. If the effect is either infinite or random, + /// this will return `null`. + double? get duration; + + /// Has the effect started running? Some effects use a "delay" parameter to + /// postpone the start of an animation. This property then tells you whether + /// this delay period has already passed. + bool get started => true; + + /// Has the effect already finished? + /// + /// For a finite animation, this property will turn `true` once the animation + /// has finished running and the [progress] variable will no longer change + /// in the future. For an infinite animation this should always return + /// `false`. + bool get completed; + + /// The current progress of the effect, a value between 0 and 1. + double get progress; + + /// Advances this controller's internal clock by [dt] seconds. + /// + /// If the controller is still running, the return value will be 0. If it + /// already finished, then the return value will be the "leftover" part of + /// the [dt]. That is, the amount of time [dt] that remains after the + /// controller has finished. + /// + /// Normally, this method will be called by the owner of the controller class. + /// For example, if the controller is passed to an `Effect` class, then that + /// class will take care of calling this method as necessary. + double advance(double dt); + + /// Similar to `advance()`, but makes the effect controller move back in time. + /// + /// If the supplied amount of time [dt] would push the effect past its + /// starting point, then the effect stops at the start and the "leftover" + /// portion of [dt] is returned. + double recede(double dt); + + /// Reverts the controller to its initial state, as it was before the start + /// of the animation. + void setToStart(); + + /// Puts the controller into its final "completed" state. + void setToEnd(); +} diff --git a/packages/flame/lib/src/effects/controllers/infinite_effect_controller.dart b/packages/flame/lib/src/effects/controllers/infinite_effect_controller.dart new file mode 100644 index 000000000..f02da0300 --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/infinite_effect_controller.dart @@ -0,0 +1,57 @@ +import 'effect_controller.dart'; + +/// Effect controller that wraps a [child] effect controller and repeats it +/// infinitely. +class InfiniteEffectController extends EffectController { + InfiniteEffectController(this.child) : super.empty(); + + final EffectController child; + + @override + bool get isInfinite => true; + + @override + bool get completed => false; + + @override + double? get duration => null; + + @override + double get progress => child.progress; + + @override + double advance(double dt) { + var t = dt; + for (;;) { + t = child.advance(t); + if (t == 0) { + break; + } + child.setToStart(); + } + return 0; + } + + @override + double recede(double dt) { + var t = dt; + for (;;) { + t = child.recede(t); + if (t == 0) { + break; + } + child.setToEnd(); + } + return 0; + } + + @override + void setToStart() { + child.setToStart(); + } + + @override + void setToEnd() { + child.setToEnd(); + } +} diff --git a/packages/flame/lib/src/effects/controllers/linear_effect_controller.dart b/packages/flame/lib/src/effects/controllers/linear_effect_controller.dart new file mode 100644 index 000000000..eb4388d73 --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/linear_effect_controller.dart @@ -0,0 +1,13 @@ +import 'duration_effect_controller.dart'; + +/// A controller that grows linearly from 0 to 1 over [duration] seconds. +/// +/// The [duration] can also be 0, in which case the effect will jump from 0 to 1 +/// instantaneously. +class LinearEffectController extends DurationEffectController { + LinearEffectController(double duration) : super(duration); + + // If duration is 0, `completed` will be true, and division by 0 avoided. + @override + double get progress => completed ? 1 : (timer / duration); +} diff --git a/packages/flame/lib/src/effects/controllers/pause_effect_controller.dart b/packages/flame/lib/src/effects/controllers/pause_effect_controller.dart new file mode 100644 index 000000000..7b851d046 --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/pause_effect_controller.dart @@ -0,0 +1,21 @@ +import 'duration_effect_controller.dart'; + +/// A controller that keeps constant [progress] over [duration] seconds. +/// +/// Since "progress" represents the "logical time" of an Effect, keeping the +/// progress constant over some time is equivalent to freezing in time or +/// pausing the effect for the prescribed duration. +/// +/// This controller is best used in combination with other controllers. For +/// example, you can create a repeated controller where the progress changes +/// 0->1->0 over a short period of time, then pauses, and this sequence repeats. +class PauseEffectController extends DurationEffectController { + PauseEffectController(double duration, {required double progress}) + : _progress = progress, + super(duration); + + final double _progress; + + @override + double get progress => _progress; +} diff --git a/packages/flame/lib/src/effects/controllers/repeated_effect_controller.dart b/packages/flame/lib/src/effects/controllers/repeated_effect_controller.dart new file mode 100644 index 000000000..6ff863d1a --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/repeated_effect_controller.dart @@ -0,0 +1,79 @@ +import 'effect_controller.dart'; + +/// Effect controller that repeats [child] controller a certain number of times. +/// +/// The [repeatCount] must be positive, and [child] controller cannot be +/// infinite. The child controller will be reset after each iteration (except +/// the last). +class RepeatedEffectController extends EffectController { + RepeatedEffectController(this.child, this.repeatCount) + : assert(repeatCount > 0, 'repeatCount must be positive'), + assert(!child.isInfinite, 'child cannot be infinite'), + _remainingCount = repeatCount, + super.empty(); + + final EffectController child; + final int repeatCount; + + /// How many iterations this controller has remaining. When this reaches 0 + /// the controller is considered completed. + int get remainingIterationsCount => _remainingCount; + int _remainingCount; + + @override + double get progress => child.progress; + + @override + bool get completed => _remainingCount == 0; + + @override + double? get duration { + final d = child.duration; + return d == null ? null : d * repeatCount; + } + + @override + double advance(double dt) { + var t = child.advance(dt); + while (t > 0 && _remainingCount > 0) { + _remainingCount--; + if (_remainingCount != 0) { + child.setToStart(); + t = child.advance(t); + } + } + if (_remainingCount == 1 && child.completed) { + _remainingCount--; + } + return t; + } + + @override + double recede(double dt) { + if (_remainingCount == 0 && dt > 0) { + // When advancing, we do not reset the child on last iteration. Hence, + // if we recede from the end position the remaining count must be + // adjusted. + _remainingCount = 1; + } + var t = child.recede(dt); + while (t > 0 && _remainingCount < repeatCount) { + _remainingCount++; + child.setToEnd(); + t = child.recede(t); + } + return t; + } + + @override + void setToStart() { + _remainingCount = repeatCount; + child.setToStart(); + } + + @override + void setToEnd() { + _remainingCount = 0; + child.setToEnd(); + } +} diff --git a/packages/flame/lib/src/effects/controllers/reverse_curved_effect_controller.dart b/packages/flame/lib/src/effects/controllers/reverse_curved_effect_controller.dart new file mode 100644 index 000000000..fb918f7a1 --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/reverse_curved_effect_controller.dart @@ -0,0 +1,18 @@ +import 'package:flutter/animation.dart'; + +import 'duration_effect_controller.dart'; + +/// A controller that grows non-linearly from 1 to 0 following the provided +/// [curve]. The [duration] cannot be 0. +class ReverseCurvedEffectController extends DurationEffectController { + ReverseCurvedEffectController(double duration, Curve curve) + : assert(duration > 0, 'Duration must be positive: $duration'), + _curve = curve, + super(duration); + + Curve get curve => _curve; + final Curve _curve; + + @override + double get progress => _curve.transform(1 - timer / duration); +} diff --git a/packages/flame/lib/src/effects/controllers/reverse_linear_effect_controller.dart b/packages/flame/lib/src/effects/controllers/reverse_linear_effect_controller.dart new file mode 100644 index 000000000..d1258cac5 --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/reverse_linear_effect_controller.dart @@ -0,0 +1,13 @@ +import 'duration_effect_controller.dart'; + +/// A controller that grows linearly from 1 to 0 over [duration] seconds. +/// +/// The [duration] can also be 0, in which case the effect will jump from 1 to 0 +/// instantaneously. +class ReverseLinearEffectController extends DurationEffectController { + ReverseLinearEffectController(double duration) : super(duration); + + // If duration is 0, `completed` will be true, and division by 0 avoided. + @override + double get progress => completed ? 0 : 1 - (timer / duration); +} diff --git a/packages/flame/lib/src/effects/controllers/sequence_effect_controller.dart b/packages/flame/lib/src/effects/controllers/sequence_effect_controller.dart new file mode 100644 index 000000000..3a217eb81 --- /dev/null +++ b/packages/flame/lib/src/effects/controllers/sequence_effect_controller.dart @@ -0,0 +1,80 @@ +import 'effect_controller.dart'; + +/// An effect controller that executes a list of other controllers one after +/// another. +class SequenceEffectController extends EffectController { + SequenceEffectController(List controllers) + : assert(controllers.isNotEmpty, 'List of controllers cannot be empty'), + assert( + !controllers.any((c) => c.isInfinite), + 'Children controllers cannot be infinite', + ), + children = controllers, + _currentIndex = 0, + super.empty(); + + /// Individual controllers in the sequence. + final List children; + + /// The index of the controller currently being executed. This starts with 0, + /// and by the end it will be equal to `_children.length - 1`. This variable + /// is always a valid index within the `_children` list. + int _currentIndex; + + @override + bool get completed { + return _currentIndex == children.length - 1 && + children[_currentIndex].completed; + } + + @override + double? get duration { + var totalDuration = 0.0; + for (final controller in children) { + final d = controller.duration; + if (d == null) { + return null; + } + totalDuration += d; + } + return totalDuration; + } + + @override + bool get isRandom => children.any((c) => c.isRandom); + + @override + double get progress => children[_currentIndex].progress; + + @override + double advance(double dt) { + var t = children[_currentIndex].advance(dt); + while (t > 0 && _currentIndex < children.length - 1) { + _currentIndex++; + t = children[_currentIndex].advance(t); + } + return t; + } + + @override + double recede(double dt) { + var t = children[_currentIndex].recede(dt); + while (t > 0 && _currentIndex > 0) { + _currentIndex--; + t = children[_currentIndex].recede(t); + } + return t; + } + + @override + void setToStart() { + _currentIndex = 0; + children.forEach((c) => c.setToStart()); + } + + @override + void setToEnd() { + _currentIndex = children.length - 1; + children.forEach((c) => c.setToEnd()); + } +} diff --git a/packages/flame/lib/src/effects/effect.dart b/packages/flame/lib/src/effects/effect.dart index 6e8a8d2bd..6298e97b6 100644 --- a/packages/flame/lib/src/effects/effect.dart +++ b/packages/flame/lib/src/effects/effect.dart @@ -1,7 +1,7 @@ import 'package:meta/meta.dart'; import '../../components.dart'; -import 'effect_controller.dart'; +import 'controllers/effect_controller.dart'; /// An [Effect] is a component that changes properties or appearance of another /// component over time. @@ -30,7 +30,8 @@ abstract class Effect extends Component { : removeOnFinish = true, _paused = false, _started = false, - _finished = false; + _finished = false, + _reversed = false; /// An object that describes how the effect should evolve over time. final EffectController controller; @@ -57,6 +58,13 @@ abstract class Effect extends Component { bool get isPaused => _paused; bool _paused; + /// Whether the effect is currently running back in time. + /// + /// Call `reverse()` in order to change this. When the effect is reset, this + /// is set to false. + bool get isReversed => _reversed; + bool _reversed; + /// Pause the effect. The effect will not respond to updates while it is /// paused. Calling `resume()` or `reset()` will un-pause it. Pausing an /// already paused effect is a no-op. @@ -66,6 +74,9 @@ abstract class Effect extends Component { /// currently paused, this call is a no-op. void resume() => _paused = false; + /// Cause the effect to run back in time. + void reverse() => _reversed = !_reversed; + /// Restore the effect to its original state as it was when the effect was /// just created. /// @@ -75,10 +86,11 @@ abstract class Effect extends Component { /// it to the target. @mustCallSuper void reset() { - controller.reset(); + controller.setToStart(); _paused = false; _started = false; _finished = false; + _reversed = false; } /// Implementation of [Component]'s `update()` method. Derived classes are @@ -89,7 +101,11 @@ abstract class Effect extends Component { return; } super.update(dt); - controller.update(dt); + if (_reversed) { + controller.recede(dt); + } else { + controller.advance(dt); + } if (!_started && controller.started) { _started = true; onStart(); diff --git a/packages/flame/lib/src/effects/effect_controller.dart b/packages/flame/lib/src/effects/effect_controller.dart deleted file mode 100644 index 0ac6d83df..000000000 --- a/packages/flame/lib/src/effects/effect_controller.dart +++ /dev/null @@ -1,57 +0,0 @@ -/// Base "controller" class to facilitate animation of effects. -/// -/// The purpose of an effect controller is to define how an effect or an -/// animation should progress over time. To facilitate that, this class provides -/// variable [progress], which will grow from 0.0 to 1.0 over time. The value -/// of 0 corresponds to the beginning of an animation, and the value of 1.0 is -/// the end of the animation. -/// -/// The [progress] variable can be best thought of as a "logical time". For -/// example, if you want to animate a certain property from value A to value B, -/// then you can use [progress] to linearly interpolate between these two -/// extremes and obtain variable `x = A*(1 - progress) + B*progress`. -/// -/// The exact behavior of [progress] is determined by subclasses, but the -/// following general considerations apply: -/// - the progress may go in negative direction (i.e. from 1 to 0); -/// - the progress may oscillate, going from 0 to 1, then back to 0, etc; -/// - the progress may change over a finite or infinite period of time; -/// - the value of 0 corresponds to logical start of an animation; -/// - the value of 1 is either a start or "peak" of an animation; -/// - the progress may briefly attain values outside of [0; 1] range (for -/// example if a "bouncy" easing curve is applied). -/// -/// Unlike the `dart.ui.AnimationController`, this class does not use a `Ticker` -/// to keep track of time. Instead, it must be pushed through time manually, by -/// calling the `update()` method within the game loop. -abstract class EffectController { - /// Will the effect continue to run forever (i.e. has no logical end)? - bool get isInfinite; - - /// Has the effect started running? Some effects use a "delay" parameter to - /// postpone the start of an animation. This property then tells you whether - /// this delay period has already passed. - bool get started; - - /// Has the effect already completed running? - /// - /// For a finite animation, this property will turn `true` once the animation - /// has finished running and the [progress] variable will no longer change - /// in the future. For an infinite animation this should always return - /// `false`. - bool get completed; - - /// The current value of the effect/animation, a value between 0 and 1. - double get progress; - - /// Reverts the controller to its initial state, as it was before the start - /// of the animation. - void reset(); - - /// Advances this controller's internal clock by [dt] seconds. - /// - /// Normally, this method will be called by the owner of the controller class. - /// For example, if the controller is passed to an `Effect` class, then that - /// class will take care of calling the `update()` method as necessary. - void update(double dt); -} diff --git a/packages/flame/lib/src/effects/move_effect.dart b/packages/flame/lib/src/effects/move_effect.dart index 25de0bd9f..9306c6191 100644 --- a/packages/flame/lib/src/effects/move_effect.dart +++ b/packages/flame/lib/src/effects/move_effect.dart @@ -2,7 +2,7 @@ import 'dart:ui'; import 'package:vector_math/vector_math_64.dart'; -import 'effect_controller.dart'; +import 'controllers/effect_controller.dart'; import 'transform2d_effect.dart'; /// Move a component to a new position. diff --git a/packages/flame/lib/src/effects/opacity_effect.dart b/packages/flame/lib/src/effects/opacity_effect.dart index 636f15674..932681e92 100644 --- a/packages/flame/lib/src/effects/opacity_effect.dart +++ b/packages/flame/lib/src/effects/opacity_effect.dart @@ -1,6 +1,6 @@ import '../../components.dart'; import 'component_effect.dart'; -import 'effect_controller.dart'; +import 'controllers/effect_controller.dart'; /// Change the opacity of a component over time. /// diff --git a/packages/flame/lib/src/effects/remove_effect.dart b/packages/flame/lib/src/effects/remove_effect.dart index b174e0612..8893ae227 100644 --- a/packages/flame/lib/src/effects/remove_effect.dart +++ b/packages/flame/lib/src/effects/remove_effect.dart @@ -1,11 +1,10 @@ +import 'controllers/linear_effect_controller.dart'; import 'effect.dart'; -import 'simple_effect_controller.dart'; /// This simple effect, when attached to a component, will cause that component /// to be removed from the game tree after `delay` seconds. class RemoveEffect extends Effect { - RemoveEffect({double delay = 0.0}) - : super(SimpleEffectController(delay: delay)); + RemoveEffect({double delay = 0.0}) : super(LinearEffectController(delay)); @override void apply(double progress) { diff --git a/packages/flame/lib/src/effects/rotate_effect.dart b/packages/flame/lib/src/effects/rotate_effect.dart index f31def68d..a8c23f2a3 100644 --- a/packages/flame/lib/src/effects/rotate_effect.dart +++ b/packages/flame/lib/src/effects/rotate_effect.dart @@ -1,4 +1,4 @@ -import 'effect_controller.dart'; +import 'controllers/effect_controller.dart'; import 'transform2d_effect.dart'; /// Rotate a component around its anchor. diff --git a/packages/flame/lib/src/effects/scale_effect.dart b/packages/flame/lib/src/effects/scale_effect.dart index a29c5cfd5..a311ac496 100644 --- a/packages/flame/lib/src/effects/scale_effect.dart +++ b/packages/flame/lib/src/effects/scale_effect.dart @@ -1,6 +1,6 @@ import 'package:vector_math/vector_math_64.dart'; -import 'effect_controller.dart'; +import 'controllers/effect_controller.dart'; import 'transform2d_effect.dart'; /// Scale a component. diff --git a/packages/flame/lib/src/effects/simple_effect_controller.dart b/packages/flame/lib/src/effects/simple_effect_controller.dart deleted file mode 100644 index 9360a8d95..000000000 --- a/packages/flame/lib/src/effects/simple_effect_controller.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'effect_controller.dart'; -import 'standard_effect_controller.dart'; - -/// Simplest possible [EffectController], which supports an effect progressing -/// linearly over [duration] seconds. -/// -/// The [duration] can be 0, in which case the effect will jump from 0 to 1 -/// instantaneously. -/// -/// The [delay] parameter allows to delay the start of the effect by the -/// specified number of seconds. -/// -/// See also: [StandardEffectController] -class SimpleEffectController extends EffectController { - SimpleEffectController({ - this.duration = 0.0, - this.delay = 0.0, - }) : assert(duration >= 0, 'duration cannot be negative: $duration'), - assert(delay >= 0, 'delay cannot be negative: $delay'); - - final double duration; - final double delay; - double _timer = 0.0; - - @override - bool get started => _timer >= delay; - - @override - bool get completed => _timer >= delay + duration; - - @override - bool get isInfinite => false; - - @override - double get progress { - // If duration == 0, then `completed == started`, and the middle case - // (which divides by duration) cannot occur. - return completed - ? 1 - : started - ? (_timer - delay) / duration - : 0; - } - - @override - void update(double dt) { - _timer += dt; - } - - @override - void reset() { - _timer = 0; - } -} diff --git a/packages/flame/lib/src/effects/size_effect.dart b/packages/flame/lib/src/effects/size_effect.dart index 373d75b4f..6d1267fa1 100644 --- a/packages/flame/lib/src/effects/size_effect.dart +++ b/packages/flame/lib/src/effects/size_effect.dart @@ -1,7 +1,7 @@ import '../../components.dart'; import '../../extensions.dart'; import 'component_effect.dart'; -import 'effect_controller.dart'; +import 'controllers/effect_controller.dart'; /// Change the size of a component over time. /// diff --git a/packages/flame/lib/src/effects/standard_effect_controller.dart b/packages/flame/lib/src/effects/standard_effect_controller.dart deleted file mode 100644 index 00bf88010..000000000 --- a/packages/flame/lib/src/effects/standard_effect_controller.dart +++ /dev/null @@ -1,234 +0,0 @@ -import 'package:flutter/animation.dart'; - -import 'effect_controller.dart'; - -/// A commonly used implementation of a [EffectController]. -/// -/// In the simplest case, [StandardEffectController] will have a positive -/// `duration` and will change its [progress] linearly from 0 to 1 over the -/// period of that duration. -/// -/// More generally, a [StandardEffectController] allows to add a delay before -/// the beginning of the animation, to animate both forward and in reverse, -/// to iterate several times (or infinitely), to apply an arbitrary [Curve] -/// making the effect progression non-linear, etc. -/// -/// In the most general case, the animation proceeds through the following -/// steps: -/// 1. wait for [startDelay] seconds, -/// 2. repeat the following steps [repeatCount] times (or infinitely): -/// a. progress from 0 to 1 over the [forwardDuration] seconds, -/// b. wait for [atMaxDuration] seconds, -/// c. progress from 1 to 0 over the [backwardDuration] seconds, -/// d. wait for [atMinDuration] seconds, -/// 3. mark the animation as [completed]. -/// -/// If the animation is finite and there are no `backward` or `atMin` stages -/// then the animation will complete at `progress == 1`, otherwise it will -/// complete at `progress == 0`. -/// -/// The animation is "sticky" at the end of the `forward` and `backward` stages. -/// This means that within a single [update()] call the animation may complete -/// these stages but will not move on to the next ones. Thus, you're guaranteed -/// to be able to observe `progress == 1` and `progress == 0` at least once -/// within each iteration cycle. -class StandardEffectController extends EffectController { - StandardEffectController({ - required double duration, - Curve curve = Curves.linear, - double reverseDuration = 0.0, - Curve? reverseCurve, - bool infinite = false, - int? repeatCount, - this.startDelay = 0.0, - this.atMaxDuration = 0.0, - this.atMinDuration = 0.0, - }) : assert( - !infinite || repeatCount == null, - 'An infinite animation cannot have a repeat count', - ), - assert( - infinite || (repeatCount ?? 1) > 0, - 'repeatCount must be positive', - ), - assert(duration > 0, 'duration must be positive'), - assert(reverseDuration >= 0, 'reverseDuration cannot be negative'), - assert(startDelay >= 0, 'startDelay cannot be negative'), - assert(atMaxDuration >= 0, 'atMaxDuration cannot be negative'), - assert(atMinDuration >= 0, 'atMinDuration cannot be negative'), - repeatCount = infinite ? -1 : (repeatCount ?? 1), - forwardDuration = duration, - backwardDuration = reverseDuration, - forwardCurve = curve, - backwardCurve = - reverseCurve ?? (curve == Curves.linear ? curve : curve.flipped), - _progress = 0, - _remainingIterationsCount = repeatCount ?? (infinite ? -1 : 1), - _remainingTimeAtCurrentStage = startDelay, - _stage = _AnimationStage.beforeStart; - - /// The current value of the animation. - /// - /// This variable changes from 0 to 1 over time, which can be used by an - /// animation to produce the desired transition effect. In essence, you can - /// think of this variable as a "logical time". - /// - /// This variable is guaranteed to be 0 during the `beforeStart` and `atMin` - /// periods, and to be 1 during the `atMax` period. During the `forward` - /// period this variable changes from 0 to 1, and during the `backward` period - /// it goes back from 1 to 0. However, during the latter two periods it is - /// possible for `progress` to become less than 0 or greater than 1 if either - /// the [forwardCurve] or the [backwardCurve] produce values outside of [0; 1] - /// range. - @override - double get progress => _progress; - double _progress; - - /// The transformation curve that applies during the `forward` stage. By - /// default, the curve is linear. - /// - /// The effect of the curve is that if the animation is normally at x% of its - /// progression during the `forward` stage, then the reported [progress] will - /// be equal to `forwardCurve.progress(x)`. - final Curve forwardCurve; - - /// The transformation curve that applies during the `backward` stage. By - /// default, the curve is the flipped to [forwardCurve]. - /// - /// The effect of the curve is that if the animation is at normally x% of its - /// progression during the `backward` stage, then the reported [progress] will - /// be equal to `backwardCurve.progress(x)`. - final Curve backwardCurve; - - /// The number of times to play the animation, defaults to 1. - /// - /// If this value is negative, it indicates an infinitely repeating animation. - /// This value cannot be zero. - final int repeatCount; - - /// Will the effect continue to loop forever? - @override - bool get isInfinite => repeatCount < 0; - - int _remainingIterationsCount; - - @override - bool get started => _stage != _AnimationStage.beforeStart; - - @override - bool get completed => _remainingIterationsCount == 0; - - /// The time (in seconds) before the animation begins. During this time - /// the property [started] will be returning false, and [progress] will be 0. - final double startDelay; - - final double forwardDuration; - final double backwardDuration; - final double atMaxDuration; - final double atMinDuration; - - double get cycleDuration { - return forwardDuration + atMaxDuration + backwardDuration + atMinDuration; - } - - bool get isSimpleAnimation => - atMaxDuration + backwardDuration + atMinDuration == 0; - - _AnimationStage _stage; - double _remainingTimeAtCurrentStage; - - @override - void update(double dt) { - if (completed) { - return; - } - _remainingTimeAtCurrentStage -= dt; - // When remaining time becomes zero or negative, it means we're - // transitioning into the next stage. - // - // In each iteration of the while loop below we transition to the next - // stage and add the next stage's duration to the remaining timer. This may - // not be enough to make the timer positive (particularly if some stage has - // duration 0), which means we need to keep progressing onto the next stage. - // - // The exceptions to this rule are the "forward" and "backward" stages which - // always exit at the end, even if the timer is negative. This allows us to - // have "sticky" start and end of an animation, i.e. the controller will - // never jump over points with progress=0 or progress=1. - while (_remainingTimeAtCurrentStage <= 0) { - // In the switch below we are *finishing* each of the indicated stages. - switch (_stage) { - case _AnimationStage.beforeStart: - _remainingTimeAtCurrentStage += forwardDuration; - _stage = _AnimationStage.forward; - break; - case _AnimationStage.forward: - _progress = 1; - _remainingTimeAtCurrentStage += atMaxDuration; - _stage = _AnimationStage.atMax; - if (_remainingIterationsCount == 1 && isSimpleAnimation) { - _markCompleted(); - } - return; - case _AnimationStage.atMax: - _remainingTimeAtCurrentStage += backwardDuration; - _stage = _AnimationStage.backward; - if (_remainingIterationsCount == 1 && - backwardDuration == 0 && - atMinDuration == 0) { - _markCompleted(); - return; - } - break; - case _AnimationStage.backward: - _progress = 0; - _remainingTimeAtCurrentStage += atMinDuration; - _stage = _AnimationStage.atMin; - return; - case _AnimationStage.atMin: - _remainingTimeAtCurrentStage += forwardDuration; - _stage = _AnimationStage.forward; - if (!isInfinite) { - _remainingIterationsCount -= 1; - if (_remainingIterationsCount == 0) { - _markCompleted(); - return; - } - } - break; - } - } - assert(_remainingTimeAtCurrentStage > 0); - if (_stage == _AnimationStage.forward) { - _progress = forwardCurve.transform( - 1 - _remainingTimeAtCurrentStage / forwardDuration, - ); - } - if (_stage == _AnimationStage.backward) { - _progress = backwardCurve.transform( - _remainingTimeAtCurrentStage / backwardDuration, - ); - } - } - - @override - void reset() { - _progress = 0; - _stage = _AnimationStage.beforeStart; - _remainingTimeAtCurrentStage = startDelay; - _remainingIterationsCount = repeatCount; - } - - void _markCompleted() { - _remainingTimeAtCurrentStage = double.infinity; - _remainingIterationsCount = 0; - } -} - -enum _AnimationStage { - beforeStart, - forward, - atMax, - backward, - atMin, -} diff --git a/packages/flame/lib/src/effects/transform2d_effect.dart b/packages/flame/lib/src/effects/transform2d_effect.dart index b18f182d9..dae4fe276 100644 --- a/packages/flame/lib/src/effects/transform2d_effect.dart +++ b/packages/flame/lib/src/effects/transform2d_effect.dart @@ -1,7 +1,7 @@ import '../components/position_component.dart'; import '../game/transform2d.dart'; import 'component_effect.dart'; -import 'effect_controller.dart'; +import 'controllers/effect_controller.dart'; /// Base class for effects that target a [Transform2D] property. /// diff --git a/packages/flame/test/effects/controllers/curved_effect_controller_test.dart b/packages/flame/test/effects/controllers/curved_effect_controller_test.dart new file mode 100644 index 000000000..36fdaba65 --- /dev/null +++ b/packages/flame/test/effects/controllers/curved_effect_controller_test.dart @@ -0,0 +1,118 @@ +import 'dart:math'; + +import 'package:flame/src/effects/controllers/curved_effect_controller.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter/animation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('CurvedEffectController', () { + const curves = [ + Curves.bounceIn, + Curves.bounceInOut, + Curves.bounceOut, + Curves.decelerate, + Curves.ease, + Curves.easeIn, + Curves.easeInBack, + Curves.easeInCirc, + Curves.easeInCubic, + Curves.easeInExpo, + Curves.easeInOut, + Curves.easeInOutBack, + Curves.easeInOutCirc, + Curves.easeInOutCubic, + Curves.easeInOutCubicEmphasized, + Curves.easeInOutExpo, + Curves.easeInOutQuad, + Curves.easeInOutQuart, + Curves.easeInOutQuint, + Curves.easeInOutSine, + Curves.easeInQuad, + Curves.easeInQuart, + Curves.easeInQuint, + Curves.easeInSine, + Curves.easeInToLinear, + Curves.easeOut, + Curves.easeOutBack, + Curves.easeOutCirc, + Curves.easeOutCubic, + Curves.easeOutExpo, + Curves.easeOutQuad, + Curves.easeOutQuart, + Curves.easeOutQuint, + Curves.easeOutSine, + Curves.elasticIn, + Curves.elasticInOut, + Curves.elasticOut, + Curves.fastLinearToSlowEaseIn, + Curves.fastOutSlowIn, + Curves.linear, + Curves.linearToEaseOut, + Curves.slowMiddle, + ]; + + testRandom('normal', (Random random) { + final duration = random.nextDouble(); + final curve = curves[random.nextInt(curves.length)]; + final ec = CurvedEffectController(duration, curve); + + expect(ec.progress, 0); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.curve, curve); + expect(ec.isInfinite, false); + expect(ec.isRandom, false); + expect(ec.duration, duration); + + var totalTime = 0.0; + while (totalTime < duration) { + final dt = random.nextDouble() * 0.01; + totalTime += dt; + final leftoverTime = ec.advance(dt); + if (totalTime > duration) { + expect(leftoverTime, closeTo(totalTime - duration, 1e-15)); + expect(ec.progress, 1); + } else { + expect(leftoverTime, 0); + expect( + ec.progress, + closeTo(curve.transform(totalTime / duration), 1e-15), + ); + } + } + expect(ec.progress, 1); + expect(ec.completed, true); + + totalTime = duration; + while (totalTime > 0) { + final dt = random.nextDouble() * 0.01; + totalTime -= dt; + final leftoverTime = ec.recede(dt); + if (totalTime > 0) { + expect(leftoverTime, 0); + expect( + ec.progress, + closeTo(curve.transform(totalTime / duration), 1e-15), + ); + } else { + expect(leftoverTime, closeTo(-totalTime, 1e-15)); + expect(ec.progress, 0); + } + } + expect(ec.progress, 0); + expect(ec.completed, false); + }); + + test('errors', () { + expect( + () => CurvedEffectController(0, Curves.linear), + throwsA(isA()), + ); + expect( + () => CurvedEffectController(-1, Curves.linear), + throwsA(isA()), + ); + }); + }); +} diff --git a/packages/flame/test/effects/controllers/delayed_effect_controller_test.dart b/packages/flame/test/effects/controllers/delayed_effect_controller_test.dart new file mode 100644 index 000000000..8922d956b --- /dev/null +++ b/packages/flame/test/effects/controllers/delayed_effect_controller_test.dart @@ -0,0 +1,73 @@ +import 'package:flame/src/effects/controllers/delayed_effect_controller.dart'; +import 'package:flame/src/effects/controllers/linear_effect_controller.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('DelayedEffectController', () { + test('normal', () { + final ec = DelayedEffectController(LinearEffectController(1), delay: 3); + expect(ec.isInfinite, false); + expect(ec.isRandom, false); + expect(ec.started, false); + expect(ec.completed, false); + expect(ec.progress, 0); + expect(ec.duration, 4); + + for (var i = 0; i < 6; i++) { + expect(ec.advance(0.5), 0); + expect(ec.started, i == 5); + expect(ec.progress, 0); + } + expect(ec.advance(1), 0); + expect(ec.progress, 1); + expect(ec.completed, true); + expect(ec.advance(1), 1); + }); + + test('reset', () { + final ec = DelayedEffectController(LinearEffectController(1), delay: 3); + ec.setToEnd(); + expect(ec.started, true); + expect(ec.completed, true); + expect(ec.progress, 1); + ec.setToStart(); + expect(ec.started, false); + expect(ec.completed, false); + expect(ec.progress, 0); + }); + + test('advance/recede', () { + final ec = DelayedEffectController(LinearEffectController(1), delay: 3); + + expect(ec.advance(1), 0); + expect(ec.recede(0.5), 0); + expect(ec.advance(0.5), 0); + expect(ec.advance(2), 0); // 3/3 + 0/1 + expect(ec.progress, 0); + expect(ec.started, true); + expect(ec.recede(0.5), 0); // 2.5/3 + 0/1 + expect(ec.started, false); + expect(ec.advance(1), 0); // 3/3 + 0.5/1 + expect(ec.started, true); + expect(ec.progress, closeTo(0.5, 1e-15)); + expect(ec.advance(1), closeTo(0.5, 1e-15)); // 3/3 + 1/1 + expect(ec.completed, true); + expect(ec.recede(0.5), 0); // 3/3 + 0.5/1 + expect(ec.completed, false); + expect(ec.started, true); + expect(ec.progress, closeTo(0.5, 1e-15)); + expect(ec.recede(1), 0); // 2.5/3 + 0/1 + expect(ec.progress, 0); + expect(ec.started, false); + expect(ec.recede(3), closeTo(0.5, 1e-15)); + }); + + test('errors', () { + expect( + () => DelayedEffectController(LinearEffectController(1), delay: -1), + failsAssert('Delay must be non-negative: -1.0'), + ); + }); + }); +} diff --git a/packages/flame/test/effects/standard_effect_controller_test.dart b/packages/flame/test/effects/controllers/effect_controller_test.dart similarity index 60% rename from packages/flame/test/effects/standard_effect_controller_test.dart rename to packages/flame/test/effects/controllers/effect_controller_test.dart index 318c7382d..290a4b670 100644 --- a/packages/flame/test/effects/standard_effect_controller_test.dart +++ b/packages/flame/test/effects/controllers/effect_controller_test.dart @@ -1,97 +1,90 @@ import 'dart:math'; -import 'package:flame/src/effects/standard_effect_controller.dart'; +import 'package:flame/src/effects/controllers/effect_controller.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter/animation.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { - group('StandardEffectController', () { + group('EffectController', () { test('forward', () { - final ec = StandardEffectController(duration: 1.0); + final ec = EffectController(duration: 1.0); expect(ec.isInfinite, false); - expect(ec.isSimpleAnimation, true); - expect(ec.started, false); + expect(ec.started, true); expect(ec.completed, false); expect(ec.progress, 0.0); - ec.update(0.5); + ec.advance(0.5); expect(ec.started, true); expect(ec.completed, false); expect(ec.progress, 0.5); - ec.update(0.5); + ec.advance(0.5); expect(ec.started, true); expect(ec.completed, true); expect(ec.progress, 1.0); // Updating controller after it already finished is a no-op - ec.update(0.5); + ec.advance(0.5); expect(ec.started, true); expect(ec.completed, true); expect(ec.progress, 1.0); }); test('forward x 2', () { - final ec = StandardEffectController(duration: 1, repeatCount: 2); - expect(ec.isSimpleAnimation, true); - expect(ec.started, false); + final ec = EffectController(duration: 1, repeatCount: 2); + expect(ec.started, true); expect(ec.progress, 0); - ec.update(1); + ec.advance(1); expect(ec.progress, 1); - ec.update(0); - expect(ec.progress, 0); - ec.update(1); + ec.advance(1e-10); + expect(ec.progress, closeTo(1e-10, 1e-15)); + ec.advance(1); expect(ec.progress, 1); expect(ec.completed, true); }); test('forward + delay', () { - final ec = StandardEffectController(duration: 1.0, startDelay: 0.2); + final ec = EffectController(duration: 1.0, startDelay: 0.2); expect(ec.isInfinite, false); - expect(ec.isSimpleAnimation, true); // initial delay for (var i = 0; i < 20; i++) { expect(ec.started, false); expect(ec.completed, false); expect(ec.progress, 0); - ec.update(0.01); + ec.advance(0.01); } expect(ec.progress, closeTo(0, 1e-10)); // progress from 0 to 1 over 1s for (var i = 0; i < 100; i++) { - ec.update(0.01); + ec.advance(0.01); expect(ec.started, true); expect(ec.progress, closeTo((i + 1) / 100, 1e-10)); } // final update, to account for any rounding errors - ec.update(1e-10); + ec.advance(1e-10); expect(ec.completed, true); expect(ec.progress, 1); }); test('forward + atMax', () { - final ec = StandardEffectController(duration: 1, atMaxDuration: 0.5); - expect(ec.cycleDuration, 1.5); - expect(ec.isSimpleAnimation, false); + final ec = EffectController(duration: 1, atMaxDuration: 0.5); + expect(ec.duration, 1.5); expect(ec.isInfinite, false); expect(ec.progress, 0); - ec.update(1.5); + ec.advance(1.5); expect(ec.progress, 1); expect(ec.started, true); - expect(ec.completed, false); - ec.update(0); - expect(ec.progress, 1); expect(ec.completed, true); }); test('(forward + reverse) x 5', () { - final ec = StandardEffectController( + final ec = EffectController( startDelay: 1.0, duration: 2.0, reverseDuration: 1.0, @@ -101,18 +94,11 @@ void main() { ); expect(ec.isInfinite, false); expect(ec.progress, 0); - expect(ec.repeatCount, 5); expect(ec.started, false); expect(ec.completed, false); - expect(ec.forwardDuration, 2.0); - expect(ec.backwardDuration, 1.0); - expect(ec.atMaxDuration, 0.2); - expect(ec.atMinDuration, 0.5); - expect(ec.cycleDuration, 3.7); - expect(ec.isSimpleAnimation, false); // Initial delay - ec.update(1.0); + ec.advance(1.0); expect(ec.started, true); expect(ec.progress, 0); @@ -120,60 +106,50 @@ void main() { for (var iteration = 0; iteration < 5; iteration++) { // forward for (var i = 0; i < 200; i++) { - ec.update(0.01); + ec.advance(0.01); expect(ec.progress, closeTo((i + 1) / 200, 1e-10)); } // atPeak for (var i = 0; i < 20; i++) { - ec.update(0.01); + ec.advance(0.01); expect(ec.progress, closeTo(1, i == 19 ? 1e-10 : 0)); } // reverse for (var i = 0; i < 100; i++) { - ec.update(0.01); + ec.advance(0.01); expect(ec.progress, closeTo((99 - i) / 100, 1e-10)); } // atPit for (var i = 0; i < 50; i++) { - ec.update(0.01); + ec.advance(0.01); expect(ec.progress, closeTo(0, i == 49 ? 1e-10 : 0)); } } // In the end, the progress will remain at zero - ec.update(1e-10); + ec.advance(1e-10); expect(ec.started, true); expect(ec.completed, true); expect(ec.progress, 0); }); testRandom('infinite', (Random random) { - final ec = StandardEffectController(duration: 1.4, infinite: true); + const duration = 1.4; + final ec = EffectController(duration: duration, infinite: true); expect(ec.isInfinite, true); - expect(ec.isSimpleAnimation, true); expect(ec.progress, 0); - expect(ec.started, false); - - ec.update(0); expect(ec.started, true); expect(ec.completed, false); - expect(ec.progress, 0); var stageTime = 0.0; for (var i = 0; i < 100; i++) { final dt = random.nextDouble() * 0.3; - ec.update(dt); + ec.advance(dt); stageTime += dt; - if (stageTime >= ec.forwardDuration) { - stageTime -= ec.forwardDuration; - // The controller will report once `progress==1`, exactly, and then - // once `progress==0`, also exactly. - expect(ec.progress, 1); - ec.update(0); - expect(ec.progress, 0); - } else { - expect(ec.progress, closeTo(stageTime / ec.forwardDuration, 1e-10)); + if (stageTime >= duration) { + stageTime -= duration; } + expect(ec.progress, closeTo(stageTime / duration, 1e-10)); } expect(ec.started, true); @@ -182,32 +158,32 @@ void main() { }); test('reset', () { - final ec = StandardEffectController(duration: 1.23); - expect(ec.started, false); + final ec = EffectController(duration: 1.23); + expect(ec.started, true); expect(ec.progress, 0); - ec.update(0.4); + ec.advance(0.4); expect(ec.started, true); expect(ec.completed, false); expect(ec.progress, closeTo(0.4 / 1.23, 1e-10)); - ec.reset(); - expect(ec.started, false); + ec.setToStart(); + expect(ec.started, true); expect(ec.completed, false); expect(ec.progress, 0); - ec.update(0.5); + ec.advance(0.5); expect(ec.started, true); expect(ec.completed, false); expect(ec.progress, closeTo(0.5 / 1.23, 1e-10)); - ec.update(1); + ec.advance(1); expect(ec.started, true); expect(ec.completed, true); expect(ec.progress, 1); - ec.reset(); - expect(ec.started, false); + ec.setToStart(); + expect(ec.started, true); expect(ec.completed, false); expect(ec.progress, 0); }); @@ -218,80 +194,83 @@ void main() { } expectThrows( - () => StandardEffectController(duration: -1), + () => EffectController(duration: -1), ); expectThrows( - () => StandardEffectController(duration: 1, repeatCount: 0), + () => EffectController(duration: 1, repeatCount: 0), ); expectThrows( - () => StandardEffectController( + () => EffectController( duration: 1, infinite: true, repeatCount: 3, ), ); expectThrows( - () => StandardEffectController(duration: 1, repeatCount: -1), + () => EffectController(duration: 1, repeatCount: -1), ); expectThrows( - () => StandardEffectController(duration: 1, reverseDuration: -1), + () => EffectController(duration: 1, reverseDuration: -1), ); expectThrows( - () => StandardEffectController(duration: 1, startDelay: -1), + () => EffectController(duration: 1, startDelay: -1), ); expectThrows( - () => StandardEffectController(duration: 1, atMaxDuration: -1), + () => EffectController(duration: 1, atMaxDuration: -1), ); expectThrows( - () => StandardEffectController(duration: 1, atMinDuration: -1), + () => EffectController(duration: 1, atMinDuration: -1), ); }); test('curve', () { - final curve = Curves.easeIn; - final ec = StandardEffectController( + const curve = Curves.easeIn; + final ec = EffectController( duration: 1, curve: curve, reverseDuration: 0.8, ); - expect(ec.started, false); - expect(ec.cycleDuration, 1.8); + expect(ec.started, true); + expect(ec.duration, 1.8); for (var i = 0; i < 100; i++) { - ec.update(0.01); - expect(ec.progress, closeTo(curve.transform((i + 1) / 100), 1e-10)); + ec.advance(0.01); + // Precision is less for final iteration, because it may flip over + // to the backwards curve. + final epsilon = i == 99 ? 1e-6 : 1e-10; + expect(ec.progress, closeTo(curve.transform((i + 1) / 100), epsilon)); } for (var i = 0; i < 80; i++) { - ec.update(0.01); + ec.advance(0.01); expect( ec.progress, closeTo(curve.flipped.transform(1 - (i + 1) / 80), 1e-10), ); } - ec.update(1e-10); + ec.advance(1e-10); expect(ec.completed, true); expect(ec.progress, 0); }); test('reverse curve', () { - final curve = Curves.easeInQuad; - final ec = StandardEffectController( + const curve = Curves.easeInQuad; + final ec = EffectController( duration: 1, reverseDuration: 1, reverseCurve: curve, ); - expect(ec.started, false); - expect(ec.cycleDuration, 2); + expect(ec.started, true); + expect(ec.duration, 2); - ec.update(1); + ec.advance(1); expect(ec.progress, 1); expect(ec.completed, false); for (var i = 0; i < 100; i++) { - ec.update(0.01); + ec.advance(0.01); expect(ec.progress, closeTo(curve.transform(1 - (i + 1) / 100), 1e-10)); } - ec.update(1e-10); + ec.advance(1e-10); expect(ec.completed, true); }); }); diff --git a/packages/flame/test/effects/controllers/infinite_effect_controller_test.dart b/packages/flame/test/effects/controllers/infinite_effect_controller_test.dart new file mode 100644 index 000000000..6ce47c9da --- /dev/null +++ b/packages/flame/test/effects/controllers/infinite_effect_controller_test.dart @@ -0,0 +1,56 @@ +import 'dart:math'; + +import 'package:flame/src/effects/controllers/infinite_effect_controller.dart'; +import 'package:flame/src/effects/controllers/linear_effect_controller.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('InfiniteEffectController', () { + test('basic properties', () { + final ec = InfiniteEffectController(LinearEffectController(1)); + expect(ec.isInfinite, true); + expect(ec.isRandom, false); + expect(ec.duration, null); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, 0); + }); + + test('reset', () { + final ec = InfiniteEffectController(LinearEffectController(1)); + ec.setToEnd(); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, 1); + ec.setToStart(); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, 0); + }); + + testRandom('advance', (Random random) { + final ec = InfiniteEffectController(LinearEffectController(1)); + var totalTime = 0.0; + while (totalTime < 10) { + final dt = random.nextDouble() * 0.1; + totalTime += dt; + expect(ec.advance(dt), 0); + expect(ec.progress, closeTo(totalTime % 1, 5e-14)); + } + expect(ec.completed, false); + }); + + testRandom('recede', (Random random) { + final ec = InfiniteEffectController(LinearEffectController(1)); + var totalTime = 0.0; + while (totalTime < 10) { + final dt = random.nextDouble() * 0.1; + totalTime += dt; + expect(ec.recede(dt), 0); + expect(ec.progress, closeTo((11 - totalTime) % 1, 5e-14)); + } + expect(ec.completed, false); + }); + }); +} diff --git a/packages/flame/test/effects/controllers/linear_effect_controller_test.dart b/packages/flame/test/effects/controllers/linear_effect_controller_test.dart new file mode 100644 index 000000000..6ac73426e --- /dev/null +++ b/packages/flame/test/effects/controllers/linear_effect_controller_test.dart @@ -0,0 +1,86 @@ +import 'package:flame/src/effects/controllers/linear_effect_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('LinearEffectController', () { + test('[duration==0]', () { + final ec = LinearEffectController(0); + expect(ec.duration, 0); + expect(ec.isInfinite, false); + expect(ec.started, true); + expect(ec.completed, true); + expect(ec.progress, 1); + + expect(ec.advance(0.1), 0.1); + expect(ec.progress, 1); + }); + + test('[duration==0] reset', () { + final ec = LinearEffectController(0); + ec.setToStart(); + expect(ec.completed, true); + expect(ec.progress, 1); + ec.setToEnd(); + expect(ec.completed, true); + expect(ec.progress, 1); + }); + + test('[duration==1]', () { + final ec = LinearEffectController(1); + expect(ec.duration, 1); + expect(ec.progress, 0); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.isInfinite, false); + + expect(ec.advance(0.5), 0); + expect(ec.progress, 0.5); + expect(ec.completed, false); + + expect(ec.advance(0.5), 0); + expect(ec.progress, 1); + expect(ec.completed, true); + + expect(ec.advance(0.00001), closeTo(0.00001, 1e-15)); + expect(ec.progress, 1); + expect(ec.completed, true); + + expect(ec.recede(0.5), 0); + expect(ec.progress, 0.5); + + expect(ec.recede(0.5), 0); + expect(ec.progress, 0); + + expect(ec.recede(0.00001), closeTo(0.00001, 1e-15)); + expect(ec.progress, 0); + }); + + test('[duration==2] reset', () { + final ec = LinearEffectController(2); + expect(ec.advance(3), 1); + expect(ec.completed, true); + expect(ec.progress, 1); + + ec.setToStart(); + expect(ec.completed, false); + expect(ec.progress, 0); + + expect(ec.advance(1), 0); + expect(ec.progress, closeTo(0.5, 1e-15)); + expect(ec.completed, false); + + expect(ec.advance(1), 0); + expect(ec.progress, 1); + expect(ec.completed, true); + + expect(ec.advance(1), 1); + expect(ec.progress, 1); + expect(ec.completed, true); + + ec.setToStart(); + ec.setToEnd(); + expect(ec.progress, 1); + expect(ec.completed, true); + }); + }); +} diff --git a/packages/flame/test/effects/controllers/repeated_effect_controller_test.dart b/packages/flame/test/effects/controllers/repeated_effect_controller_test.dart new file mode 100644 index 000000000..f5721cc1a --- /dev/null +++ b/packages/flame/test/effects/controllers/repeated_effect_controller_test.dart @@ -0,0 +1,182 @@ +import 'package:flame/src/effects/controllers/infinite_effect_controller.dart'; +import 'package:flame/src/effects/controllers/linear_effect_controller.dart'; +import 'package:flame/src/effects/controllers/repeated_effect_controller.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('RepeatedEffectController', () { + test('basic properties', () { + final ec = RepeatedEffectController(LinearEffectController(1), 5); + expect(ec.isInfinite, false); + expect(ec.isRandom, false); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.duration, 5); + expect(ec.progress, 0); + expect(ec.repeatCount, 5); + expect(ec.remainingIterationsCount, 5); + }); + + test('reset', () { + final ec = RepeatedEffectController(LinearEffectController(1), 5); + ec.setToEnd(); + expect(ec.started, true); + expect(ec.completed, true); + expect(ec.child.completed, true); + expect(ec.progress, 1); + expect(ec.remainingIterationsCount, 0); + + ec.setToStart(); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.child.completed, false); + expect(ec.progress, 0); + expect(ec.remainingIterationsCount, 5); + }); + + test('advance', () { + final ec = RepeatedEffectController(LinearEffectController(2), 5); + expect(ec.remainingIterationsCount, 5); + + // First iteration + expect(ec.advance(1), 0); + expect(ec.progress, 0.5); + expect(ec.remainingIterationsCount, 5); + + expect(ec.advance(1), 0); + expect(ec.progress, 1); + expect(ec.remainingIterationsCount, 5); + + // Second iteration + expect(ec.advance(1), 0); + expect(ec.progress, 0.5); + expect(ec.remainingIterationsCount, 4); + + expect(ec.advance(1), 0); + expect(ec.progress, 1); + expect(ec.remainingIterationsCount, 4); + + // Third iteration + expect(ec.advance(1), 0); + expect(ec.progress, 0.5); + expect(ec.remainingIterationsCount, 3); + + expect(ec.advance(1), 0); + expect(ec.progress, 1); + expect(ec.remainingIterationsCount, 3); + + // Forth iteration + expect(ec.advance(1), 0); + expect(ec.progress, 0.5); + expect(ec.remainingIterationsCount, 2); + + expect(ec.advance(1), 0); + expect(ec.progress, 1); + expect(ec.remainingIterationsCount, 2); + + // Fifth iteration + expect(ec.advance(1), 0); + expect(ec.progress, 0.5); + expect(ec.remainingIterationsCount, 1); + + expect(ec.advance(1), 0); + expect(ec.progress, 1); + // last iteration is consumed immediately + expect(ec.remainingIterationsCount, 0); + expect(ec.completed, true); + + // Any subsequent time will be spilled over + expect(ec.advance(1), 1); + expect(ec.progress, 1); + expect(ec.completed, true); + }); + + test('advance 2', () { + const n = 5; + const dt = 0.17; + final nIterations = (n / dt).floor(); + final ec = RepeatedEffectController(LinearEffectController(1), n); + for (var i = 0; i < nIterations; i++) { + expect(ec.advance(dt), 0); + expect(ec.progress, closeTo((i + 1) * dt % 1, 1e-15)); + } + expect(ec.advance(dt), closeTo((nIterations + 1) * dt - n, 1e-15)); + expect(ec.completed, true); + expect(ec.progress, 1); + }); + + test('recede', () { + final ec = RepeatedEffectController(LinearEffectController(2), 5); + ec.setToEnd(); + expect(ec.completed, true); + expect(ec.recede(0), 0); + expect(ec.completed, true); + expect(ec.remainingIterationsCount, 0); + + // Fifth iteration + expect(ec.recede(1), 0); + expect(ec.progress, 0.5); + expect(ec.completed, false); + expect(ec.remainingIterationsCount, 1); + + expect(ec.recede(1), 0); + expect(ec.progress, 0); + expect(ec.remainingIterationsCount, 1); + + // Forth iteration + expect(ec.recede(1), 0); + expect(ec.progress, 0.5); + expect(ec.remainingIterationsCount, 2); + + expect(ec.recede(1), 0); + expect(ec.progress, 0); + expect(ec.remainingIterationsCount, 2); + + // Third iteration + expect(ec.recede(1), 0); + expect(ec.progress, 0.5); + expect(ec.remainingIterationsCount, 3); + + expect(ec.recede(1), 0); + expect(ec.progress, 0); + expect(ec.remainingIterationsCount, 3); + + // Second iteration + expect(ec.recede(1), 0); + expect(ec.progress, 0.5); + expect(ec.remainingIterationsCount, 4); + + expect(ec.recede(1), 0); + expect(ec.progress, 0); + expect(ec.remainingIterationsCount, 4); + + // First iteration + expect(ec.recede(1), 0); + expect(ec.progress, 0.5); + expect(ec.remainingIterationsCount, 5); + + expect(ec.recede(1), 0); + expect(ec.progress, 0); + expect(ec.remainingIterationsCount, 5); + expect(ec.started, true); + + // Extra iterations + expect(ec.recede(1), 1); + expect(ec.progress, 0); + expect(ec.remainingIterationsCount, 5); + }); + + test('errors', () { + final ec = LinearEffectController(1); + expect( + () => RepeatedEffectController(InfiniteEffectController(ec), 1), + failsAssert('child cannot be infinite'), + ); + expect( + () => RepeatedEffectController(ec, 0), + failsAssert('repeatCount must be positive'), + ); + }); + }); +} diff --git a/packages/flame/test/effects/controllers/sequence_effect_controller_test.dart b/packages/flame/test/effects/controllers/sequence_effect_controller_test.dart new file mode 100644 index 000000000..0d8235f51 --- /dev/null +++ b/packages/flame/test/effects/controllers/sequence_effect_controller_test.dart @@ -0,0 +1,117 @@ +import 'dart:math'; + +import 'package:flame/src/effects/controllers/infinite_effect_controller.dart'; +import 'package:flame/src/effects/controllers/linear_effect_controller.dart'; +import 'package:flame/src/effects/controllers/sequence_effect_controller.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:test/test.dart'; + +void main() { + group('SequenceEffectController', () { + test('basic properties', () { + final ec = SequenceEffectController([ + LinearEffectController(1), + LinearEffectController(2), + LinearEffectController(3), + ]); + expect(ec.isRandom, false); + expect(ec.isInfinite, false); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.duration, 6); + expect(ec.progress, 0); + expect(ec.children.length, 3); + }); + + test('reset', () { + final ec = SequenceEffectController([ + LinearEffectController(1), + LinearEffectController(2), + LinearEffectController(3), + ]); + ec.setToEnd(); + expect(ec.started, true); + expect(ec.completed, true); + expect(ec.progress, 1); + expect(ec.children.every((c) => c.completed), true); + + ec.setToStart(); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, 0); + expect(ec.children.every((c) => c.progress == 0), true); + }); + + testRandom('advance', (Random random) { + final ec = SequenceEffectController([ + LinearEffectController(1), + LinearEffectController(2), + LinearEffectController(3), + ]); + + var totalTime = 0.0; + while (totalTime <= 6) { + expect( + ec.progress, + closeTo( + totalTime <= 1 + ? totalTime + : totalTime <= 3 + ? (totalTime - 1) / 2 + : (totalTime - 3) / 3, + 1e-15, + ), + ); + final dt = random.nextDouble(); + totalTime += dt; + ec.advance(dt); + } + expect(ec.completed, true); + expect(ec.progress, 1); + expect(ec.children.every((c) => c.completed), true); + }); + + testRandom('recede', (Random random) { + final ec = SequenceEffectController([ + LinearEffectController(1), + LinearEffectController(2), + LinearEffectController(3), + ]); + ec.setToEnd(); + + var totalTime = 6.0; + while (totalTime >= 0) { + expect( + ec.progress, + closeTo( + totalTime <= 1 + ? totalTime + : totalTime <= 3 + ? (totalTime - 1) / 2 + : (totalTime - 3) / 3, + 1e-14, + ), + ); + final dt = random.nextDouble() * 0.1; + totalTime -= dt; + ec.recede(dt); + } + expect(ec.completed, false); + expect(ec.progress, 0); + expect(ec.children.every((c) => c.progress == 0), true); + }); + + test('errors', () { + expect( + () => SequenceEffectController([]), + failsAssert('List of controllers cannot be empty'), + ); + expect( + () => SequenceEffectController( + [InfiniteEffectController(LinearEffectController(1))], + ), + failsAssert('Children controllers cannot be infinite'), + ); + }); + }); +} diff --git a/packages/flame/test/effects/effect_test.dart b/packages/flame/test/effects/effect_test.dart index df0a5b029..3abe9a1aa 100644 --- a/packages/flame/test/effects/effect_test.dart +++ b/packages/flame/test/effects/effect_test.dart @@ -1,7 +1,6 @@ import 'package:flame/src/components/component.dart'; +import 'package:flame/src/effects/controllers/effect_controller.dart'; import 'package:flame/src/effects/effect.dart'; -import 'package:flame/src/effects/effect_controller.dart'; -import 'package:flame/src/effects/standard_effect_controller.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -39,7 +38,7 @@ class _MyEffect extends Effect { void main() { group('Effect', () { test('pause & resume', () { - final effect = _MyEffect(StandardEffectController(duration: 10)); + final effect = _MyEffect(EffectController(duration: 10)); expect(effect.x, -1); expect(effect.isPaused, false); @@ -81,7 +80,7 @@ void main() { (game) { final obj = Component(); game.add(obj); - final effect = _MyEffect(StandardEffectController(duration: 1)); + final effect = _MyEffect(EffectController(duration: 1)); obj.add(effect); game.update(0); expect(obj.children.length, 1); @@ -102,7 +101,7 @@ void main() { (game) { final obj = Component(); game.add(obj); - final effect = _MyEffect(StandardEffectController(duration: 1)); + final effect = _MyEffect(EffectController(duration: 1)); effect.removeOnFinish = false; obj.add(effect); game.update(0); @@ -130,11 +129,9 @@ void main() { effect.reset(); expect(effect.x, -1); expect(effect.controller.completed, false); - expect(effect.controller.started, false); game.update(0.5); expect(effect.x, 0.5); - expect(effect.controller.started, true); expect(effect.controller.completed, false); // Now the effect completes once again, but still remains mounted @@ -149,7 +146,7 @@ void main() { test('onStart & onFinish', () { var nStarted = 0; var nFinished = 0; - final effect = _MyEffect(StandardEffectController(duration: 1)) + final effect = _MyEffect(EffectController(duration: 1)) ..onStartCallback = () { nStarted++; } @@ -158,19 +155,16 @@ void main() { }; effect.update(0); - expect(effect.controller.started, true); expect(effect.x, 0); expect(nStarted, 1); expect(nFinished, 0); effect.update(0.5); - expect(effect.controller.started, true); expect(effect.x, 0.5); expect(nStarted, 1); expect(nFinished, 0); effect.update(0.5); - expect(effect.controller.started, true); expect(effect.controller.completed, true); expect(effect.x, 1); expect(nStarted, 1); diff --git a/packages/flame/test/effects/move_effect_test.dart b/packages/flame/test/effects/move_effect_test.dart index 39b8e20d6..8efc5e7fe 100644 --- a/packages/flame/test/effects/move_effect_test.dart +++ b/packages/flame/test/effects/move_effect_test.dart @@ -3,8 +3,8 @@ import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; +import 'package:flame/src/effects/controllers/linear_effect_controller.dart'; import 'package:flame/src/effects/move_effect.dart'; -import 'package:flame/src/effects/simple_effect_controller.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -17,7 +17,7 @@ void main() { game.update(0); object.add( - MoveEffect.by(Vector2(5, -1), SimpleEffectController(duration: 1)), + MoveEffect.by(Vector2(5, -1), LinearEffectController(1)), ); game.update(0.5); expect(object.position.x, closeTo(3 + 2.5, 1e-15)); @@ -35,7 +35,7 @@ void main() { game.update(0); object.add( - MoveEffect.to(Vector2(5, -1), SimpleEffectController(duration: 1)), + MoveEffect.to(Vector2(5, -1), LinearEffectController(1)), ); game.update(0.5); expect(object.position.x, closeTo(3 * 0.5 + 5 * 0.5, 1e-15)); @@ -57,7 +57,7 @@ void main() { MoveEffect.along( Path() ..addOval(Rect.fromCircle(center: const Offset(6, 10), radius: 50)), - SimpleEffectController(duration: 1), + LinearEffectController(1), ), ); game.update(0); @@ -73,7 +73,7 @@ void main() { }); test('#along wrong arguments', () { - final controller = SimpleEffectController(); + final controller = LinearEffectController(0); expect( () => MoveEffect.along(Path(), controller), throwsArgumentError, diff --git a/packages/flame/test/effects/opacity_effect_test.dart b/packages/flame/test/effects/opacity_effect_test.dart index 0ee18c079..0d063eca2 100644 --- a/packages/flame/test/effects/opacity_effect_test.dart +++ b/packages/flame/test/effects/opacity_effect_test.dart @@ -2,8 +2,8 @@ import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; +import 'package:flame/src/effects/controllers/effect_controller.dart'; import 'package:flame/src/effects/opacity_effect.dart'; -import 'package:flame/src/effects/standard_effect_controller.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -19,7 +19,7 @@ void main() { component.setOpacity(0.2); component.add( - OpacityEffect.by(0.4, StandardEffectController(duration: 1)), + OpacityEffect.by(0.4, EffectController(duration: 1)), ); game.update(0); expect(component.getOpacity(), 0.2); @@ -41,7 +41,7 @@ void main() { component.setOpacity(0.2); component.add( - OpacityEffect.to(0.4, StandardEffectController(duration: 1)), + OpacityEffect.to(0.4, EffectController(duration: 1)), ); game.update(0); expect(component.getOpacity(), 0.2); @@ -66,7 +66,7 @@ void main() { const step = 10 * 1 / 255; final effect = OpacityEffect.by( -step, - StandardEffectController(duration: 1), + EffectController(duration: 1), ); component.add(effect..removeOnFinish = false); for (var i = 0; i < 5; i++) { @@ -89,7 +89,7 @@ void main() { final effect = OpacityEffect.to( 0.0, - StandardEffectController(duration: 1), + EffectController(duration: 1), ); component.add(effect..removeOnFinish = false); for (var i = 0; i < 5; i++) { @@ -111,12 +111,12 @@ void main() { game.update(0); component.add( - OpacityEffect.by(0.5, StandardEffectController(duration: 10)), + OpacityEffect.by(0.5, EffectController(duration: 10)), ); component.add( OpacityEffect.by( 0.5, - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, repeatCount: 5, @@ -149,7 +149,7 @@ void main() { game.ensureAdd(component); final effect = OpacityEffect.fadeOut( - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, infinite: true, diff --git a/packages/flame/test/effects/rotate_effect_test.dart b/packages/flame/test/effects/rotate_effect_test.dart index b1d6185f8..c4340104a 100644 --- a/packages/flame/test/effects/rotate_effect_test.dart +++ b/packages/flame/test/effects/rotate_effect_test.dart @@ -2,8 +2,8 @@ import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; +import 'package:flame/src/effects/controllers/effect_controller.dart'; import 'package:flame/src/effects/rotate_effect.dart'; -import 'package:flame/src/effects/standard_effect_controller.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -18,7 +18,7 @@ void main() { object.angle = 1; object.add( - RotateEffect.by(1, StandardEffectController(duration: 1)), + RotateEffect.by(1, EffectController(duration: 1)), ); game.update(0); expect(object.angle, 1); @@ -43,7 +43,7 @@ void main() { object.angle = 1; object.add( - RotateEffect.to(3, StandardEffectController(duration: 1)), + RotateEffect.to(3, EffectController(duration: 1)), ); game.update(0); expect(object.angle, 1); @@ -65,7 +65,7 @@ void main() { game.add(object); game.update(0); - final effect = RotateEffect.by(1, StandardEffectController(duration: 1)); + final effect = RotateEffect.by(1, EffectController(duration: 1)); object.add(effect..removeOnFinish = false); for (var i = 0; i < 5; i++) { expect(object.angle, i); @@ -83,7 +83,7 @@ void main() { game.add(object); game.update(0); - final effect = RotateEffect.to(1, StandardEffectController(duration: 1)); + final effect = RotateEffect.to(1, EffectController(duration: 1)); object.add(effect..removeOnFinish = false); for (var i = 0; i < 5; i++) { object.angle = 1 + 4.0 * i; @@ -102,12 +102,12 @@ void main() { game.update(0); object.add( - RotateEffect.by(5, StandardEffectController(duration: 10)), + RotateEffect.by(5, EffectController(duration: 10)), ); object.add( RotateEffect.by( 0.5, - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, repeatCount: 5, @@ -134,7 +134,7 @@ void main() { final effect = RotateEffect.by( 1.0, - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, infinite: true, diff --git a/packages/flame/test/effects/scale_effect_test.dart b/packages/flame/test/effects/scale_effect_test.dart index cdc7961a7..a39b6ba81 100644 --- a/packages/flame/test/effects/scale_effect_test.dart +++ b/packages/flame/test/effects/scale_effect_test.dart @@ -2,8 +2,8 @@ import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; +import 'package:flame/src/effects/controllers/effect_controller.dart'; import 'package:flame/src/effects/scale_effect.dart'; -import 'package:flame/src/effects/standard_effect_controller.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -15,7 +15,7 @@ void main() { component.scale = Vector2.all(1.0); component.add( - ScaleEffect.by(Vector2.all(1.0), StandardEffectController(duration: 1)), + ScaleEffect.by(Vector2.all(1.0), EffectController(duration: 1)), ); game.update(0); expectVector2(component.scale, Vector2.all(1.0)); @@ -37,7 +37,7 @@ void main() { component.scale = Vector2.all(1.0); component.add( - ScaleEffect.to(Vector2.all(3.0), StandardEffectController(duration: 1)), + ScaleEffect.to(Vector2.all(3.0), EffectController(duration: 1)), ); game.update(0); expectVector2(component.scale, Vector2.all(1.0)); @@ -59,7 +59,7 @@ void main() { final effect = ScaleEffect.by( Vector2.all(1.0), - StandardEffectController(duration: 1), + EffectController(duration: 1), ); component.add(effect..removeOnFinish = false); final expectedScale = Vector2.all(1.0); @@ -80,7 +80,7 @@ void main() { final effect = ScaleEffect.to( Vector2.all(1.0), - StandardEffectController(duration: 1), + EffectController(duration: 1), ); component.add(effect..removeOnFinish = false); for (var i = 0; i < 5; i++) { @@ -98,12 +98,12 @@ void main() { game.ensureAdd(component); component.add( - ScaleEffect.by(Vector2.all(5), StandardEffectController(duration: 10)), + ScaleEffect.by(Vector2.all(5), EffectController(duration: 10)), ); component.add( ScaleEffect.by( Vector2.all(0.5), - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, repeatCount: 5, @@ -137,7 +137,7 @@ void main() { final effect = ScaleEffect.by( Vector2.all(1.0), - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, infinite: true, diff --git a/packages/flame/test/effects/simple_effect_controller_test.dart b/packages/flame/test/effects/simple_effect_controller_test.dart deleted file mode 100644 index 8c292f266..000000000 --- a/packages/flame/test/effects/simple_effect_controller_test.dart +++ /dev/null @@ -1,129 +0,0 @@ -import 'package:flame/src/effects/simple_effect_controller.dart'; -import 'package:flutter_test/flutter_test.dart'; - -void main() { - group('SimpleEffectController', () { - test('default', () { - final ec = SimpleEffectController(); - expect(ec.duration, 0); - expect(ec.delay, 0); - expect(ec.isInfinite, false); - expect(ec.started, true); - expect(ec.completed, true); - expect(ec.progress, 1); - }); - - test('simple with duration', () { - final ec = SimpleEffectController(duration: 1); - expect(ec.delay, 0); - expect(ec.duration, 1); - expect(ec.progress, 0); - expect(ec.started, true); - expect(ec.completed, false); - expect(ec.isInfinite, false); - - ec.update(0.5); - expect(ec.progress, 0.5); - expect(ec.started, true); - expect(ec.completed, false); - - ec.update(0.5); - expect(ec.progress, 1); - expect(ec.started, true); - expect(ec.completed, true); - - ec.update(0.00001); - expect(ec.progress, 1); - expect(ec.started, true); - expect(ec.completed, true); - }); - - test('simple with delay', () { - final ec = SimpleEffectController(delay: 1); - expect(ec.isInfinite, false); - expect(ec.started, false); - expect(ec.completed, false); - expect(ec.progress, 0); - expect(ec.delay, 1); - expect(ec.duration, 0); - - ec.update(0.5); - expect(ec.started, false); - expect(ec.completed, false); - expect(ec.progress, 0); - - ec.update(0.5); - expect(ec.started, true); - expect(ec.completed, true); - expect(ec.progress, 1); - }); - - test('duration + delay', () { - final ec = SimpleEffectController(duration: 1, delay: 2); - expect(ec.isInfinite, false); - expect(ec.started, false); - expect(ec.completed, false); - expect(ec.duration, 1); - expect(ec.delay, 2); - expect(ec.progress, 0); - - ec.update(0.5); - expect(ec.started, false); - expect(ec.progress, 0); - - ec.update(0.5); - expect(ec.started, false); - expect(ec.progress, 0); - - ec.update(1); - expect(ec.started, true); - expect(ec.completed, false); - expect(ec.progress, 0); - - ec.update(0.5); - expect(ec.started, true); - expect(ec.completed, false); - expect(ec.progress, 0.5); - - ec.update(0.5); - expect(ec.started, true); - expect(ec.completed, true); - expect(ec.progress, 1); - }); - - test('reset', () { - final ec = SimpleEffectController(); - ec.reset(); - expect(ec.started, true); - expect(ec.completed, true); - expect(ec.progress, 1); - }); - - test('reset 2', () { - final ec = SimpleEffectController(duration: 2, delay: 1); - ec.update(3); - expect(ec.completed, true); - expect(ec.progress, 1); - - ec.reset(); - expect(ec.started, false); - expect(ec.completed, false); - expect(ec.progress, 0); - - ec.update(1); - expect(ec.started, true); - expect(ec.completed, false); - expect(ec.progress, 0); - - ec.update(1); - expect(ec.started, true); - expect(ec.completed, false); - expect(ec.progress, closeTo(0.5, 1e-15)); - - ec.update(1); - expect(ec.started, true); - expect(ec.completed, true); - expect(ec.progress, 1); - }); - }); -} diff --git a/packages/flame/test/effects/size_effect_test.dart b/packages/flame/test/effects/size_effect_test.dart index 6f2d8b738..9ce2e94b7 100644 --- a/packages/flame/test/effects/size_effect_test.dart +++ b/packages/flame/test/effects/size_effect_test.dart @@ -2,8 +2,8 @@ import 'dart:math'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; +import 'package:flame/src/effects/controllers/effect_controller.dart'; import 'package:flame/src/effects/size_effect.dart'; -import 'package:flame/src/effects/standard_effect_controller.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -15,7 +15,7 @@ void main() { component.size = Vector2.all(1.0); component.add( - SizeEffect.by(Vector2.all(1.0), StandardEffectController(duration: 1)), + SizeEffect.by(Vector2.all(1.0), EffectController(duration: 1)), ); game.update(0); expectVector2(component.size, Vector2.all(1.0)); @@ -37,7 +37,7 @@ void main() { component.size = Vector2.all(1.0); component.add( - SizeEffect.to(Vector2.all(3.0), StandardEffectController(duration: 1)), + SizeEffect.to(Vector2.all(3.0), EffectController(duration: 1)), ); game.update(0); expectVector2(component.size, Vector2.all(1.0)); @@ -59,7 +59,7 @@ void main() { final effect = SizeEffect.by( Vector2.all(1.0), - StandardEffectController(duration: 1), + EffectController(duration: 1), ); component.add(effect..removeOnFinish = false); final expectedSize = Vector2.zero(); @@ -80,7 +80,7 @@ void main() { final effect = SizeEffect.to( Vector2.all(1.0), - StandardEffectController(duration: 1), + EffectController(duration: 1), ); component.add(effect..removeOnFinish = false); for (var i = 0; i < 5; i++) { @@ -98,12 +98,12 @@ void main() { game.ensureAdd(component); component.add( - SizeEffect.by(Vector2.all(5), StandardEffectController(duration: 10)), + SizeEffect.by(Vector2.all(5), EffectController(duration: 10)), ); component.add( SizeEffect.by( Vector2.all(0.5), - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, repeatCount: 5, @@ -137,7 +137,7 @@ void main() { final effect = SizeEffect.by( Vector2.all(1.0), - StandardEffectController( + EffectController( duration: 1, reverseDuration: 1, infinite: true, diff --git a/packages/flame/test/effects/transform2d_effect_test.dart b/packages/flame/test/effects/transform2d_effect_test.dart index 4469759f4..f55731c54 100644 --- a/packages/flame/test/effects/transform2d_effect_test.dart +++ b/packages/flame/test/effects/transform2d_effect_test.dart @@ -1,6 +1,5 @@ import 'package:flame/src/components/position_component.dart'; -import 'package:flame/src/effects/effect_controller.dart'; -import 'package:flame/src/effects/standard_effect_controller.dart'; +import 'package:flame/src/effects/controllers/effect_controller.dart'; import 'package:flame/src/effects/transform2d_effect.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -18,12 +17,12 @@ void main() { game.add(component); game.update(0); - final effect = _MyEffect(StandardEffectController(duration: 1)); + final effect = _MyEffect(EffectController(duration: 1)); component.add(effect); game.update(0); expect(effect.transform, component.transform); - final effect2 = _MyEffect(StandardEffectController(duration: 1)); + final effect2 = _MyEffect(EffectController(duration: 1)); expect( () async => game.add(effect2), throwsA(isA()), diff --git a/packages/flame_test/lib/flame_test.dart b/packages/flame_test/lib/flame_test.dart index 40c741a46..be76a0e5a 100644 --- a/packages/flame_test/lib/flame_test.dart +++ b/packages/flame_test/lib/flame_test.dart @@ -1,5 +1,6 @@ export 'src/expect_double.dart'; export 'src/expect_vector2.dart'; +export 'src/fails_assert.dart'; export 'src/flame_test.dart'; export 'src/mock_gesture_events.dart'; export 'src/mock_image.dart'; diff --git a/packages/flame_test/lib/src/fails_assert.dart b/packages/flame_test/lib/src/fails_assert.dart new file mode 100644 index 000000000..17d0c7d4b --- /dev/null +++ b/packages/flame_test/lib/src/fails_assert.dart @@ -0,0 +1,22 @@ +import 'package:test/test.dart'; + +/// Matcher that can be used in a test that expects an assertion error. +/// +/// This is similar to standard `throwsAssertionError` matcher, but also +/// allows an optional message string to verify that the assertion has the +/// expected message. +/// +/// For example: +/// ```dart +/// expect( +/// () => PositionComponent(size: Vector2.all(-1)), +/// failsAssert('size of a PositionComponent cannot be negative'), +/// ) +/// ``` +Matcher failsAssert([String? message]) { + var typeMatcher = isA(); + if (message != null) { + typeMatcher = typeMatcher.having((e) => e.message, 'message', message); + } + return throwsA(typeMatcher); +} diff --git a/packages/flame_test/test/expect_fails_assert.dart b/packages/flame_test/test/expect_fails_assert.dart new file mode 100644 index 000000000..9054024da --- /dev/null +++ b/packages/flame_test/test/expect_fails_assert.dart @@ -0,0 +1,24 @@ +import 'package:flame_test/flame_test.dart'; +import 'package:test/test.dart'; + +void main() { + group('failsAssert', () { + test('without message', () { + expect( + () { + assert(2 + 2 == 5); + }, + failsAssert(), + ); + }); + + test('with message', () { + expect( + () { + assert(2 + 2 == 5, 'Basic arithmetic error'); + }, + failsAssert('Basic arithmetic error'), + ); + }); + }); +}