mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 20:13:50 +08:00
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:
@ -49,6 +49,7 @@ There are multiple effects provided by Flame, and you can also
|
||||
- [`ColorEffect`](#coloreffect)
|
||||
- [`SequenceEffect`](#sequenceeffect)
|
||||
- [`RemoveEffect`](#removeeffect)
|
||||
- [`FunctionEffect`](#functioneffect)
|
||||
|
||||
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
|
||||
@ -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.
|
||||
|
||||
|
||||
### `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
|
||||
|
||||
Although Flame provides a wide array of built-in effects, eventually you may find them to be
|
||||
|
||||
@ -3,6 +3,7 @@ import 'package:examples/commons/commons.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/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/opacity_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'),
|
||||
info: RemoveEffectExample.description,
|
||||
)
|
||||
..add(
|
||||
'Function Effect',
|
||||
(_) => GameWidget(game: FunctionEffectExample()),
|
||||
codeLink: baseLink('effects/function_effect_example.dart'),
|
||||
info: FunctionEffectExample.description,
|
||||
)
|
||||
..add(
|
||||
'EffectControllers',
|
||||
(_) => GameWidget(game: EffectControllersExample()),
|
||||
|
||||
64
examples/lib/stories/effects/function_effect_example.dart
Normal file
64
examples/lib/stories/effects/function_effect_example.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -21,6 +21,7 @@ export 'src/effects/controllers/speed_effect_controller.dart';
|
||||
export 'src/effects/controllers/zigzag_effect_controller.dart';
|
||||
export 'src/effects/effect.dart';
|
||||
export 'src/effects/effect_target.dart';
|
||||
export 'src/effects/function_effect.dart';
|
||||
export 'src/effects/glow_effect.dart';
|
||||
export 'src/effects/move_along_path_effect.dart';
|
||||
export 'src/effects/move_by_effect.dart';
|
||||
|
||||
26
packages/flame/lib/src/effects/function_effect.dart
Normal file
26
packages/flame/lib/src/effects/function_effect.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
79
packages/flame/test/effects/function_effect_test.dart
Normal file
79
packages/flame/test/effects/function_effect_test.dart
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user