diff --git a/doc/examples/effects/combined_effects/lib/main.dart b/doc/examples/effects/combined_effects/lib/main.dart index 3717350da..8af16bc0b 100644 --- a/doc/examples/effects/combined_effects/lib/main.dart +++ b/doc/examples/effects/combined_effects/lib/main.dart @@ -3,13 +3,12 @@ import 'package:flame/effects/move_effect.dart'; import 'package:flame/effects/scale_effect.dart'; import 'package:flame/effects/rotate_effect.dart'; import 'package:flame/gestures.dart'; +import 'package:flame/extensions/offset.dart'; import 'package:flame/extensions/vector2.dart'; import 'package:flame/flame.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; -import 'dart:math'; - import './square.dart'; void main() async { @@ -32,10 +31,8 @@ class MyGame extends BaseGame with TapDetector { @override void onTapUp(TapUpDetails details) { - final dx = details.localPosition.dx; - final dy = details.localPosition.dy; greenSquare.clearEffects(); - final Vector2 currentTap = Vector2(dx, dy); + final Vector2 currentTap = details.localPosition.toVector2(); final move = MoveEffect( path: [ @@ -43,23 +40,22 @@ class MyGame extends BaseGame with TapDetector { currentTap - Vector2(50, 20), currentTap + Vector2.all(30), ], - duration: 5.0, - curve: Curves.linear, + duration: 4.0, + curve: Curves.bounceInOut, isInfinite: false, isAlternating: false, - onComplete: () => print(DateTime.now()), ); final scale = ScaleEffect( - size: Vector2(dx, dy), - speed: 250.0, + size: currentTap, + speed: 200.0, curve: Curves.linear, isInfinite: false, isAlternating: false, ); final rotate = RotateEffect( - angle: (dx + dy) % pi, + angle: currentTap.angleTo(Vector2.all(100)), duration: 3, curve: Curves.decelerate, isInfinite: false, diff --git a/doc/examples/effects/sequence_effect/lib/main.dart b/doc/examples/effects/sequence_effect/lib/main.dart index 4445f83d4..fc8a961de 100644 --- a/doc/examples/effects/sequence_effect/lib/main.dart +++ b/doc/examples/effects/sequence_effect/lib/main.dart @@ -4,13 +4,12 @@ import 'package:flame/effects/scale_effect.dart'; import 'package:flame/effects/rotate_effect.dart'; import 'package:flame/effects/sequence_effect.dart'; import 'package:flame/gestures.dart'; +import 'package:flame/extensions/offset.dart'; import 'package:flame/extensions/vector2.dart'; import 'package:flame/flame.dart'; import 'package:flame/game.dart'; import 'package:flutter/material.dart'; -import 'dart:math'; - import './square.dart'; void main() async { @@ -30,23 +29,22 @@ class MyGame extends BaseGame with TapDetector { @override void onTapUp(TapUpDetails details) { - final dx = details.localPosition.dx; - final dy = details.localPosition.dy; + final Vector2 currentTap = details.localPosition.toVector2(); greenSquare.clearEffects(); final move1 = MoveEffect( - path: [Vector2(dx, dy)], + path: [currentTap], speed: 250.0, curve: Curves.bounceInOut, isInfinite: false, - isAlternating: false, + isAlternating: true, ); final move2 = MoveEffect( path: [ - Vector2(dx, dy + 50), - Vector2(dx - 50, dy - 50), - Vector2(dx + 50, dy), + currentTap + Vector2(0, 50), + currentTap + Vector2(-50, -50), + currentTap + Vector2(50, 0), ], speed: 150.0, curve: Curves.easeIn, @@ -55,7 +53,7 @@ class MyGame extends BaseGame with TapDetector { ); final scale = ScaleEffect( - size: Vector2(dx, dy), + size: currentTap, speed: 100.0, curve: Curves.easeInCubic, isInfinite: false, @@ -63,12 +61,11 @@ class MyGame extends BaseGame with TapDetector { ); final rotate = RotateEffect( - angle: (dx + dy) % pi, + angle: currentTap.angleTo(Vector2.all(100)), duration: 0.8, curve: Curves.decelerate, isInfinite: false, isAlternating: false, - onComplete: () => print("rotation complete"), ); final combination = CombinedEffect( @@ -79,11 +76,11 @@ class MyGame extends BaseGame with TapDetector { ); final sequence = SequenceEffect( - effects: [move1, scale, combination], + effects: [move1, scale, move2], isInfinite: false, isAlternating: true, ); sequence.onComplete = () => print("sequence complete"); - greenSquare.addEffect(sequence); + greenSquare.addEffect(combination); } } diff --git a/lib/effects/combined_effect.dart b/lib/effects/combined_effect.dart index f8aff3fad..0bd6c6581 100644 --- a/lib/effects/combined_effect.dart +++ b/lib/effects/combined_effect.dart @@ -28,19 +28,21 @@ class CombinedEffect extends PositionComponentEffect { } @override - void initialize(PositionComponent _comp) { - super.initialize(_comp); + void initialize(PositionComponent component) { + super.initialize(component); effects.forEach((effect) { - effect.initialize(_comp); - final isSameSize = effect.endSize == _comp.size; - final isSamePosition = effect.endPosition == _comp.position; - final isSameAngle = effect.endAngle == _comp.angle; - endSize = isSameSize ? endSize : effect.endSize; - endPosition = isSamePosition ? endPosition : effect.endPosition; - endAngle = isSameAngle ? endAngle : effect.endAngle; + effect.initialize(component); + endPosition = effect.endPosition; + endAngle = effect.endAngle; + endSize = effect.endSize; peakTime = max(peakTime ?? 0, effect.iterationTime + offset * effects.indexOf(effect)); }); + if (isAlternating) { + endPosition = originalPosition; + endAngle = originalAngle; + endSize = originalSize; + } } @override @@ -57,13 +59,11 @@ class CombinedEffect extends PositionComponentEffect { @override void reset() { super.reset(); - if (component != null) { - component.position = originalPosition; - component.angle = originalAngle; - component.size = originalSize; - initialize(component); - } + component?.position = originalPosition; + component?.angle = originalAngle; + component?.size = originalSize; effects.forEach((effect) => effect.reset()); + initialize(component); } @override diff --git a/lib/effects/effects.dart b/lib/effects/effects.dart index 89a3bba01..52e15c76b 100644 --- a/lib/effects/effects.dart +++ b/lib/effects/effects.dart @@ -63,14 +63,17 @@ abstract class ComponentEffect { } if (!hasFinished()) { currentTime += (dt + driftTime) * curveDirection; - currentTime = (currentTime * 10000000).floor() / 10000000; percentage = (currentTime / peakTime).clamp(0.0, 1.0).toDouble(); curveProgress = curve.transform(percentage); _updateDriftTime(); currentTime = currentTime.clamp(0.0, peakTime).toDouble(); if (hasFinished()) { onComplete?.call(); - _setComponentToEndState(); + // Set the component to the exact end state if this component is the + // root component (not inside another component). + if (component.effects.contains(this)) { + _setComponentToEndState(); + } } } } @@ -180,7 +183,7 @@ abstract class SimplePositionComponentEffect extends PositionComponentEffect { bool isRelative = false, void Function() onComplete, }) : assert( - duration != null || speed != null, + (duration != null) ^ (speed != null), "Either speed or duration necessary", ), super( diff --git a/lib/effects/move_effect.dart b/lib/effects/move_effect.dart index 17c022e5c..a6624ad34 100644 --- a/lib/effects/move_effect.dart +++ b/lib/effects/move_effect.dart @@ -24,6 +24,7 @@ class MoveEffect extends SimplePositionComponentEffect { List _percentagePath; Vector2 _startPosition; + /// Duration or speed needs to be defined MoveEffect({ @required this.path, double duration, @@ -34,7 +35,7 @@ class MoveEffect extends SimplePositionComponentEffect { bool isRelative = false, void Function() onComplete, }) : assert( - duration != null || speed != null, + (duration != null) ^ (speed != null), "Either speed or duration necessary", ), super( @@ -109,19 +110,16 @@ class MoveEffect extends SimplePositionComponentEffect { @override void update(double dt) { super.update(dt); - if (!hasFinished()) { - _currentSubPath ??= _percentagePath.first; - if (!curveDirection.isNegative && _currentSubPath.endAt < curveProgress || - curveDirection.isNegative && - _currentSubPath.startAt > curveProgress) { - _currentSubPath = - _percentagePath.firstWhere((v) => v.endAt >= curveProgress); - } - final double lastEndAt = _currentSubPath.startAt; - final double localPercentage = - (curveProgress - lastEndAt) / (_currentSubPath.endAt - lastEndAt); - component.position = _currentSubPath.previous + - ((_currentSubPath.v - _currentSubPath.previous) * localPercentage); + _currentSubPath ??= _percentagePath.first; + if (!curveDirection.isNegative && _currentSubPath.endAt < curveProgress || + curveDirection.isNegative && _currentSubPath.startAt > curveProgress) { + _currentSubPath = + _percentagePath.firstWhere((v) => v.endAt >= curveProgress); } + final double lastEndAt = _currentSubPath.startAt; + final double localPercentage = + (curveProgress - lastEndAt) / (_currentSubPath.endAt - lastEndAt); + component.position = _currentSubPath.previous + + ((_currentSubPath.v - _currentSubPath.previous) * localPercentage); } } diff --git a/lib/effects/rotate_effect.dart b/lib/effects/rotate_effect.dart index 18811619a..536c27ed1 100644 --- a/lib/effects/rotate_effect.dart +++ b/lib/effects/rotate_effect.dart @@ -8,6 +8,7 @@ class RotateEffect extends SimplePositionComponentEffect { double _startAngle; double _delta; + /// Duration or speed needs to be defined RotateEffect({ @required this.angle, // As many radians as you want to rotate double duration, // How long it should take for completion @@ -18,7 +19,7 @@ class RotateEffect extends SimplePositionComponentEffect { bool isRelative = false, void Function() onComplete, }) : assert( - duration != null || speed != null, + (duration != null) ^ (speed != null), "Either speed or duration necessary", ), super( @@ -47,8 +48,6 @@ class RotateEffect extends SimplePositionComponentEffect { @override void update(double dt) { super.update(dt); - if (!hasFinished()) { - component.angle = _startAngle + _delta * curveProgress; - } + component.angle = _startAngle + _delta * curveProgress; } } diff --git a/lib/effects/scale_effect.dart b/lib/effects/scale_effect.dart index 054445efb..fb12ee54f 100644 --- a/lib/effects/scale_effect.dart +++ b/lib/effects/scale_effect.dart @@ -9,6 +9,7 @@ class ScaleEffect extends SimplePositionComponentEffect { Vector2 _startSize; Vector2 _delta; + /// Duration or speed needs to be defined ScaleEffect({ @required this.size, double duration, // How long it should take for completion @@ -48,8 +49,6 @@ class ScaleEffect extends SimplePositionComponentEffect { @override void update(double dt) { super.update(dt); - if (!hasFinished()) { - component.size = _startSize + _delta * curveProgress; - } + component.size = _startSize + _delta * curveProgress; } } diff --git a/lib/effects/sequence_effect.dart b/lib/effects/sequence_effect.dart index 387bb8397..41d017423 100644 --- a/lib/effects/sequence_effect.dart +++ b/lib/effects/sequence_effect.dart @@ -27,24 +27,30 @@ class SequenceEffect extends PositionComponentEffect { } @override - void initialize(PositionComponent _comp) { - super.initialize(_comp); + void initialize(PositionComponent component) { + super.initialize(component); _currentIndex = 0; _driftModifier = 0.0; + effects.forEach((effect) { effect.reset(); - _comp.size = endSize; - _comp.position = endPosition; - _comp.angle = endAngle; - effect.initialize(_comp); - endSize = effect.endSize; + component.position = endPosition; + component.angle = endAngle; + component.size = endSize; + effect.initialize(component); endPosition = effect.endPosition; endAngle = effect.endAngle; + endSize = effect.endSize; }); peakTime = effects.fold( 0, - (time, effect) => time + effect.iterationTime, + (time, effect) => time + effect.peakTime, ); + if (isAlternating) { + endPosition = originalPosition; + endAngle = originalAngle; + endSize = originalSize; + } component.position = originalPosition; component.angle = originalAngle; component.size = originalSize; @@ -54,9 +60,6 @@ class SequenceEffect extends PositionComponentEffect { @override void update(double dt) { - if (hasFinished()) { - return; - } super.update(dt); // If the last effect's time to completion overshot its total time, add that @@ -96,12 +99,10 @@ class SequenceEffect extends PositionComponentEffect { @override void reset() { super.reset(); + component?.position = originalPosition; + component?.angle = originalAngle; + component?.size = originalSize; effects.forEach((e) => e.reset()); - if (component != null) { - component.position = originalPosition; - component.angle = originalAngle; - component.size = originalSize; - initialize(component); - } + initialize(component); } } diff --git a/test/effects/effect_test_utils.dart b/test/effects/effect_test_utils.dart index db3f4b1de..a401d656f 100644 --- a/test/effects/effect_test_utils.dart +++ b/test/effects/effect_test_utils.dart @@ -41,6 +41,16 @@ void effectTest( game.update(stepDelta); timeLeft -= stepDelta; } + + // To account for float number operations making effects not finish + const double epsilon = 0.000001; + if (effect.percentage < epsilon || 1.0 - effect.percentage < epsilon) { + game.update(epsilon); + } + + expect(effect.hasFinished(), shouldFinish, reason: "Effect shouldFinish"); + expect(callback.isCalled, shouldFinish, reason: "Callback was treated wrong"); + if (!shouldFinish) { const double floatRange = 0.001; bool acceptableVector(Vector2 vector, Vector2 expectedVector) { @@ -65,7 +75,6 @@ void effectTest( "Size is not correct (had: ${component.size} should be: $expectedSize)", ); } else { - game.update(0.1); expect( component.position, expectedPosition, @@ -82,8 +91,6 @@ void effectTest( reason: "Size is not exactly correct", ); } - expect(effect.hasFinished(), shouldFinish, reason: "Effect shouldFinish"); - expect(callback.isCalled, shouldFinish, reason: "Callback was treated wrong"); game.update(0.0); // Since effects are removed before they are updated expect(component.effects.isEmpty, shouldFinish); } diff --git a/test/effects/move_effect_test.dart b/test/effects/move_effect_test.dart index f79c9a0ed..00d6ebe2e 100644 --- a/test/effects/move_effect_test.dart +++ b/test/effects/move_effect_test.dart @@ -13,7 +13,7 @@ void main() { final List path = List.generate(3, (i) => randomVector2()); TestComponent component() => TestComponent(position: randomVector2()); - MoveEffect effect(bool isInfinite, bool isAlternating) { + MoveEffect effect({bool isInfinite = false, bool isAlternating = false}) { return MoveEffect( path: path, duration: 1 + random.nextInt(100).toDouble(), @@ -26,7 +26,7 @@ void main() { effectTest( tester, component(), - effect(false, false), + effect(), expectedPosition: path.last, ); }); @@ -37,7 +37,7 @@ void main() { effectTest( tester, component(), - effect(false, false), + effect(), expectedPosition: path.last, iterations: 1.5, ); @@ -49,7 +49,7 @@ void main() { effectTest( tester, positionComponent, - effect(false, true), + effect(isAlternating: true), expectedPosition: positionComponent.position.clone(), ); }); @@ -61,7 +61,7 @@ void main() { effectTest( tester, positionComponent, - effect(true, true), + effect(isInfinite: true, isAlternating: true), expectedPosition: positionComponent.position.clone(), iterations: 1.0, shouldFinish: false, @@ -74,7 +74,7 @@ void main() { effectTest( tester, positionComponent, - effect(false, true), + effect(isAlternating: true), expectedPosition: path.last, shouldFinish: false, iterations: 0.5, @@ -86,7 +86,7 @@ void main() { effectTest( tester, positionComponent, - effect(true, false), + effect(isInfinite: true), expectedPosition: path.last, iterations: 3.0, shouldFinish: false, diff --git a/test/effects/rotate_effect_test.dart b/test/effects/rotate_effect_test.dart index ff196b562..b79ce31a2 100644 --- a/test/effects/rotate_effect_test.dart +++ b/test/effects/rotate_effect_test.dart @@ -8,7 +8,7 @@ void main() { const double angleArgument = 6.0; TestComponent component() => TestComponent(angle: 0.5); - RotateEffect effect(bool isInfinite, bool isAlternating) { + RotateEffect effect({bool isInfinite = false, bool isAlternating = false}) { return RotateEffect( angle: angleArgument, duration: 1 + random.nextInt(100).toDouble(), @@ -21,7 +21,7 @@ void main() { effectTest( tester, component(), - effect(false, false), + effect(), expectedAngle: angleArgument, ); }); @@ -32,7 +32,7 @@ void main() { effectTest( tester, component(), - effect(false, false), + effect(), expectedAngle: angleArgument, iterations: 1.5, ); @@ -44,7 +44,7 @@ void main() { effectTest( tester, positionComponent, - effect(false, true), + effect(isAlternating: true), expectedAngle: positionComponent.angle, ); }); @@ -56,7 +56,7 @@ void main() { effectTest( tester, positionComponent, - effect(true, true), + effect(isInfinite: true, isAlternating: true), expectedAngle: positionComponent.angle, iterations: 1.0, shouldFinish: false, @@ -69,7 +69,7 @@ void main() { effectTest( tester, positionComponent, - effect(false, true), + effect(isAlternating: true), expectedAngle: angleArgument, shouldFinish: false, iterations: 0.5, @@ -81,7 +81,7 @@ void main() { effectTest( tester, positionComponent, - effect(true, false), + effect(isInfinite: true), expectedAngle: angleArgument, iterations: 3.0, shouldFinish: false, diff --git a/test/effects/scale_effect_test.dart b/test/effects/scale_effect_test.dart index 26a46c352..a549d7d62 100644 --- a/test/effects/scale_effect_test.dart +++ b/test/effects/scale_effect_test.dart @@ -13,7 +13,7 @@ void main() { final Vector2 argumentSize = randomVector2(); TestComponent component() => TestComponent(size: randomVector2()); - ScaleEffect effect(bool isInfinite, bool isAlternating) { + ScaleEffect effect({bool isInfinite = false, bool isAlternating = false}) { return ScaleEffect( size: argumentSize, duration: 1 + random.nextInt(100).toDouble(), @@ -26,7 +26,7 @@ void main() { effectTest( tester, component(), - effect(false, false), + effect(), expectedSize: argumentSize, ); }); @@ -37,7 +37,7 @@ void main() { effectTest( tester, component(), - effect(false, false), + effect(), expectedSize: argumentSize, iterations: 1.5, ); @@ -49,7 +49,7 @@ void main() { effectTest( tester, positionComponent, - effect(false, true), + effect(isAlternating: true), expectedSize: positionComponent.size.clone(), ); }); @@ -61,7 +61,7 @@ void main() { effectTest( tester, positionComponent, - effect(true, true), + effect(isInfinite: true, isAlternating: true), expectedSize: positionComponent.size.clone(), iterations: 1.0, shouldFinish: false, @@ -74,7 +74,7 @@ void main() { effectTest( tester, positionComponent, - effect(false, true), + effect(isAlternating: true), expectedSize: argumentSize, shouldFinish: false, iterations: 0.5, @@ -86,7 +86,7 @@ void main() { effectTest( tester, positionComponent, - effect(true, false), + effect(isInfinite: true), expectedSize: argumentSize, iterations: 3.0, shouldFinish: false,