mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 03:15:43 +08:00
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:
@ -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/#/).
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
}
|
||||
@ -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));
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user