Effect controllers restructuring (#1134)

* Update effectController

* move effect controllers into the controllers/ directory

* Add .forward property to EffectController

* SimpleEffectController supports reverse time

* Fixing some compile errors

* rename SimpleEffectController -> LinearEffectController

* minor cleanup

* DurationEffectController and PauseEffectController

* ReverseLinearEffectController

* CurvedEffectController and its reverse

* InfiniteEffectController

* Added EffectController.recede()

* Add EffectController.update()

* Add InfiniteEffectController'

* RepeatedEffectController

* SequenceEffectController

* DelayedEffectController

* Restore the [EffectController.started] property

* minor

* Rename reset() -> setToStart()

* time direction is now managed from the Effect class

* StandardEffectController replaced with function standardController()

* update some doc-comments

* flutter analyze

* flutter format

* fix some tests

* more test fixes

* fix remaining tests

* format

* rename local variable

* minor simplification

* Expand docs in PauseEffectController

* added tests

* Curved controller test

* fix errors

* formatting

* added more tests

* format

* fix RepeatedEffectController

* more tests

* format

* changelog

* increase tolerance

* Replaced standardController with factory EffectController constructor

* Added parameter EffectController({alternate=false})

* Added default for curve= parameter

* rename

* rename tests

* added more exports

* rename tests

* rename src/effects2

Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
This commit is contained in:
Pasha Stetsenko
2021-12-04 07:58:42 -08:00
committed by GitHub
parent 4b40479563
commit bfcda073bb
51 changed files with 1470 additions and 658 deletions

View File

@ -188,7 +188,7 @@ class Rock extends SpriteComponent
add(
ScaleEffect.by(
Vector2.all(10),
StandardEffectController(duration: 0.3),
EffectController(duration: 0.3),
),
);
return true;

View File

@ -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,

View File

@ -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,

View File

@ -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)));
}
}
}

View File

@ -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,

View File

@ -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,
),

View File

@ -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),
),
);
}

View File

@ -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),
),
),
);

View File

@ -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

View File

@ -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';

View File

@ -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].
///

View File

@ -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);
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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();
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<EffectController> 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<EffectController> 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());
}
}

View File

@ -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();

View File

@ -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);
}

View File

@ -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.

View File

@ -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.
///

View File

@ -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) {

View File

@ -1,4 +1,4 @@
import 'effect_controller.dart';
import 'controllers/effect_controller.dart';
import 'transform2d_effect.dart';
/// Rotate a component around its anchor.

View File

@ -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.

View File

@ -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;
}
}

View File

@ -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.
///

View File

@ -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,
}

View File

@ -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.
///

View File

@ -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 = <Curve>[
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<AssertionError>()),
);
expect(
() => CurvedEffectController(-1, Curves.linear),
throwsA(isA<AssertionError>()),
);
});
});
}

View File

@ -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'),
);
});
});
}

View File

@ -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);
});
});

View File

@ -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);
});
});
}

View File

@ -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);
});
});
}

View File

@ -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'),
);
});
});
}

View File

@ -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'),
);
});
});
}

View File

@ -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);

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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,

View File

@ -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);
});
});
}

View File

@ -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,

View File

@ -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<UnsupportedError>()),

View File

@ -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';

View File

@ -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<AssertionError>();
if (message != null) {
typeMatcher = typeMatcher.having((e) => e.message, 'message', message);
}
return throwsA(typeMatcher);
}

View File

@ -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'),
);
});
});
}