Merge pull request #479 from flame-engine/spydon.fix-effects-bugs

Fix effects bugs, use Vector2 path for MoveEffect and generalize effects
This commit is contained in:
Lukas Klingsbo
2020-10-11 00:16:37 +02:00
committed by GitHub
19 changed files with 263 additions and 82 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -37,6 +37,7 @@ class Player extends Component implements JoystickListener {
@override
void update(double dt) {
super.update(dt);
if (_move) {
moveFromAngle(dt);
}

View File

@ -587,6 +587,7 @@ class TrafficLightComponent extends Component {
@override
void update(double dt) {
super.update(dt);
colorChangeTimer.update(dt);
}

View File

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

View File

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

View File

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

View File

@ -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<PositionComponentEffect> _effects = [];
final OrderedSet<Component> _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),

View File

@ -25,7 +25,4 @@ class SpriteBatchComponent extends Component {
paint: paint,
);
}
@override
void update(double t) {}
}

View File

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

View File

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

View File

@ -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 extends Component> {
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<PositionComponent> {
/// 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;
}
}

View File

@ -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<Vector2> path;
Vector2Percentage _currentSubPath;
List<Vector2Percentage> _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<Vector2> _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 = <Vector2Percentage>[];
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);
}
}

View File

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

View File

@ -5,10 +5,10 @@ import 'effects.dart';
class SequenceEffect extends PositionComponentEffect {
final List<PositionComponentEffect> 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);
}
}
}