feat: The FunctionEffect, run any function as an Effect (#3537)

The `FunctionEffect` is super simple, but very powerful.
It makes it possible to run any function over time as an effect without
having to create a new effect.
This is very useful when for example doing state changes over time.
This commit is contained in:
Lukas Klingsbo
2025-03-25 10:32:43 +01:00
committed by GitHub
parent e685649b85
commit f4ac1ec63a
6 changed files with 215 additions and 0 deletions

View File

@ -49,6 +49,7 @@ There are multiple effects provided by Flame, and you can also
- [`ColorEffect`](#coloreffect) - [`ColorEffect`](#coloreffect)
- [`SequenceEffect`](#sequenceeffect) - [`SequenceEffect`](#sequenceeffect)
- [`RemoveEffect`](#removeeffect) - [`RemoveEffect`](#removeeffect)
- [`FunctionEffect`](#functioneffect)
An `EffectController` is an object that describes how the effect should evolve over time. If you An `EffectController` is an object that describes how the effect should evolve over time. If you
think of the initial value of the effect as 0% progress, and the final value as 100% progress, then think of the initial value of the effect as 0% progress, and the final value as 100% progress, then
@ -575,6 +576,43 @@ effect can't be mixed with other `ColorEffect`s, when more than one is added to
the last one will have effect. the last one will have effect.
### `FunctionEffect`
The `FunctionEffect` class is a very generic Effect that allows you to do almost anything without
having to define a new effect.
It runs a function that takes the target and the progress of the effect and then the user can
decide what to do with that input.
This could for example be used to make game state changes that happen over time, but that isn't
necessarily visual, like most other effects are.
In the following example we have a `PlayerState` enum that we want to change over time. We want to
change the state to `yawn` when the progress is over 50% and then back to `idle` when the progress
is over 80%.
```dart
enum PlayerState {
idle,
yawn,
}
final effect = FunctionEffect<SpriteAnimationGroupComponent<PlayerState>>(
(target, progress) {
if (progress > 0.5) {
target.current = PlayerState.yawn;
} else if(progress > 0.8) {
target.current = PlayerState.idle;
}
},
EffectController(
duration: 10,
infinite: true,
),
);
```
## Creating new effects ## Creating new effects
Although Flame provides a wide array of built-in effects, eventually you may find them to be Although Flame provides a wide array of built-in effects, eventually you may find them to be

View File

@ -3,6 +3,7 @@ import 'package:examples/commons/commons.dart';
import 'package:examples/stories/effects/color_effect_example.dart'; import 'package:examples/stories/effects/color_effect_example.dart';
import 'package:examples/stories/effects/dual_effect_removal_example.dart'; import 'package:examples/stories/effects/dual_effect_removal_example.dart';
import 'package:examples/stories/effects/effect_controllers_example.dart'; import 'package:examples/stories/effects/effect_controllers_example.dart';
import 'package:examples/stories/effects/function_effect_example.dart';
import 'package:examples/stories/effects/move_effect_example.dart'; import 'package:examples/stories/effects/move_effect_example.dart';
import 'package:examples/stories/effects/opacity_effect_example.dart'; import 'package:examples/stories/effects/opacity_effect_example.dart';
import 'package:examples/stories/effects/remove_effect_example.dart'; import 'package:examples/stories/effects/remove_effect_example.dart';
@ -75,6 +76,12 @@ void addEffectsStories(Dashbook dashbook) {
codeLink: baseLink('effects/remove_effect_example.dart'), codeLink: baseLink('effects/remove_effect_example.dart'),
info: RemoveEffectExample.description, info: RemoveEffectExample.description,
) )
..add(
'Function Effect',
(_) => GameWidget(game: FunctionEffectExample()),
codeLink: baseLink('effects/function_effect_example.dart'),
info: FunctionEffectExample.description,
)
..add( ..add(
'EffectControllers', 'EffectControllers',
(_) => GameWidget(game: EffectControllersExample()), (_) => GameWidget(game: EffectControllersExample()),

View File

@ -0,0 +1,64 @@
import 'package:flame/components.dart';
import 'package:flame/effects.dart';
import 'package:flame/game.dart';
import 'package:flame/input.dart';
enum RobotState {
idle,
running,
}
class FunctionEffectExample extends FlameGame with TapDetector {
static const String description = '''
This example shows how to use the FunctionEffect to create custom effects.
The robot will switch between running and idle animations over the duration of
10 seconds.
''';
@override
Future<void> onLoad() async {
final running = await loadSpriteAnimation(
'animations/robot.png',
SpriteAnimationData.sequenced(
amount: 8,
stepTime: 0.2,
textureSize: Vector2(16, 18),
),
);
final idle = await loadSpriteAnimation(
'animations/robot-idle.png',
SpriteAnimationData.sequenced(
amount: 4,
stepTime: 0.4,
textureSize: Vector2(16, 18),
),
);
final robotSize = Vector2(64, 72);
final functionEffect =
FunctionEffect<SpriteAnimationGroupComponent<RobotState>>(
(target, progress) {
if (progress > 0.7) {
target.current = RobotState.idle;
} else if (progress > 0.3) {
target.current = RobotState.running;
}
},
EffectController(duration: 10.0, infinite: true),
);
final component = SpriteAnimationGroupComponent<RobotState>(
animations: {
RobotState.running: running,
RobotState.idle: idle,
},
current: RobotState.idle,
position: size / 2,
anchor: Anchor.center,
size: robotSize,
children: [functionEffect],
);
add(component);
}
}

View File

@ -21,6 +21,7 @@ export 'src/effects/controllers/speed_effect_controller.dart';
export 'src/effects/controllers/zigzag_effect_controller.dart'; export 'src/effects/controllers/zigzag_effect_controller.dart';
export 'src/effects/effect.dart'; export 'src/effects/effect.dart';
export 'src/effects/effect_target.dart'; export 'src/effects/effect_target.dart';
export 'src/effects/function_effect.dart';
export 'src/effects/glow_effect.dart'; export 'src/effects/glow_effect.dart';
export 'src/effects/move_along_path_effect.dart'; export 'src/effects/move_along_path_effect.dart';
export 'src/effects/move_by_effect.dart'; export 'src/effects/move_by_effect.dart';

View File

@ -0,0 +1,26 @@
import 'package:flame/src/effects/effect.dart';
import 'package:flame/src/effects/effect_target.dart';
/// The `FunctionEffect` class is a very generic Effect that allows you to
/// do almost anything without having to define a new effect.
///
/// It runs a function that takes the target and the progress of the effect and
/// then the user can decide what to do with that input.
///
/// This could for example be used to make game state changes that happen over
/// time, but that isn't necessarily visual, like most other effects are.
class FunctionEffect<T> extends Effect with EffectTarget<T> {
FunctionEffect(
this.function,
super.controller, {
super.onComplete,
super.key,
});
void Function(T target, double progress) function;
@override
void apply(double progress) {
function(target, progress);
}
}

View File

@ -0,0 +1,79 @@
import 'package:flame/components.dart';
import 'package:flame/src/effects/controllers/effect_controller.dart';
import 'package:flame/src/effects/function_effect.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('FunctionEffect', () {
testWithFlameGame('applies function correctly', (game) async {
final effect = FunctionEffect<PositionComponent>(
(target, progress) {
target.x = progress * 100;
},
EffectController(duration: 1),
);
final component = PositionComponent(children: [effect]);
await game.ensureAdd(component);
effect.update(0);
expect(component.x, 0);
effect.update(0.5);
expect(component.x, 50);
effect.update(0.5);
expect(component.x, 100);
});
testWithFlameGame('completes correctly', (game) async {
final effect = FunctionEffect<PositionComponent>(
(target, progress) {
target.x = progress * 100;
},
EffectController(duration: 1),
);
final component = PositionComponent(children: [effect]);
await game.ensureAdd(component);
effect.update(1);
expect(component.x, 100);
expect(effect.controller.completed, true);
});
testWithFlameGame('removes on finish', (game) async {
final effect = FunctionEffect<PositionComponent>(
(target, progress) {
target.x = progress * 100;
},
EffectController(duration: 1),
);
final component = PositionComponent(children: [effect]);
await game.ensureAdd(component);
expect(component.children.length, 1);
game.update(1);
expect(effect.controller.completed, true);
game.update(0);
expect(component.children.length, 0);
});
testWithFlameGame('does not remove on finish', (game) async {
final effect = FunctionEffect<PositionComponent>(
(target, progress) {
target.x = progress * 100;
},
EffectController(duration: 1),
);
effect.removeOnFinish = false;
final component = PositionComponent(children: [effect]);
await game.ensureAdd(component);
expect(component.children.length, 1);
game.update(1);
expect(effect.controller.completed, true);
game.update(0);
expect(component.children.length, 1);
});
});
}