Skip to content

Commit

Permalink
feat(effects)!: Added SequenceEffect (#1218)
Browse files Browse the repository at this point in the history
Added SequenceEffect, which performs a series of other effects.

The biggest challenge in implementing this feature came from the need to run the sequence in reverse, due to the alternate flag. This required that every effect and every controller supported running "back in time", which is not as simple as it sounds.

The following breaking changes were introduced:

    The Effect class no longer supports .reverse() method and .isReversed flag.

    This flag was added only 2 weeks ago (

Effect controllers restructuring #1134), with the idea that it will be necessary for the SequenceEffect. However, as it turned out, this flag is not as helpful as I thought it would be. In fact, given the user's ability to change it any point, it makes the implementation very error-prone.

To be clear, the ability for each effect to run in reverse remains -- only now it can no longer be triggered by the user manually. Instead, SequenceEffect triggers that ability itself at the alternation point. If there is demand in the future to manually force any effect to run backwards, we could restore this flag, but this would require thorough testing to make it work correctly.

Infinite effects now return duration = double.infinity instead of null, which seems more appropriate.
  • Loading branch information
st-pasha authored Dec 27, 2021
1 parent 20f521f commit 7c6ae6d
Show file tree
Hide file tree
Showing 17 changed files with 688 additions and 52 deletions.
33 changes: 26 additions & 7 deletions doc/effects.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ There are multiple effects provided by Flame, and you can also
- [`SizeEffect.to`](#sizeeffectto)
- [`OpacityEffect`](#opacityeffect)
- [`ColorEffect`](#coloreffect)
- [`SequenceEffect`](#sequenceeffect)
- [`RemoveEffect`](#removeeffect)

An `EffectController` is an object that describes how the effect should evolve over time. If you
Expand Down Expand Up @@ -146,7 +147,7 @@ is in radians. For example, the following effect will rotate the target 90º (=[
clockwise:

```dart
final effect = RotateEffect.by(tau/4, EffectController(2));
final effect = RotateEffect.by(tau/4, EffectController(duration: 2));
```


Expand All @@ -156,7 +157,7 @@ Rotates the target clockwise to the specified angle. For example, the following
target to look east (0º is north, 90º=[tau]/4 east, 180º=tau/2 south, and 270º=tau*3/4 west):

```dart
final effect = RotateEffect.to(tau/4, EffectController(2));
final effect = RotateEffect.to(tau/4, EffectController(duration: 2));
```


Expand All @@ -166,7 +167,7 @@ This effect will change the target's scale by the specified amount. For example,
the component to grow 50% larger:

```dart
final effect = ScaleEffect.by(Vector2.all(1.5), EffectController(0.3));
final effect = ScaleEffect.by(Vector2.all(1.5), EffectController(duration: 0.3));
```


Expand All @@ -175,7 +176,7 @@ final effect = ScaleEffect.by(Vector2.all(1.5), EffectController(0.3));
This effect works similar to `ScaleEffect.by`, but sets the absolute value of the target's scale.

```dart
final effect = ScaleEffect.to(Vector2.zero(), EffectController(0.5));
final effect = ScaleEffect.to(Vector2.zero(), EffectController(duration: 0.5));
```


Expand All @@ -186,7 +187,7 @@ if the target has size `Vector2(100, 100)`, then after the following effect is a
course, the new size will be `Vector2(120, 50)`:

```dart
final effect = SizeEffect.by(Vector2(20, -50), EffectController(1));
final effect = SizeEffect.by(Vector2(20, -50), EffectController(duration: 1));
```

The size of a `PositionComponent` cannot be negative. If an effect attempts to set the size to a
Expand All @@ -203,7 +204,7 @@ more generally and scales the children components too.
Changes the size of the target component to the specified size. Target size cannot be negative:

```dart
final effect = SizeEffect.to(Vector2(120, 120), EffectController(1));
final effect = SizeEffect.to(Vector2(120, 120), EffectController(duration: 1));
```


Expand All @@ -214,14 +215,32 @@ this effect can only be applied to components that have a `HasPaint` mixin. If t
uses multiple paints, the effect can target any individual color using the `paintId` parameter.

```dart
final effect = OpacityEffect.to(0.5, EffectController(0.75));
final effect = OpacityEffect.to(0.5, EffectController(duration: 0.75));
```

The opacity value of 0 corresponds to a fully transparent component, and the opacity value of 1 is
fully opaque. Convenience constructors `OpacityEffect.fadeOut()` and `OpacityEffect.fadeIn()` will
animate the target into full transparency / full visibility respectively.


### `SequenceEffect`

This effect can be used to run multiple other effects one after another. The constituent effects
may have different types.

The sequence effect can also be alternating (the sequence will first run forward, and then
backward); and also repeat a certain predetermined number of times, or infinitely.

```dart
final effect = SequenceEffect([
ScaleEffect.by(1.5, EffectController(duration: 0.2, alternate: true)),
MoveEffect.by(Vector2(30, -50), EffectController(duration: 0.5)),
OpacityEffect.to(0, EffectController(duration: 0.3)),
RemoveEffect(),
]);
```


### `RemoveEffect`

This is a simple effect that can be attached to a component causing it to be removed from the game
Expand Down
7 changes: 7 additions & 0 deletions examples/lib/stories/effects/effects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import 'opacity_effect_example.dart';
import 'remove_effect_example.dart';
import 'rotate_effect_example.dart';
import 'scale_effect_example.dart';
import 'sequence_effect_example.dart';
import 'size_effect_example.dart';

void addEffectsStories(Dashbook dashbook) {
Expand Down Expand Up @@ -49,6 +50,12 @@ void addEffectsStories(Dashbook dashbook) {
codeLink: baseLink('effects/color_effect_example.dart'),
info: ColorEffectExample.description,
)
..add(
'Sequence Effect',
(_) => GameWidget(game: SequenceEffectExample()),
codeLink: baseLink('effects/sequence_effect_example.dart'),
info: SequenceEffectExample.description,
)
..add(
'Remove Effect',
(_) => GameWidget(game: RemoveEffectExample()),
Expand Down
63 changes: 63 additions & 0 deletions examples/lib/stories/effects/sequence_effect_example.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import 'dart:ui';

import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';

class SequenceEffectExample extends FlameGame {
static const String description = '''
Sequence of effects, consisting of a move effect, a rotate effect, another
move effect, a scale effect, and then one more move effect. The sequence
then runs in the opposite order (alternate = true) and loops infinitely
(infinite = true).
''';

@override
Future<void> onLoad() async {
super.onLoad();
const tau = Transform2D.tau;
EffectController duration(double x) => EffectController(duration: x);
add(
Player()
..position = Vector2(200, 300)
..add(
SequenceEffect(
[
MoveEffect.to(Vector2(400, 300), duration(0.7)),
RotateEffect.by(tau / 4, duration(0.5)),
MoveEffect.to(Vector2(400, 400), duration(0.7)),
ScaleEffect.by(Vector2.all(1.5), duration(0.7)),
MoveEffect.to(Vector2(400, 500), duration(0.7)),
],
alternate: true,
infinite: true,
),
),
);
}
}

class Player extends PositionComponent {
Player()
: path = Path()
..lineTo(40, 20)
..lineTo(0, 40)
..quadraticBezierTo(8, 20, 0, 0)
..close(),
bodyPaint = Paint()..color = const Color(0x887F99B3),
borderPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 3
..color = const Color(0xFFFFFD9A),
super(anchor: Anchor.center, size: Vector2(40, 40));

final Path path;
final Paint borderPaint;
final Paint bodyPaint;

@override
void render(Canvas canvas) {
canvas.drawPath(path, bodyPaint);
canvas.drawPath(path, borderPaint);
}
}
2 changes: 1 addition & 1 deletion packages/flame/.min_coverage
Original file line number Diff line number Diff line change
@@ -1 +1 @@
62.0
67.0
1 change: 1 addition & 0 deletions packages/flame/lib/effects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ 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/sequence_effect.dart' show SequenceEffect;
export 'src/effects/size_effect.dart';
export 'src/effects/transform2d_effect.dart';
14 changes: 12 additions & 2 deletions packages/flame/lib/src/effects/component_effect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ abstract class ComponentEffect<T extends Component> extends Effect {
void onMount() {
super.onMount();
assert(parent != null);
if (parent is T) {
target = parent! as T;
var p = parent;
while (p is Effect) {
p = p.parent;
}
if (p is T) {
target = p;
} else {
throw UnsupportedError('Can only apply this effect to $T');
}
Expand All @@ -41,4 +45,10 @@ abstract class ComponentEffect<T extends Component> extends Effect {
super.reset();
_lastProgress = 0;
}

@override
void resetToEnd() {
super.resetToEnd();
_lastProgress = 1;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,6 @@ class DelayedEffectController extends EffectController {
final double delay;
double _timer;

@override
bool get isInfinite => _child.isInfinite;

@override
bool get isRandom => _child.isRandom;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -217,13 +217,13 @@ abstract class EffectController {
EffectController.empty();

/// Will the effect continue to run forever (never completes)?
bool get isInfinite => false;
bool get isInfinite => duration == double.infinity;

/// Is the effect's duration random or fixed?
bool get isRandom => false;

/// Total duration of the effect. If the duration cannot be determined, this
/// will return `null`.
/// will return `null`. For an infinite effect the duration is infinity.
double? get duration;

/// Has the effect started running? Some effects use a "delay" parameter to
Expand All @@ -247,7 +247,8 @@ abstract class EffectController {
/// 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.
/// controller has finished. In all cases, the return value can be positive
/// only when `completed == true`.
///
/// 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ class InfiniteEffectController extends EffectController {

final EffectController child;

@override
bool get isInfinite => true;

@override
bool get completed => false;

@override
double? get duration => null;
double? get duration => double.infinity;

@override
double get progress => child.progress;

@override
bool get isRandom => child.isRandom;

@override
double advance(double dt) {
var t = dt;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,6 @@ class RandomEffectController extends EffectController {
final DurationEffectController child;
final RandomVariable randomGenerator;

@override
bool get isInfinite => false;

@override
bool get isRandom => true;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ class RepeatedEffectController extends EffectController {
return d == null ? null : d * repeatCount;
}

@override
bool get isRandom => child.isRandom;

@override
double advance(double dt) {
var t = child.advance(dt);
while (t > 0 && _remainingCount > 0) {
assert(child.completed);
_remainingCount--;
if (_remainingCount != 0) {
child.setToStart();
Expand All @@ -56,6 +60,7 @@ class RepeatedEffectController extends EffectController {
// if we recede from the end position the remaining count must be
// adjusted.
_remainingCount = 1;
assert(child.completed);
}
var t = child.recede(dt);
while (t > 0 && _remainingCount < repeatCount) {
Expand Down
Loading

0 comments on commit 7c6ae6d

Please sign in to comment.