mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-03 04:18:25 +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)
|
- [`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
|
||||||
|
|||||||
@ -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()),
|
||||||
|
|||||||
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/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';
|
||||||
|
|||||||
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