feat: Add RandomEffectController (#1203)

* Create RandomEffectController

* added tests

* added example

* documentation

* formatting

* fix a test

* formatting

Co-authored-by: Erick <erickzanardoo@gmail.com>
This commit is contained in:
Pasha Stetsenko
2021-12-12 15:38:05 -08:00
committed by GitHub
parent 3b20129c8b
commit cdb2650b29
6 changed files with 293 additions and 11 deletions

View File

@ -31,7 +31,6 @@ the final value is provided by the user explicitly, and progression over time is
There are multiple effects provided by Flame, and you can also
[create your own](#creating-new-effects). The following effects are included:
- [`ColorEffect`](#coloreffect)
- [`MoveEffect.by`](#moveeffectby)
- [`MoveEffect.to`](#moveeffectto)
- [`MoveAlongPathEffect`](#movealongpatheffect)
@ -42,6 +41,7 @@ There are multiple effects provided by Flame, and you can also
- [`SizeEffect.by`](#sizeeffectby)
- [`SizeEffect.to`](#sizeeffectto)
- [`OpacityEffect`](#opacityeffect)
- [`ColorEffect`](#coloreffect)
- [`RemoveEffect`](#removeeffect)
An `EffectController` is an object that describes how the effect should evolve over time. If you
@ -60,6 +60,7 @@ There are multiple effect controllers provided by the Flame framework as well:
- [`InfiniteEffectController`](#infiniteeffectcontroller)
- [`SequenceEffectController`](#sequenceeffectcontroller)
- [`DelayedEffectController`](#delayedeffectcontroller)
- [`RandomEffectController`](#randomeffectcontroller)
## Built-in effects
@ -234,12 +235,10 @@ the provided color between a provided range.
Usage example:
```dart
myComponent.add(
ColorEffect(
const Color(0xFF00FF00),
const Offset(0.0, 0.8),
EffectController(duration: 1.5),
),
final effect = ColorEffect(
const Color(0xFF00FF00),
const Offset(0.0, 0.8),
EffectController(duration: 1.5),
);
```
@ -250,6 +249,7 @@ __Note :__Due to how this effect is implemented, and how Flutter's `ColorFilter`
effect can't be mixed with other `ColorEffect`s, when more than one is added to the component, only
the last one will have effect.
## Creating new effects
Although Flame provides a wide array of built-in effects, eventually you may find them to be
@ -459,6 +459,25 @@ final ec = DelayedEffectController(LinearEffectController(1), delay: 5);
```
### `RandomEffectController`
This controller wraps another controller and makes its duration random. The actual value for the
duration is re-generated upon each reset, which makes this controller particularly useful within
repeated contexts, such as [](#repeatedeffectcontroller) or [](#infiniteeffectcontroller).
```dart
final effect = RandomEffectController.uniform(
LinearEffectController(0), // duration here is irrelevant
min: 0.5,
max: 1.5,
);
```
The user has the ability to control which `Random` source to use, as well as the exact distribution
of the produced random durations. Two distributions -- `.uniform` and `.exponential` are included,
any other can be implemented by the user.
## See also
* [Examples of various effects](https://examples.flame-engine.org/#/).

View File

@ -1,3 +1,5 @@
import 'dart:math';
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';
@ -7,10 +9,10 @@ import 'package:flutter/material.dart';
class ScaleEffectExample extends FlameGame with TapDetector {
static const String description = '''
The `ScaleEffect` scales up the canvas before drawing the components and its
children.
In this example you can tap the screen and the component will scale up or
down, depending on its current state.
The star pulsates randomly using a RandomEffectController.
''';
late RectangleComponent square;
@ -30,6 +32,26 @@ class ScaleEffectExample extends FlameGame with TapDetector {
);
square.add(childSquare);
add(square);
add(
Star()
..position = Vector2(200, 100)
..add(
ScaleEffect.to(
Vector2.all(1.2),
InfiniteEffectController(
SequenceEffectController([
LinearEffectController(0.1),
ReverseLinearEffectController(0.1),
RandomEffectController.exponential(
PauseEffectController(1, progress: 0),
beta: 1,
),
]),
),
),
),
);
}
@override
@ -49,3 +71,26 @@ class ScaleEffectExample extends FlameGame with TapDetector {
);
}
}
class Star extends PositionComponent {
Star() {
const smallR = 15.0;
const bigR = 30.0;
const tau = 2 * pi;
shape = Path()..moveTo(bigR, 0);
for (var i = 1; i < 10; i++) {
final r = i.isEven ? bigR : smallR;
final a = i / 10 * tau;
shape.lineTo(r * cos(a), r * sin(a));
}
shape.close();
}
late final Path shape;
late final Paint paint = Paint()..color = const Color(0xFFFFF127);
@override
void render(Canvas canvas) {
canvas.drawPath(shape, paint);
}
}

View File

@ -7,6 +7,7 @@ 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/pause_effect_controller.dart';
export 'src/effects/controllers/random_effect_controller.dart';
export 'src/effects/controllers/repeated_effect_controller.dart';
export 'src/effects/controllers/reverse_curved_effect_controller.dart';
export 'src/effects/controllers/reverse_linear_effect_controller.dart';

View File

@ -123,8 +123,8 @@ abstract class EffectController {
/// Is the effect's duration random or fixed?
bool get isRandom => false;
/// Total duration of the effect. If the effect is either infinite or random,
/// this will return `null`.
/// Total duration of the effect. If the duration cannot be determined, this
/// will return `null`.
double? get duration;
/// Has the effect started running? Some effects use a "delay" parameter to

View File

@ -0,0 +1,132 @@
import 'dart:math';
import 'duration_effect_controller.dart';
import 'effect_controller.dart';
/// An [EffectController] that wraps another effect controller [child] and
/// randomizes its duration after each reset.
///
/// This effect controller works best in contexts were it has a chance to be
/// executed multiple times, such as within a `RepeatedEffectController`, or
/// `InfiniteEffectController`, etc.
///
/// 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)
: assert(!child.isInfinite, 'Child cannot be infinite'),
super.empty() {
_initializeDuration();
}
/// Factory constructor that uses a random variable uniformly distributed on
/// `[min, max)`.
factory RandomEffectController.uniform(
DurationEffectController child, {
required double min,
required double max,
Random? random,
}) {
assert(min >= 0, 'Min value cannot be negative: $min');
assert(min < max, 'Max value must exceed min: max=$max, min=$min');
return RandomEffectController(
child,
_UniformRandomVariable(min, max, random),
);
}
/// Factory constructor that employs a random variable distributed
/// exponentially with rate parameter `beta`. The produced random values will
/// have the average duration of `beta`.
factory RandomEffectController.exponential(
DurationEffectController child, {
required double beta,
Random? random,
}) {
assert(beta > 0, 'Beta must be positive: $beta');
return RandomEffectController(
child,
_ExponentialRandomVariable(beta, random),
);
}
final DurationEffectController child;
final RandomVariable randomGenerator;
@override
bool get isInfinite => false;
@override
bool get isRandom => true;
@override
bool get completed => child.completed;
@override
double? get duration => child.duration;
@override
double get progress => child.progress;
@override
double advance(double dt) => child.advance(dt);
@override
double recede(double dt) => child.recede(dt);
@override
void setToEnd() => child.setToEnd();
@override
void setToStart() {
child.setToStart();
_initializeDuration();
}
void _initializeDuration() {
final duration = randomGenerator.nextValue();
assert(
duration >= 0,
'Random generator produced a negative value: $duration',
);
child.duration = duration;
}
}
/// [RandomVariable] is an object capable of producing random values with the
/// prescribed distribution function. Each distribution is implemented within
/// its own derived class.
abstract class RandomVariable {
RandomVariable(Random? random) : _random = random ?? _defaultRandom;
/// Internal random number generator.
final Random _random;
static final Random _defaultRandom = Random();
/// Produces the next value for this random variable.
double nextValue();
}
/// Random variable distributed uniformly between [min] and [max].
class _UniformRandomVariable extends RandomVariable {
_UniformRandomVariable(this.min, this.max, Random? random) : super(random);
final double min;
final double max;
@override
double nextValue() => _random.nextDouble() * (max - min) + min;
}
/// Exponentially distributed random variable with rate parameter [beta].
class _ExponentialRandomVariable extends RandomVariable {
_ExponentialRandomVariable(this.beta, Random? random) : super(random);
/// Rate parameter of the exponential distribution. This will be the average
/// of all returned values
final double beta;
@override
double nextValue() => -log(1 - _random.nextDouble()) * beta;
}

View File

@ -0,0 +1,85 @@
import 'dart:math';
import 'package:flame/effects.dart';
import 'package:test/test.dart';
class MyRandom implements Random {
double value = 0.5;
@override
double nextDouble() => value;
@override
bool nextBool() => true;
@override
int nextInt(int max) => 1;
}
class MyRandomVariable extends RandomVariable {
MyRandomVariable() : super(null);
double value = 1.23;
@override
double nextValue() => value;
}
void main() {
group('RandomEffectController', () {
test('custom random', () {
final randomVariable = MyRandomVariable();
final ec = RandomEffectController(
LinearEffectController(1000),
randomVariable,
);
expect(ec.duration, 1.23);
expect(ec.isRandom, true);
expect(ec.isInfinite, false);
expect(ec.progress, 0);
expect(ec.started, true);
expect(ec.completed, false);
expect(ec.advance(1), 0);
expect(ec.advance(0.23), 0);
expect(ec.completed, true);
expect(ec.advance(1), 1);
expect(ec.duration, 1.23);
});
test('.uniform', () {
final random = MyRandom();
final ec = RandomEffectController.uniform(
LinearEffectController(1000),
min: 0,
max: 10,
random: random,
);
expect(random.nextDouble(), 0.5);
expect(ec.duration, 5);
random.value = 0;
ec.setToStart();
expect(ec.duration, 0);
random.value = 1;
ec.setToStart();
expect(ec.duration, 10);
});
test('.exponential', () {
const n = 1000;
final random = MyRandom();
final ec = RandomEffectController.exponential(
LinearEffectController(1e6),
beta: 42,
random: random,
);
var sum = 0.0;
for (var i = 0; i < n; i++) {
random.value = i / n;
ec.setToStart();
expect(ec.duration! >= 0, true);
sum += ec.duration!;
}
expect(sum / n, closeTo(42, 400 / n));
});
});
}