diff --git a/examples/lib/stories/effects/sequence_effect_example.dart b/examples/lib/stories/effects/sequence_effect_example.dart index ea3f15e73b0..868257a15cb 100644 --- a/examples/lib/stories/effects/sequence_effect_example.dart +++ b/examples/lib/stories/effects/sequence_effect_example.dart @@ -29,6 +29,7 @@ class SequenceEffectExample extends FlameGame { MoveEffect.to(Vector2(400, 500), duration(0.7)), ], alternate: true, + alternatePattern: AlternatePattern.doNotRepeatLast, infinite: true, ), ), diff --git a/packages/flame/lib/effects.dart b/packages/flame/lib/effects.dart index d3382e11e44..03153237c05 100644 --- a/packages/flame/lib/effects.dart +++ b/packages/flame/lib/effects.dart @@ -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'; diff --git a/packages/flame/lib/src/effects/controllers/duration_effect_controller.dart b/packages/flame/lib/src/effects/controllers/duration_effect_controller.dart index 04df91a111d..52894d07dd4 100644 --- a/packages/flame/lib/src/effects/controllers/duration_effect_controller.dart +++ b/packages/flame/lib/src/effects/controllers/duration_effect_controller.dart @@ -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; } diff --git a/packages/flame/lib/src/effects/effect.dart b/packages/flame/lib/src/effects/effect.dart index 7ed3bf6dca4..b9edb0dd865 100644 --- a/packages/flame/lib/src/effects/effect.dart +++ b/packages/flame/lib/src/effects/effect.dart @@ -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) { diff --git a/packages/flame/lib/src/effects/move_to_effect.dart b/packages/flame/lib/src/effects/move_to_effect.dart index 81e86db8612..444b7c6c934 100644 --- a/packages/flame/lib/src/effects/move_to_effect.dart +++ b/packages/flame/lib/src/effects/move_to_effect.dart @@ -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(); } @override diff --git a/packages/flame/lib/src/effects/sequence_effect.dart b/packages/flame/lib/src/effects/sequence_effect.dart index 625146dd5ce..40ff3597932 100644 --- a/packages/flame/lib/src/effects/sequence_effect.dart +++ b/packages/flame/lib/src/effects/sequence_effect.dart @@ -6,12 +6,14 @@ import 'package:flame/src/effects/effect.dart'; EffectController _createController({ required List 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); @@ -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 @@ -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 @@ -48,6 +72,7 @@ EffectController _createController({ class SequenceEffect extends Effect { SequenceEffect( List effects, { + AlternatePattern alternatePattern = AlternatePattern.repeatLast, bool alternate = false, bool infinite = false, int repeatCount = 1, @@ -63,6 +88,7 @@ class SequenceEffect extends Effect { _createController( effects: effects, alternate: alternate, + alternatePattern: alternatePattern, infinite: infinite, repeatCount: repeatCount, ), @@ -93,6 +119,7 @@ class _SequenceEffectEffectController extends EffectController { _SequenceEffectEffectController( this.effects, { required this.alternate, + required this.alternatePattern, }) : super.empty(); /// The list of children effects. @@ -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, @@ -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; @@ -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; + totalDuration -= effects.last.controller.duration ?? 0; + } } + return totalDuration; } @@ -136,7 +197,7 @@ class _SequenceEffectEffectController extends EffectController { } @override - double get progress => (_index + 1) / n; + double get progress => (_index < 0 ? -_index : _index + 1) / n; @override double advance(double dt) { @@ -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; @@ -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; } diff --git a/packages/flame/test/effects/sequence_effect_test.dart b/packages/flame/test/effects/sequence_effect_test.dart index dc6240b74f4..8e1fe0add18 100644 --- a/packages/flame/test/effects/sequence_effect_test.dart +++ b/packages/flame/test/effects/sequence_effect_test.dart @@ -55,8 +55,8 @@ void main() { ); final effect = SequenceEffect( [randomEffect], - alternate: true, repeatCount: 1000, + alternate: true, ); expect( effect.controller.duration, @@ -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( @@ -264,9 +264,9 @@ void main() { MoveEffect.by(Vector2(0, dy5), duration(1)), ], repeatCount: 5, + alternate: true, ), ], - alternate: true, ); expect(effect.controller.duration, 42); diff --git a/packages/flame_tiled/example/.gitignore b/packages/flame_tiled/example/.gitignore new file mode 100644 index 00000000000..29a3a5017f0 --- /dev/null +++ b/packages/flame_tiled/example/.gitignore @@ -0,0 +1,43 @@ +# Miscellaneous +*.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 diff --git a/packages/flame_tiled/example/.metadata b/packages/flame_tiled/example/.metadata index 90afe1c32f4..8dda3beb859 100644 --- a/packages/flame_tiled/example/.metadata +++ b/packages/flame_tiled/example/.metadata @@ -1,11 +1,11 @@ # 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 @@ -13,11 +13,11 @@ project_type: app 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 diff --git a/packages/flame_tiled/example/lib/main.dart b/packages/flame_tiled/example/lib/main.dart index 639cab92aed..c8951febae7 100644 --- a/packages/flame_tiled/example/lib/main.dart +++ b/packages/flame_tiled/example/lib/main.dart @@ -1,5 +1,8 @@ +import 'dart:math'; + import 'package:flame/components.dart'; import 'package:flame/effects.dart'; +import 'package:flame/experimental.dart'; import 'package:flame/flame.dart'; import 'package:flame/game.dart'; import 'package:flame_tiled/flame_tiled.dart'; @@ -22,23 +25,36 @@ class TiledGame extends FlameGame { @override Future onLoad() async { - camera.viewfinder - ..zoom = 0.5 - ..anchor = Anchor.topLeft - ..add( - MoveToEffect( - Vector2(1000, 0), - EffectController( - duration: 10, - alternate: true, - infinite: true, - ), - ), - ); - mapComponent = await TiledComponent.load('map.tmx', Vector2.all(16)); world.add(mapComponent); + // Create a camera-panning sequence across the 4 corners + // of the map + camera.viewfinder.add( + SequenceEffect( + [ + for (int i = 0, j = 0; j < 2; i++, j = i ~/ 2) + MoveToEffect( + Vector2( + mapComponent.width * min(1, i % 3), + mapComponent.height * j, + ), + EffectController( + duration: 3, + ), + ), + ], + alternate: true, + alternatePattern: AlternatePattern.doNotRepeatLast, + infinite: true, + ), + ); + + camera.setBounds( + Rectangle.fromLTRB(0, 0, mapComponent.width, mapComponent.height), + considerViewport: true, + ); + final objectGroup = mapComponent.tileMap.getLayer('AnimatedCoins'); final coins = await Flame.images.load('coins.png');