Skip to content

Commit

Permalink
fix: Invoke setToStart on child effect controller of wrapping effec…
Browse files Browse the repository at this point in the history
…t controllers (#3168)

`DelayedEffectController` was not calling `setToStart` on its child,
causing it to not behave as expected when used with
`InfiniteEffectController`. This was reported by a discord member [in
this
message](https://discord.com/channels/509714518008528896/516639688581316629/1242258377766080666).

This PR introduces a `HasSingleChildEffectController` mixin which can be
added to any effect controller that wraps a single child effect
controller. This mixin makes sure that the `setToStart`, `setToEnd` and
`onMount` methods always get invoked on the child controllers.
  • Loading branch information
ufrshubham committed May 24, 2024
1 parent ffba0f9 commit 217c95f
Show file tree
Hide file tree
Showing 8 changed files with 131 additions and 57 deletions.
1 change: 1 addition & 0 deletions packages/flame/lib/effects.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export 'src/effects/controllers/duration_effect_controller.dart';
export 'src/effects/controllers/effect_controller.dart';
export 'src/effects/controllers/infinite_effect_controller.dart';
export 'src/effects/controllers/linear_effect_controller.dart';
export 'src/effects/controllers/mixins/has_single_child_effect_controller.dart';
export 'src/effects/controllers/pause_effect_controller.dart';
export 'src/effects/controllers/random_effect_controller.dart';
export 'src/effects/controllers/repeated_effect_controller.dart';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import 'package:flame/src/effects/controllers/effect_controller.dart';
import 'package:flame/src/effects/effect.dart';
import 'package:flame/effects.dart';

/// An effect controller that waits for [delay] seconds before running the
/// child controller. While waiting, the progress will be reported at 0.
class DelayedEffectController extends EffectController {
class DelayedEffectController extends EffectController
with HasSingleChildEffectController {
DelayedEffectController(EffectController child, {required this.delay})
: assert(delay >= 0, 'Delay must be non-negative: $delay'),
_child = child,
Expand All @@ -14,6 +14,9 @@ class DelayedEffectController extends EffectController {
final double delay;
double _timer;

@override
EffectController get child => _child;

@override
bool get isRandom => _child.isRandom;

Expand Down Expand Up @@ -65,14 +68,12 @@ class DelayedEffectController extends EffectController {
@override
void setToStart() {
_timer = 0;
super.setToStart();
}

@override
void setToEnd() {
_timer = delay;
_child.setToEnd();
super.setToEnd();
}

@override
void onMount(Effect parent) => _child.onMount(parent);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import 'package:flame/src/effects/controllers/effect_controller.dart';
import 'package:flame/src/effects/effect.dart';
import 'package:flame/effects.dart';

/// Effect controller that wraps a [child] effect controller and repeats it
/// infinitely.
class InfiniteEffectController extends EffectController {
InfiniteEffectController(this.child) : super.empty();
class InfiniteEffectController extends EffectController
with HasSingleChildEffectController {
InfiniteEffectController(EffectController child)
: _child = child,
super.empty();

final EffectController child;
final EffectController _child;

@override
EffectController get child => _child;

@override
bool get completed => false;
Expand Down Expand Up @@ -45,17 +50,4 @@ class InfiniteEffectController extends EffectController {
}
return 0;
}

@override
void setToStart() {
child.setToStart();
}

@override
void setToEnd() {
child.setToEnd();
}

@override
void onMount(Effect parent) => child.onMount(parent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flame/effects.dart';
import 'package:meta/meta.dart';

/// This mixin must be used with [EffectController]s that wrap a single child
/// effect controller of type [T].
mixin HasSingleChildEffectController<T extends EffectController>
on EffectController {
/// Returns the wrapped child effect controller.
T get child;

@mustCallSuper
@override
void setToStart() {
child.setToStart();
}

@mustCallSuper
@override
void setToEnd() {
child.setToEnd();
}

@mustCallSuper
@override
void onMount(Effect parent) {
child.onMount(parent);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import 'dart:math';

import 'package:flame/src/effects/controllers/duration_effect_controller.dart';
import 'package:flame/src/effects/controllers/effect_controller.dart';
import 'package:flame/src/effects/effect.dart';
import 'package:flame/effects.dart';

/// An [EffectController] that wraps another effect controller [child] and
/// randomizes its duration after each reset.
Expand All @@ -14,9 +12,11 @@ import 'package:flame/src/effects/effect.dart';
/// The child's duration is randomized first at construction, and then at each
/// reset (`setToStart`). Thus, the child has a concrete well-defined duration
/// at any point in time.
class RandomEffectController extends EffectController {
RandomEffectController(this.child, this.randomGenerator)
class RandomEffectController extends EffectController
with HasSingleChildEffectController<DurationEffectController> {
RandomEffectController(DurationEffectController child, this.randomGenerator)
: assert(!child.isInfinite, 'Child cannot be infinite'),
_child = child,
super.empty() {
_initializeDuration();
}
Expand Down Expand Up @@ -52,9 +52,12 @@ class RandomEffectController extends EffectController {
);
}

final DurationEffectController child;
final DurationEffectController _child;
final RandomVariable randomGenerator;

@override
DurationEffectController get child => _child;

@override
bool get isRandom => true;

Expand All @@ -73,18 +76,12 @@ class RandomEffectController extends EffectController {
@override
double recede(double dt) => child.recede(dt);

@override
void setToEnd() => child.setToEnd();

@override
void setToStart() {
child.setToStart();
super.setToStart();
_initializeDuration();
}

@override
void onMount(Effect parent) => child.onMount(parent);

void _initializeDuration() {
final duration = randomGenerator.nextValue();
assert(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
import 'package:flame/src/effects/controllers/effect_controller.dart';
import 'package:flame/src/effects/effect.dart';
import 'package:flame/effects.dart';

/// Effect controller that repeats [child] controller a certain number of times.
///
/// The [repeatCount] must be positive, and [child] controller cannot be
/// infinite. The child controller will be reset after each iteration (except
/// the last).
class RepeatedEffectController extends EffectController {
RepeatedEffectController(this.child, this.repeatCount)
class RepeatedEffectController extends EffectController
with HasSingleChildEffectController {
RepeatedEffectController(EffectController child, this.repeatCount)
: assert(repeatCount > 0, 'repeatCount must be positive'),
assert(!child.isInfinite, 'child cannot be infinite'),
_child = child,
_remainingCount = repeatCount,
super.empty();

final EffectController child;
final EffectController _child;
final int repeatCount;

/// How many iterations this controller has remaining. When this reaches 0
/// the controller is considered completed.
int get remainingIterationsCount => _remainingCount;
int _remainingCount;

@override
EffectController get child => _child;

@override
double get progress => child.progress;

Expand Down Expand Up @@ -74,15 +78,12 @@ class RepeatedEffectController extends EffectController {
@override
void setToStart() {
_remainingCount = repeatCount;
child.setToStart();
super.setToStart();
}

@override
void setToEnd() {
_remainingCount = 0;
child.setToEnd();
super.setToEnd();
}

@override
void onMount(Effect parent) => child.onMount(parent);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import 'package:flame/src/effects/controllers/duration_effect_controller.dart';
import 'package:flame/src/effects/controllers/effect_controller.dart';
import 'package:flame/src/effects/effect.dart';
import 'package:flame/effects.dart';
import 'package:flame/src/effects/measurable_effect.dart';

/// This controller can force execution of an effect at a predefined speed.
Expand All @@ -15,12 +13,14 @@ import 'package:flame/src/effects/measurable_effect.dart';
/// - the [speed] cannot be zero (or negative),
/// - the [child] controller must be a [DurationEffectController],
/// - the parent effect must be a [MeasurableEffect].
class SpeedEffectController extends EffectController {
SpeedEffectController(this.child, {required this.speed})
class SpeedEffectController extends EffectController
with HasSingleChildEffectController<DurationEffectController> {
SpeedEffectController(DurationEffectController child, {required this.speed})
: assert(speed > 0, 'Speed must be positive: $speed'),
_child = child,
super.empty();

final DurationEffectController child;
final DurationEffectController _child;
final double speed;
late MeasurableEffect _parentEffect;

Expand All @@ -30,6 +30,9 @@ class SpeedEffectController extends EffectController {
/// (which will happen at the first call to `advance()`).
bool _initialized = false;

@override
DurationEffectController get child => _child;

@override
bool get isRandom => true;

Expand Down Expand Up @@ -68,13 +71,13 @@ class SpeedEffectController extends EffectController {
@override
void setToEnd() {
_initialized = false;
child.setToEnd();
super.setToEnd();
}

@override
void setToStart() {
_initialized = false;
child.setToStart();
super.setToStart();
}

@override
Expand All @@ -84,6 +87,6 @@ class SpeedEffectController extends EffectController {
'SpeedEffectController can only be applied to a MeasurableEffect',
);
_parentEffect = parent as MeasurableEffect;
child.onMount(parent);
super.onMount(parent);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import 'package:flame/effects.dart';
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';

void main() {
group('HasSingleChildEffectController', () {
test('child getter should return the wrapped child effect controller', () {
final childController = _MockEffectController();
final controller = _TestEffectController(childController);

expect(controller.child, equals(childController));
});

test('setToStart should call setToStart on the child effect controller',
() {
final childController = _MockEffectController();
final controller = _TestEffectController(childController);
controller.setToStart();
verify(childController.setToStart).called(1);
});

test('setToEnd should call setToEnd on the child effect controller', () {
final childController = _MockEffectController();
final controller = _TestEffectController(childController);
controller.setToEnd();
verify(childController.setToEnd).called(1);
});

test('onMount should call onMount on the child effect controller', () {
final childController = _MockEffectController();
final controller = _TestEffectController(childController);
final parentEffect = _MockEffect();
controller.onMount(parentEffect);
verify(() => childController.onMount(parentEffect)).called(1);
});
});
}

class _TestEffectController extends _MockEffectController
with HasSingleChildEffectController<_MockEffectController> {
_TestEffectController(_MockEffectController child) : _child = child;

final _MockEffectController _child;

@override
_MockEffectController get child => _child;
}

class _MockEffectController extends Mock implements EffectController {}

class _MockEffect extends Mock implements Effect {}

0 comments on commit 217c95f

Please sign in to comment.