Skip to content

Commit f4ac1ec

Browse files
authored
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.
1 parent e685649 commit f4ac1ec

File tree

6 files changed

+215
-0
lines changed

6 files changed

+215
-0
lines changed

Diff for: doc/flame/effects.md

+38
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ There are multiple effects provided by Flame, and you can also
4949
- [`ColorEffect`](#coloreffect)
5050
- [`SequenceEffect`](#sequenceeffect)
5151
- [`RemoveEffect`](#removeeffect)
52+
- [`FunctionEffect`](#functioneffect)
5253

5354
An `EffectController` is an object that describes how the effect should evolve over time. If you
5455
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
575576
the last one will have effect.
576577

577578

579+
### `FunctionEffect`
580+
581+
The `FunctionEffect` class is a very generic Effect that allows you to do almost anything without
582+
having to define a new effect.
583+
584+
It runs a function that takes the target and the progress of the effect and then the user can
585+
decide what to do with that input.
586+
587+
This could for example be used to make game state changes that happen over time, but that isn't
588+
necessarily visual, like most other effects are.
589+
590+
In the following example we have a `PlayerState` enum that we want to change over time. We want to
591+
change the state to `yawn` when the progress is over 50% and then back to `idle` when the progress
592+
is over 80%.
593+
594+
```dart
595+
enum PlayerState {
596+
idle,
597+
yawn,
598+
}
599+
600+
final effect = FunctionEffect<SpriteAnimationGroupComponent<PlayerState>>(
601+
(target, progress) {
602+
if (progress > 0.5) {
603+
target.current = PlayerState.yawn;
604+
} else if(progress > 0.8) {
605+
target.current = PlayerState.idle;
606+
}
607+
},
608+
EffectController(
609+
duration: 10,
610+
infinite: true,
611+
),
612+
);
613+
```
614+
615+
578616
## Creating new effects
579617

580618
Although Flame provides a wide array of built-in effects, eventually you may find them to be

Diff for: examples/lib/stories/effects/effects.dart

+7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import 'package:examples/commons/commons.dart';
33
import 'package:examples/stories/effects/color_effect_example.dart';
44
import 'package:examples/stories/effects/dual_effect_removal_example.dart';
55
import 'package:examples/stories/effects/effect_controllers_example.dart';
6+
import 'package:examples/stories/effects/function_effect_example.dart';
67
import 'package:examples/stories/effects/move_effect_example.dart';
78
import 'package:examples/stories/effects/opacity_effect_example.dart';
89
import 'package:examples/stories/effects/remove_effect_example.dart';
@@ -75,6 +76,12 @@ void addEffectsStories(Dashbook dashbook) {
7576
codeLink: baseLink('effects/remove_effect_example.dart'),
7677
info: RemoveEffectExample.description,
7778
)
79+
..add(
80+
'Function Effect',
81+
(_) => GameWidget(game: FunctionEffectExample()),
82+
codeLink: baseLink('effects/function_effect_example.dart'),
83+
info: FunctionEffectExample.description,
84+
)
7885
..add(
7986
'EffectControllers',
8087
(_) => GameWidget(game: EffectControllersExample()),
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import 'package:flame/components.dart';
2+
import 'package:flame/effects.dart';
3+
import 'package:flame/game.dart';
4+
import 'package:flame/input.dart';
5+
6+
enum RobotState {
7+
idle,
8+
running,
9+
}
10+
11+
class FunctionEffectExample extends FlameGame with TapDetector {
12+
static const String description = '''
13+
This example shows how to use the FunctionEffect to create custom effects.
14+
15+
The robot will switch between running and idle animations over the duration of
16+
10 seconds.
17+
''';
18+
19+
@override
20+
Future<void> onLoad() async {
21+
final running = await loadSpriteAnimation(
22+
'animations/robot.png',
23+
SpriteAnimationData.sequenced(
24+
amount: 8,
25+
stepTime: 0.2,
26+
textureSize: Vector2(16, 18),
27+
),
28+
);
29+
final idle = await loadSpriteAnimation(
30+
'animations/robot-idle.png',
31+
SpriteAnimationData.sequenced(
32+
amount: 4,
33+
stepTime: 0.4,
34+
textureSize: Vector2(16, 18),
35+
),
36+
);
37+
final robotSize = Vector2(64, 72);
38+
39+
final functionEffect =
40+
FunctionEffect<SpriteAnimationGroupComponent<RobotState>>(
41+
(target, progress) {
42+
if (progress > 0.7) {
43+
target.current = RobotState.idle;
44+
} else if (progress > 0.3) {
45+
target.current = RobotState.running;
46+
}
47+
},
48+
EffectController(duration: 10.0, infinite: true),
49+
);
50+
final component = SpriteAnimationGroupComponent<RobotState>(
51+
animations: {
52+
RobotState.running: running,
53+
RobotState.idle: idle,
54+
},
55+
current: RobotState.idle,
56+
position: size / 2,
57+
anchor: Anchor.center,
58+
size: robotSize,
59+
children: [functionEffect],
60+
);
61+
62+
add(component);
63+
}
64+
}

Diff for: packages/flame/lib/effects.dart

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export 'src/effects/controllers/speed_effect_controller.dart';
2121
export 'src/effects/controllers/zigzag_effect_controller.dart';
2222
export 'src/effects/effect.dart';
2323
export 'src/effects/effect_target.dart';
24+
export 'src/effects/function_effect.dart';
2425
export 'src/effects/glow_effect.dart';
2526
export 'src/effects/move_along_path_effect.dart';
2627
export 'src/effects/move_by_effect.dart';

Diff for: packages/flame/lib/src/effects/function_effect.dart

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import 'package:flame/src/effects/effect.dart';
2+
import 'package:flame/src/effects/effect_target.dart';
3+
4+
/// The `FunctionEffect` class is a very generic Effect that allows you to
5+
/// do almost anything without having to define a new effect.
6+
///
7+
/// It runs a function that takes the target and the progress of the effect and
8+
/// then the user can decide what to do with that input.
9+
///
10+
/// This could for example be used to make game state changes that happen over
11+
/// time, but that isn't necessarily visual, like most other effects are.
12+
class FunctionEffect<T> extends Effect with EffectTarget<T> {
13+
FunctionEffect(
14+
this.function,
15+
super.controller, {
16+
super.onComplete,
17+
super.key,
18+
});
19+
20+
void Function(T target, double progress) function;
21+
22+
@override
23+
void apply(double progress) {
24+
function(target, progress);
25+
}
26+
}
+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import 'package:flame/components.dart';
2+
import 'package:flame/src/effects/controllers/effect_controller.dart';
3+
import 'package:flame/src/effects/function_effect.dart';
4+
import 'package:flame_test/flame_test.dart';
5+
import 'package:flutter_test/flutter_test.dart';
6+
7+
void main() {
8+
group('FunctionEffect', () {
9+
testWithFlameGame('applies function correctly', (game) async {
10+
final effect = FunctionEffect<PositionComponent>(
11+
(target, progress) {
12+
target.x = progress * 100;
13+
},
14+
EffectController(duration: 1),
15+
);
16+
final component = PositionComponent(children: [effect]);
17+
await game.ensureAdd(component);
18+
19+
effect.update(0);
20+
expect(component.x, 0);
21+
22+
effect.update(0.5);
23+
expect(component.x, 50);
24+
25+
effect.update(0.5);
26+
expect(component.x, 100);
27+
});
28+
29+
testWithFlameGame('completes correctly', (game) async {
30+
final effect = FunctionEffect<PositionComponent>(
31+
(target, progress) {
32+
target.x = progress * 100;
33+
},
34+
EffectController(duration: 1),
35+
);
36+
final component = PositionComponent(children: [effect]);
37+
await game.ensureAdd(component);
38+
39+
effect.update(1);
40+
expect(component.x, 100);
41+
expect(effect.controller.completed, true);
42+
});
43+
44+
testWithFlameGame('removes on finish', (game) async {
45+
final effect = FunctionEffect<PositionComponent>(
46+
(target, progress) {
47+
target.x = progress * 100;
48+
},
49+
EffectController(duration: 1),
50+
);
51+
final component = PositionComponent(children: [effect]);
52+
await game.ensureAdd(component);
53+
54+
expect(component.children.length, 1);
55+
game.update(1);
56+
expect(effect.controller.completed, true);
57+
game.update(0);
58+
expect(component.children.length, 0);
59+
});
60+
61+
testWithFlameGame('does not remove on finish', (game) async {
62+
final effect = FunctionEffect<PositionComponent>(
63+
(target, progress) {
64+
target.x = progress * 100;
65+
},
66+
EffectController(duration: 1),
67+
);
68+
effect.removeOnFinish = false;
69+
final component = PositionComponent(children: [effect]);
70+
await game.ensureAdd(component);
71+
72+
expect(component.children.length, 1);
73+
game.update(1);
74+
expect(effect.controller.completed, true);
75+
game.update(0);
76+
expect(component.children.length, 1);
77+
});
78+
});
79+
}

0 commit comments

Comments
 (0)