diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d5405beb..7595fc744 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,10 @@ - Removing all deprecated methods - Rename `resize` method on components to `onGameResize` - Make `Resizable` have a `gameSize` property instead of `size` + - Fix bug with CombinedEffect inside SequenceEffect + - Fix wrong end angle for relative rotational effects + - Use a list of Vector2 for Move effect to open up for more advanced move effects + - Generalize effects api to include all components ## [next] - Fix spriteAsWidget deprecation message diff --git a/doc/effects.md b/doc/effects.md index 9cfea0d89..92f2ac733 100644 --- a/doc/effects.md +++ b/doc/effects.md @@ -1,7 +1,9 @@ # Effects -An effect can be applied to any `PositionComponent`, there are currently two effects that you can use and that is the MoveEffect and the ScaleEffect. +An effect can be applied to any `Component` that the effect supports. -If you want to create an effect that only runs once only specify the required parameters of your wanted Effect class. +At the moment there are only `PositionComponentEffect`s, which are applied to PositionComponent`'s, which are presented below. + +If you want to create an effect for another component just extend the `ComponentEffect` class and add your created effect to the component by calling `component.addEffect(yourEffect)`. ## More advanced effects Then there are two optional boolean parameters called `isInfinite` and `isAlternating`, by combining them you can get different effects. @@ -20,7 +22,7 @@ When an effect is completed the callback `onComplete` will be called, it can be ## MoveEffect -Applied to `PositionComponent`s, this effect can be used to move the component to a new position, using an [animation curve](https://api.flutter.dev/flutter/animation/Curves-class.html). +Applied to `PositionComponent`s, this effect can be used to move the component to new positions, using an [animation curve](https://api.flutter.dev/flutter/animation/Curves-class.html). Usage example: ```dart @@ -28,12 +30,18 @@ import 'package:flame/effects/effects.dart'; // Square is a PositionComponent square.addEffect(MoveEffect( - destination: Position(200, 200), + path: [Vector2(200, 200), Vector2(200, 100), Vector(0, 50)], speed: 250.0, curve: Curves.bounceInOut, + isRelative: false, )); ``` +If you want the positions in the path list to be relative to the components last position, and not absolute values on the screen, then you can set `isRelative = true`. +When you use that, the next position in the list will be relative to the previous position in the list, or if it is the first element of the list it is relative to the components position. +So if you have a component which is positioned at `Vector2(100, 100)` and you use `isRelative = true` with the following path list `path: [Vector(20, 0), Vector(0, 50)]`, then the component will +first move to `(120, 0)` and then to `(120, 100)`. + ## ScaleEffect Applied to `PositionComponent`s, this effect can be used to change the width and height of the component, using an [animation curve](https://api.flutter.dev/flutter/animation/Curves-class.html). diff --git a/doc/examples/effects/combined_effects/lib/main.dart b/doc/examples/effects/combined_effects/lib/main.dart index c1c35a307..1480ffa12 100644 --- a/doc/examples/effects/combined_effects/lib/main.dart +++ b/doc/examples/effects/combined_effects/lib/main.dart @@ -37,7 +37,7 @@ class MyGame extends BaseGame with TapDetector { greenSquare.clearEffects(); final move = MoveEffect( - destination: Vector2(dx, dy), + path: [Vector2(dx, dy)], speed: 250.0, curve: Curves.linear, isInfinite: false, diff --git a/doc/examples/effects/infinite_effects/lib/main.dart b/doc/examples/effects/infinite_effects/lib/main.dart index 50e3422ec..2d3b5cc8e 100644 --- a/doc/examples/effects/infinite_effects/lib/main.dart +++ b/doc/examples/effects/infinite_effects/lib/main.dart @@ -44,7 +44,7 @@ class MyGame extends BaseGame with TapDetector { orangeSquare.clearEffects(); greenSquare.addEffect(MoveEffect( - destination: Vector2(dx, dy), + path: [Vector2(dx, dy)], speed: 250.0, curve: Curves.bounceInOut, isInfinite: true, diff --git a/doc/examples/effects/sequence_effect/lib/main.dart b/doc/examples/effects/sequence_effect/lib/main.dart index 03ca751c4..f1e5f9af1 100644 --- a/doc/examples/effects/sequence_effect/lib/main.dart +++ b/doc/examples/effects/sequence_effect/lib/main.dart @@ -1,3 +1,4 @@ +import 'package:flame/effects/combined_effect.dart'; import 'package:flame/effects/move_effect.dart'; import 'package:flame/effects/scale_effect.dart'; import 'package:flame/effects/rotate_effect.dart'; @@ -34,7 +35,7 @@ class MyGame extends BaseGame with TapDetector { greenSquare.clearEffects(); final move1 = MoveEffect( - destination: Vector2(dx, dy), + path: [Vector2(dx, dy)], speed: 250.0, curve: Curves.bounceInOut, isInfinite: false, @@ -42,16 +43,20 @@ class MyGame extends BaseGame with TapDetector { ); final move2 = MoveEffect( - destination: Vector2(dx, dy + 150), + path: [ + Vector2(dx, dy + 50), + Vector2(dx - 50, dy - 50), + Vector2(dx + 50, dy), + ], speed: 150.0, curve: Curves.easeIn, isInfinite: false, - isAlternating: true, + isAlternating: false, ); final scale = ScaleEffect( size: Vector2(dx, dy), - speed: 250.0, + speed: 100.0, curve: Curves.easeInCubic, isInfinite: false, isAlternating: false, @@ -59,15 +64,21 @@ class MyGame extends BaseGame with TapDetector { final rotate = RotateEffect( radians: (dx + dy) % pi, - speed: 2.0, + speed: 0.8, curve: Curves.decelerate, isInfinite: false, isAlternating: false, ); + final combination = CombinedEffect( + effects: [move2, rotate], + isAlternating: false, + isInfinite: false, + ); + final sequence = SequenceEffect( - effects: [move1, scale, move2, rotate], - isInfinite: true, + effects: [move1, scale, combination], + isInfinite: false, isAlternating: true, ); greenSquare.addEffect(sequence); diff --git a/doc/examples/effects/simple/lib/main_move.dart b/doc/examples/effects/simple/lib/main_move.dart index 3fdcdf3b8..7928b3671 100644 --- a/doc/examples/effects/simple/lib/main_move.dart +++ b/doc/examples/effects/simple/lib/main_move.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:flame/extensions/vector2.dart'; import 'package:flame/game.dart'; import 'package:flame/gestures.dart'; import 'package:flame/effects/effects.dart'; @@ -15,11 +16,21 @@ class MyGame extends BaseGame with TapDetector { } @override - void onTapUp(details) { + void onTapUp(TapUpDetails details) { square.addEffect(MoveEffect( - destination: details.localPosition.toVector2(), + path: [ + details.localPosition.toVector2(), + Vector2(100, 100), + Vector2(50, 120), + Vector2(200, 400), + Vector2(150, 0), + Vector2(100, 300), + ], speed: 250.0, curve: Curves.bounceInOut, + isRelative: false, + isInfinite: true, + isAlternating: true, )); } } diff --git a/doc/examples/joystick/lib/player.dart b/doc/examples/joystick/lib/player.dart index 2781dbd51..75a3a7aad 100644 --- a/doc/examples/joystick/lib/player.dart +++ b/doc/examples/joystick/lib/player.dart @@ -37,6 +37,7 @@ class Player extends Component implements JoystickListener { @override void update(double dt) { + super.update(dt); if (_move) { moveFromAngle(dt); } diff --git a/doc/examples/particles/lib/main.dart b/doc/examples/particles/lib/main.dart index 5acd6a515..cf2022037 100644 --- a/doc/examples/particles/lib/main.dart +++ b/doc/examples/particles/lib/main.dart @@ -587,6 +587,7 @@ class TrafficLightComponent extends Component { @override void update(double dt) { + super.update(dt); colorChangeTimer.update(dt); } diff --git a/lib/components/component.dart b/lib/components/component.dart index 4319f89de..a0647a074 100644 --- a/lib/components/component.dart +++ b/lib/components/component.dart @@ -1,7 +1,9 @@ import 'dart:ui'; import 'package:flutter/painting.dart'; +import 'package:meta/meta.dart'; +import '../effects/effects.dart'; import '../extensions/vector2.dart'; /// This represents a Component for your game. @@ -10,12 +12,19 @@ import '../extensions/vector2.dart'; /// Anything that either renders or updates can be added to the list on [BaseGame]. It will deal with calling those methods for you. /// Components also have other methods that can help you out if you want to overwrite them. abstract class Component { + /// The effects that should run on the component + final List _effects = []; + /// This method is called periodically by the game engine to request that your component updates itself. /// /// The time [t] in seconds (with microseconds precision provided by Flutter) since the last update cycle. /// This time can vary according to hardware capacity, so make sure to update your state considering this. /// All components on [BaseGame] are always updated by the same amount. The time each one takes to update adds up to the next update cycle. - void update(double t); + @mustCallSuper + void update(double dt) { + _effects.forEach((e) => e.update(dt)); + _effects.removeWhere((e) => e.hasFinished()); + } /// Renders this component on the provided Canvas [c]. void render(Canvas c); @@ -58,4 +67,19 @@ abstract class Component { /// Called right before the component is destroyed and removed from the game void onDestroy() {} + + /// Add an effect to the component + void addEffect(ComponentEffect effect) { + _effects.add(effect..initialize(this)); + } + + /// Mark an effect for removal on the component + void removeEffect(ComponentEffect effect) { + effect.dispose(); + } + + /// Remove all effects + void clearEffects() { + _effects.forEach(removeEffect); + } } diff --git a/lib/components/joystick/joystick_component.dart b/lib/components/joystick/joystick_component.dart index 4c0d3e4dd..939372d4c 100644 --- a/lib/components/joystick/joystick_component.dart +++ b/lib/components/joystick/joystick_component.dart @@ -65,6 +65,7 @@ class JoystickComponent extends JoystickController { @override void update(double t) { + super.update(t); directional?.update(t); actions?.forEach((action) => action.update(t)); } diff --git a/lib/components/particle_component.dart b/lib/components/particle_component.dart index de1e08289..a30bc13a6 100644 --- a/lib/components/particle_component.dart +++ b/lib/components/particle_component.dart @@ -35,6 +35,7 @@ class ParticleComponent extends Component { /// Passes update chain to child [Particle]. @override void update(double dt) { + super.update(dt); particle.update(dt); } } diff --git a/lib/components/position_component.dart b/lib/components/position_component.dart index c58930325..3ff9489a3 100644 --- a/lib/components/position_component.dart +++ b/lib/components/position_component.dart @@ -5,7 +5,6 @@ import 'package:ordered_set/comparing.dart'; import 'package:ordered_set/ordered_set.dart'; import '../anchor.dart'; -import '../effects/effects.dart'; import '../game.dart'; import '../text_config.dart'; import '../extensions/offset.dart'; @@ -77,7 +76,6 @@ abstract class PositionComponent extends Component { /// You can also manually override this for certain components in order to identify issues. bool debugMode = false; - final List _effects = []; final OrderedSet _children = OrderedSet(Comparing.on((c) => c.priority())); @@ -138,25 +136,6 @@ abstract class PositionComponent extends Component { } } - void addEffect(PositionComponentEffect effect) { - _effects.add(effect..initialize(this)); - } - - void removeEffect(PositionComponentEffect effect) { - effect.dispose(); - } - - void clearEffects() { - _effects.forEach(removeEffect); - } - - @mustCallSuper - @override - void update(double dt) { - _effects.forEach((e) => e.update(dt)); - _effects.removeWhere((e) => e.hasFinished()); - } - /// This function recursively propagates an action to every children and grandchildren (and so on) of this component, /// by keeping track of their positions by composing the positions of their parents. /// For example, if this has a child that itself has a child, this will invoke handler for this (with no translation), diff --git a/lib/components/sprite_batch_component.dart b/lib/components/sprite_batch_component.dart index 23dc590a3..c3270cc67 100644 --- a/lib/components/sprite_batch_component.dart +++ b/lib/components/sprite_batch_component.dart @@ -25,7 +25,4 @@ class SpriteBatchComponent extends Component { paint: paint, ); } - - @override - void update(double t) {} } diff --git a/lib/components/timer_component.dart b/lib/components/timer_component.dart index 108eb766c..7c58855e4 100644 --- a/lib/components/timer_component.dart +++ b/lib/components/timer_component.dart @@ -10,7 +10,10 @@ class TimerComponent extends Component { TimerComponent(this.timer); @override - void update(double dt) => timer.update(dt); + void update(double dt) { + super.update(dt); + timer.update(dt); + } @override void render(Canvas canvas) {} diff --git a/lib/effects/combined_effect.dart b/lib/effects/combined_effect.dart index fce5b9517..4b400ea5c 100644 --- a/lib/effects/combined_effect.dart +++ b/lib/effects/combined_effect.dart @@ -16,6 +16,10 @@ class CombinedEffect extends PositionComponentEffect { bool isAlternating = false, Function onComplete, }) : super(isInfinite, isAlternating, onComplete: onComplete) { + assert( + effects.every((effect) => effect.component == null), + 'Each effect can only be added once', + ); final types = effects.map((e) => e.runtimeType); assert( types.toSet().length == types.length, @@ -47,7 +51,9 @@ class CombinedEffect extends PositionComponentEffect { super.update(dt); effects.forEach((effect) => _updateEffect(effect, dt)); if (effects.every((effect) => effect.hasFinished())) { - if (isInfinite) { + if (isAlternating && curveDirection.isNegative) { + effects.forEach((effect) => effect.isAlternating = true); + } else if (isInfinite) { reset(); } else if (isAlternating && isMin()) { dispose(); @@ -58,8 +64,13 @@ class CombinedEffect extends PositionComponentEffect { @override void reset() { super.reset(); + if (component != null) { + component.position = originalPosition; + component.angle = originalAngle; + component.size = originalSize; + initialize(component); + } effects.forEach((effect) => effect.reset()); - initialize(component); } @override @@ -85,6 +96,11 @@ class CombinedEffect extends PositionComponentEffect { } } + @override + bool hasFinished() { + return super.hasFinished() && effects.every((e) => e.hasFinished()); + } + void _maybeReverse(PositionComponentEffect effect) { if (isAlternating && !effect.isAlternating && effect.isMax()) { // Make the effect go in reverse diff --git a/lib/effects/effects.dart b/lib/effects/effects.dart index 15d136925..f7aae6497 100644 --- a/lib/effects/effects.dart +++ b/lib/effects/effects.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import '../components/component.dart'; import '../components/position_component.dart'; import '../extensions/vector2.dart'; @@ -11,8 +12,8 @@ export './rotate_effect.dart'; export './scale_effect.dart'; export './sequence_effect.dart'; -abstract class PositionComponentEffect { - PositionComponent component; +abstract class ComponentEffect { + T component; Function() onComplete; bool _isDisposed = false; @@ -31,16 +32,11 @@ abstract class PositionComponentEffect { double driftTime = 0.0; int curveDirection = 1; - /// Used to be able to determine the end state of a sequence of effects - Vector2 endPosition; - double endAngle; - Vector2 endSize; - /// If the effect is alternating the travel time is double the normal /// travel time double get totalTravelTime => travelTime * (isAlternating ? 2 : 1); - PositionComponentEffect( + ComponentEffect( this._initialIsInfinite, this._initialIsAlternating, { this.isRelative = false, @@ -55,7 +51,7 @@ abstract class PositionComponentEffect { if (isAlternating) { curveDirection = isMax() ? -1 : (isMin() ? 1 : curveDirection); } else if (isInfinite && isMax()) { - currentTime = 0.0; + reset(); } final driftMultiplier = (isAlternating && isMax() ? 2 : 1) * curveDirection; if (!hasFinished()) { @@ -68,19 +64,12 @@ abstract class PositionComponentEffect { } @mustCallSuper - void initialize(PositionComponent _comp) { - component = _comp; + void initialize(T component) { + this.component = component; /// You need to set the travelTime during the initialization of the /// extending effect travelTime = null; - - /// If these aren't modified by the extending effect it is assumed that the - /// effect didn't bring the component to another state than the one it - /// started in - endPosition = _comp.position; - endAngle = _comp.angle; - endSize = _comp.size; } void dispose() => _isDisposed = true; @@ -114,3 +103,45 @@ abstract class PositionComponentEffect { } } } + +abstract class PositionComponentEffect + extends ComponentEffect { + /// Used to be able to determine the start state of the component + Vector2 originalPosition; + double originalAngle; + Vector2 originalSize; + + /// Used to be able to determine the end state of a sequence of effects + Vector2 endPosition; + double endAngle; + Vector2 endSize; + + PositionComponentEffect( + initialIsInfinite, + initialIsAlternating, { + isRelative = false, + onComplete, + }) : super( + initialIsInfinite, + initialIsAlternating, + isRelative: isRelative, + onComplete: onComplete, + ); + + @mustCallSuper + @override + void initialize(PositionComponent component) { + super.initialize(component); + this.component = component; + originalPosition = component.position; + originalAngle = component.angle; + originalSize = component.size; + + /// If these aren't modified by the extending effect it is assumed that the + /// effect didn't bring the component to another state than the one it + /// started in + endPosition = component.position; + endAngle = component.angle; + endSize = component.size; + } +} diff --git a/lib/effects/move_effect.dart b/lib/effects/move_effect.dart index 3b2a5f919..c26265fc4 100644 --- a/lib/effects/move_effect.dart +++ b/lib/effects/move_effect.dart @@ -4,15 +4,30 @@ import 'package:meta/meta.dart'; import '../extensions/vector2.dart'; import 'effects.dart'; +class Vector2Percentage { + final Vector2 v; + final Vector2 previous; + final double startAt; + final double endAt; + + Vector2Percentage( + this.v, + this.previous, + this.startAt, + this.endAt, + ); +} + class MoveEffect extends PositionComponentEffect { - Vector2 destination; + List path; + Vector2Percentage _currentSubPath; + List _percentagePath; double speed; Curve curve; Vector2 _startPosition; - Vector2 _delta; MoveEffect({ - @required this.destination, + @required this.path, @required this.speed, this.curve, isInfinite = false, @@ -29,18 +44,79 @@ class MoveEffect extends PositionComponentEffect { @override void initialize(_comp) { super.initialize(_comp); - if (!isAlternating) { - endPosition = destination; - } + List _movePath; _startPosition = component.position.clone(); - _delta = isRelative ? destination : destination - _startPosition; - travelTime = _delta.length / speed; + // With relative here we mean that any vector in the list is relative + // to the previous vector in the list, except the first one which is + // relative to the start position of the component. + if (isRelative) { + Vector2 lastPosition = _startPosition; + _movePath = []; + for (Vector2 v in path) { + final nextPosition = v + lastPosition; + _movePath.add(nextPosition); + lastPosition = nextPosition; + } + } else { + _movePath = path; + } + if (!isAlternating) { + endPosition = _movePath.last; + } else { + endPosition = _startPosition; + } + + double pathLength = 0; + Vector2 lastPosition = _startPosition; + for (Vector2 v in _movePath) { + pathLength += v.distanceTo(lastPosition); + lastPosition = v; + } + + _percentagePath = []; + lastPosition = _startPosition; + for (Vector2 v in _movePath) { + final lengthToPrevious = lastPosition.distanceTo(v); + final lastEndAt = + _percentagePath.isNotEmpty ? _percentagePath.last.endAt : 0.0; + final endPercentage = lastEndAt + lengthToPrevious / pathLength; + _percentagePath.add( + Vector2Percentage( + v, + lastPosition, + lastEndAt, + _movePath.last == v ? 1.0 : endPercentage, + ), + ); + lastPosition = v; + } + travelTime = pathLength / speed; + } + + @override + void reset() { + super.reset(); + if (_percentagePath?.isNotEmpty ?? false) { + _currentSubPath = _percentagePath.first; + } } @override void update(double dt) { + if (hasFinished()) { + return; + } super.update(dt); final double progress = curve?.transform(percentage) ?? 1.0; - component.position = _startPosition + _delta * progress; + _currentSubPath ??= _percentagePath.first; + if (!curveDirection.isNegative && _currentSubPath.endAt < progress || + curveDirection.isNegative && _currentSubPath.startAt > progress) { + _currentSubPath = _percentagePath.firstWhere((v) => v.endAt >= progress); + } + final double lastEndAt = _currentSubPath.startAt; + final double localPercentage = + (progress - 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 737f34875..29dceace5 100644 --- a/lib/effects/rotate_effect.dart +++ b/lib/effects/rotate_effect.dart @@ -32,7 +32,7 @@ class RotateEffect extends PositionComponentEffect { endAngle = _comp.angle + radians; } _startAngle = component.angle; - _delta = isRelative ? radians : _startAngle + radians; + _delta = isRelative ? radians : radians - _startAngle; travelTime = (_delta / speed).abs(); } diff --git a/lib/effects/sequence_effect.dart b/lib/effects/sequence_effect.dart index ce7dd306f..d217fac58 100644 --- a/lib/effects/sequence_effect.dart +++ b/lib/effects/sequence_effect.dart @@ -5,10 +5,10 @@ import 'effects.dart'; class SequenceEffect extends PositionComponentEffect { final List effects; - int _currentIndex = 0; + int _currentIndex; PositionComponentEffect currentEffect; bool _currentWasAlternating; - double _driftModifier = 0.0; + double _driftModifier; SequenceEffect({ @required this.effects, @@ -18,7 +18,11 @@ class SequenceEffect extends PositionComponentEffect { }) : super(isInfinite, isAlternating, onComplete: onComplete) { assert( effects.every((effect) => effect.component == null), - "No effects can be added to components from the start", + 'Each effect can only be added once', + ); + assert( + effects.every((effect) => !effect.isInfinite), + 'No effects added to the sequence can be infinite', ); } @@ -26,9 +30,7 @@ class SequenceEffect extends PositionComponentEffect { void initialize(PositionComponent _comp) { super.initialize(_comp); _currentIndex = 0; - final originalSize = _comp.size; - final originalPosition = _comp.position; - final originalAngle = _comp.angle; + _driftModifier = 0.0; effects.forEach((effect) { effect.reset(); _comp.size = endSize; @@ -43,9 +45,9 @@ class SequenceEffect extends PositionComponentEffect { 0, (time, effect) => time + effect.totalTravelTime, ); - component.size = originalSize; component.position = originalPosition; component.angle = originalAngle; + component.size = originalSize; currentEffect = effects.first; _currentWasAlternating = currentEffect.isAlternating; } @@ -57,27 +59,36 @@ class SequenceEffect extends PositionComponentEffect { } super.update(dt); + // If the last effect's time to completion overshot its total time, add that + // time to the first time step of the next effect. currentEffect.update(dt + _driftModifier); _driftModifier = 0.0; if (currentEffect.hasFinished()) { _driftModifier = currentEffect.driftTime; - currentEffect.isAlternating = _currentWasAlternating; _currentIndex++; final iterationSize = isAlternating ? effects.length * 2 : effects.length; - if (_currentIndex != 0 && _currentIndex % iterationSize == 0) { + if (_currentIndex != 0 && + _currentIndex == iterationSize && + (currentEffect.isAlternating || + currentEffect.isAlternating == isAlternating)) { isInfinite ? reset() : dispose(); return; } final orderedEffects = curveDirection.isNegative ? effects.reversed.toList() : effects; + // Make sure the current effect has the `isAlternating` value it + // initially started with + currentEffect.isAlternating = _currentWasAlternating; + // Get the next effect that should be executed currentEffect = orderedEffects[_currentIndex % effects.length]; + // Keep track of what value of `isAlternating` the effect had from the + // start _currentWasAlternating = currentEffect.isAlternating; if (isAlternating && !currentEffect.isAlternating && curveDirection.isNegative) { // Make the effect go in reverse currentEffect.isAlternating = true; - currentEffect.percentage = 1.0; } } } @@ -85,6 +96,12 @@ class SequenceEffect extends PositionComponentEffect { @override void reset() { super.reset(); - initialize(component); + effects.forEach((e) => e.reset()); + if (component != null) { + component.position = originalPosition; + component.angle = originalAngle; + component.size = originalSize; + initialize(component); + } } }