Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: New AlternatePattern argument for SequenceEffect #3322

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
1 change: 1 addition & 0 deletions examples/lib/stories/effects/sequence_effect_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ class SequenceEffectExample extends FlameGame {
MoveEffect.to(Vector2(400, 500), duration(0.7)),
],
alternate: true,
alternatePattern: AlternatePattern.doNotRepeatLast,
infinite: true,
),
),
Expand Down
2 changes: 1 addition & 1 deletion packages/flame/lib/effects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,6 @@ export 'src/effects/provider_interfaces.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/sequence_effect.dart' show SequenceEffect, AlternatePattern;
export 'src/effects/size_effect.dart';
export 'src/effects/transform2d_effect.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ abstract class DurationEffectController extends EffectController {
double recede(double dt) {
_timer -= dt;
if (_timer < 0) {
final leftoverTime = 0 - _timer;
final leftoverTime = _timer.abs();
_timer = 0;
return leftoverTime;
}
Expand Down
3 changes: 3 additions & 0 deletions packages/flame/lib/src/effects/effect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ abstract class Effect extends Component {
double recede(double dt) {
if (_finished && dt > 0) {
_finished = false;

/// Effects such as [MoveToEffect] must recalculate the direction vector.
onStart();
}
final remainingDt = controller.recede(dt);
if (_started) {
Expand Down
2 changes: 1 addition & 1 deletion packages/flame/lib/src/effects/move_to_effect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class MoveToEffect extends MoveEffect {
@override
void apply(double progress) {
final dProgress = progress - previousProgress;
target.position += _offset * dProgress;
target.position += _offset * dProgress.abs();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, is this really correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After thinking about it some more yesterday I think the issue wasn't the reflected vector itself, but rather that the direction vector was incorrect when the camera has a viewport-aware bounds behavior. This causes it to never reach its destination and so the reflective vector is incorrect. That's why taking the absolute value was ok after I added the step to recalculate the vector.

The camera won't be the only component that may never complete its effect with regards to MoveToEffect so this matter should be resolved in the PR imo.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But this will potentially overshoot the destination right? And also it won't be matching the time anymore?
If you are at 0.99 progress and the delta progress is -0.02 when the effect is going forward it should have moved 0.03 "steps", since it'll start going back again, but with this it will simply move to 1.01.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I'm understanding your question precisely, but it seems that you've noticed the possibility of under-fitting the expected step amounts, time-wise. Yes this does happen with the viewport-aware behavior as described (the camera cannot exceed the bounds, and therefore never arrives to its destination). ATM with the changes I've added, I have not seen any noticeable problems.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simply put; I don't think it will always be at the expected place at each time step when abs is used, that was what I tried to describe in the last comment, the delta doesn't become correct. If it overshoots its destination it should start going back, in the cases where it is alternating.

Copy link
Contributor Author

@TheMaverickProgrammer TheMaverickProgrammer Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

progress will be decrementing dt units from 1.0 during the reverse phase of the alternating pattern, while previousProgress retains the progress+dt value from before. Therefore dProgress = progress - previousProgress would be negative, which we don't want to use. It's better to recalculate the starting position and use positive scalars instead.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But it'll surely overshoot the end value then?

Copy link
Contributor Author

@TheMaverickProgrammer TheMaverickProgrammer Nov 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi. No it never does for this effect or any as far as I can tell. The same math could have overshot in the reverse direction when dProgress was negative.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the case that dProgress becomes negative it is supposed to go backwards. Say that the previous progress is 0.9 and it it supposed to move backwards to 0.8, then dProgress should be -0.1, not 0.1.
Try running only the the MoveToEffect with alternate: true.

Copy link
Contributor Author

@TheMaverickProgrammer TheMaverickProgrammer Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi. A negative progress value for this effect specifically is not only unintuitive but also does not solve the MoveToEffect starting vector issue.

}

@override
Expand Down
96 changes: 85 additions & 11 deletions packages/flame/lib/src/effects/sequence_effect.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import 'package:flame/src/effects/effect.dart';
EffectController _createController({
required List<Effect> effects,
required bool alternate,
required AlternatePattern alternatePattern,
required bool infinite,
required int repeatCount,
}) {
EffectController ec = _SequenceEffectEffectController(
effects,
alternate: alternate,
alternatePattern: alternatePattern,
);
if (infinite) {
ec = InfiniteEffectController(ec);
Expand All @@ -22,6 +24,24 @@ EffectController _createController({
return ec;
}

/// Specifies how to playback an alternating [SequenceEffect] pattern.
///
/// [AlternatePattern.repeatLast] will replay the first and last [Effect]
/// when the sequence repeats, causing those [Effect]s to play twice-in-a-row.
///
/// [AlternatePattern.doNotRepeatLast] will not replay the first and last
/// [Effect] and instead jumps to the second and second-to-last [Effect]
/// respectively, if available, at the start of the alternating pattern.
/// This is equivalent to playing the first and last [Effect] once throughout
/// the combined span of the original pattern plus its reversed pattern.
enum AlternatePattern {
repeatLast(1),
doNotRepeatLast(2);

final int value;
const AlternatePattern(this.value);
}

/// Run multiple effects in a sequence, one after another.
///
/// The provided effects will be added as child components; however the custom
Expand All @@ -32,6 +52,10 @@ EffectController _createController({
/// If the `alternate` flag is provided, then the sequence will run in the
/// reverse after it ran forward.
///
/// Parameter `alternatePattern` is only used when `alternate` is true.
/// This parameter modifies how the pattern repeats in reverse.
/// See [AlternatePattern] for options.
///
/// Parameter `repeatCount` will make the sequence repeat a certain number of
/// times. If `alternate` is also true, then the sequence will first run
/// forward, then back, and then repeat this pattern `repeatCount` times in
Expand All @@ -48,6 +72,7 @@ EffectController _createController({
class SequenceEffect extends Effect {
SequenceEffect(
List<Effect> effects, {
AlternatePattern alternatePattern = AlternatePattern.repeatLast,
bool alternate = false,
bool infinite = false,
int repeatCount = 1,
Expand All @@ -63,6 +88,7 @@ class SequenceEffect extends Effect {
_createController(
effects: effects,
alternate: alternate,
alternatePattern: alternatePattern,
infinite: infinite,
repeatCount: repeatCount,
),
Expand Down Expand Up @@ -93,6 +119,7 @@ class _SequenceEffectEffectController extends EffectController {
_SequenceEffectEffectController(
this.effects, {
required this.alternate,
required this.alternatePattern,
}) : super.empty();

/// The list of children effects.
Expand All @@ -102,6 +129,11 @@ class _SequenceEffectEffectController extends EffectController {
/// run again in the reverse order.
final bool alternate;

/// If [alternate] is true, and after the sequence runs to completion once,
/// this will run again in the reverse order according to the policy
/// of the [AlternatePattern] value provided.
final AlternatePattern alternatePattern;

/// Index of the currently running effect within the [effects] list. If there
/// are n effects in total, then this runs as 0, 1, ..., n-1. After that, if
/// the effect alternates, then the `_index` continues as -1, -2, ..., -n,
Expand All @@ -114,6 +146,23 @@ class _SequenceEffectEffectController extends EffectController {
/// Total number of effects in this sequence.
int get n => effects.length;

/// If [alternate] is not set, our last index will be `n-1`.
/// Otherwise, the sequence approaches 0 from the left of the
/// number-line, and if our [alternatePattern] excludes the first
/// [Effect], then it will reduce the destination index by 1.
int get _computeLastIndex => switch (alternate) {
true => switch (alternatePattern) {
// index 0 is the start of the original pattern,
// therefore -1 is our destination index, which will
// be the exact same `Effect` as index 0.
AlternatePattern.repeatLast => -1,
// Since the original pattern will begin again from start,
// skip index -1 and make -2 our destination index.
AlternatePattern.doNotRepeatLast => -2,
},
false => n - 1,
};

@override
bool get completed => _completed;
bool _completed = false;
Expand All @@ -124,9 +173,21 @@ class _SequenceEffectEffectController extends EffectController {
for (final effect in effects) {
totalDuration += effect.controller.duration ?? 0;
}

// Abort early
if (totalDuration == 0.0) {
return totalDuration;
}

if (alternate) {
totalDuration *= 2;

if (alternatePattern == AlternatePattern.doNotRepeatLast) {
totalDuration -= effects.first.controller.duration ?? 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How come the first one is removed here too? 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi. This has been explained in the PR, code comments, and here:
#3322 (comment)

totalDuration -= effects.last.controller.duration ?? 0;
}
}

return totalDuration;
}

Expand All @@ -136,7 +197,7 @@ class _SequenceEffectEffectController extends EffectController {
}

@override
double get progress => (_index + 1) / n;
double get progress => (_index < 0 ? -_index : _index + 1) / n;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain this a bit? In what cases is the _index negative? This doesn't seem like it would follow how progress works for the rest of the effects.

Copy link
Contributor Author

@TheMaverickProgrammer TheMaverickProgrammer Nov 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way the SequenceEffect has been written for some time now is that it uses a negative _index to determine if the pattern is in its reverse phase. To be clear, I did not write it this way. This code was like this when I started this PR,


@override
double advance(double dt) {
Expand All @@ -147,20 +208,32 @@ class _SequenceEffectEffectController extends EffectController {
if (t > 0) {
_index += 1;
if (_index == n) {
if (alternate) {
_index = -1;
} else {
_index = n - 1;
_index = _computeLastIndex;

if (_index == n - 1) {
_completed = true;
break;
}
}
}
} else {
// This case represents the reversed alternating pattern
// when `alternate` is true. Our indices will be negative,
// and we recede back to index 0.

t = currentEffect.recede(t);
if (t > 0) {
_index -= 1;
if (_index < -n) {

var lastIndex = -n;
// Iff the requested alternate policy is `repeatLast`, then we must
// include and play the start Effect before considering our sequence
// completed.
if (alternate && alternatePattern == AlternatePattern.repeatLast) {
lastIndex -= 1;
}

if (_index == lastIndex) {
_index = -n;
_completed = true;
break;
Expand Down Expand Up @@ -208,13 +281,14 @@ class _SequenceEffectEffectController extends EffectController {

@override
void setToEnd() {
if (alternate) {
_index = -n;
effects.forEach((e) => e.reset());
} else {
_index = n - 1;
_index = _computeLastIndex;

if (_index == n - 1) {
effects.forEach((e) => e.resetToEnd());
} else {
effects.forEach((e) => e.reset());
}

_completed = true;
}

Expand Down
6 changes: 3 additions & 3 deletions packages/flame/test/effects/sequence_effect_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ void main() {
);
final effect = SequenceEffect(
[randomEffect],
alternate: true,
repeatCount: 1000,
alternate: true,
);
expect(
effect.controller.duration,
Expand Down Expand Up @@ -254,8 +254,8 @@ void main() {
MoveEffect.to(Vector2(x2, y2), duration(1)),
MoveEffect.to(Vector2(x3, y3), duration(1)),
],
alternate: true,
repeatCount: 2,
alternate: true,
),
MoveEffect.by(Vector2(x4 - x1, y4 - y1), duration(2)),
SequenceEffect(
Expand All @@ -264,9 +264,9 @@ void main() {
MoveEffect.by(Vector2(0, dy5), duration(1)),
],
repeatCount: 5,
alternate: true,
),
],
alternate: true,
);
expect(effect.controller.duration, 42);

Expand Down
43 changes: 43 additions & 0 deletions packages/flame_tiled/example/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Miscellaneous
TheMaverickProgrammer marked this conversation as resolved.
Show resolved Hide resolved
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.pub-cache/
.pub/
/build/

# Symbolication related
app.*.symbols

# Obfuscation related
app.*.map.json

# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
16 changes: 8 additions & 8 deletions packages/flame_tiled/example/.metadata
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled.
# This file should be version controlled and should not be manually edited.

version:
revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
channel: stable
revision: "2663184aa79047d0a33a14a3b607954f8fdd8730"
channel: "stable"

project_type: app

# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
- platform: linux
create_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
base_revision: d3d8effc686d73e0114d71abdcccef63fa1f25d2
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
- platform: web
create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730
base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730

# User provided section

Expand Down
Loading
Loading