diff --git a/doc/flame/effects.md b/doc/flame/effects.md index 452ce8e75..5a57baecb 100644 --- a/doc/flame/effects.md +++ b/doc/flame/effects.md @@ -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>( + (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 diff --git a/examples/lib/stories/effects/effects.dart b/examples/lib/stories/effects/effects.dart index 2eb012f53..62392318c 100644 --- a/examples/lib/stories/effects/effects.dart +++ b/examples/lib/stories/effects/effects.dart @@ -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()), diff --git a/examples/lib/stories/effects/function_effect_example.dart b/examples/lib/stories/effects/function_effect_example.dart new file mode 100644 index 000000000..9820f01f2 --- /dev/null +++ b/examples/lib/stories/effects/function_effect_example.dart @@ -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 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>( + (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( + animations: { + RobotState.running: running, + RobotState.idle: idle, + }, + current: RobotState.idle, + position: size / 2, + anchor: Anchor.center, + size: robotSize, + children: [functionEffect], + ); + + add(component); + } +} diff --git a/packages/flame/lib/effects.dart b/packages/flame/lib/effects.dart index 755850c70..76d88d065 100644 --- a/packages/flame/lib/effects.dart +++ b/packages/flame/lib/effects.dart @@ -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'; diff --git a/packages/flame/lib/src/effects/function_effect.dart b/packages/flame/lib/src/effects/function_effect.dart new file mode 100644 index 000000000..abad4c0a5 --- /dev/null +++ b/packages/flame/lib/src/effects/function_effect.dart @@ -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 extends Effect with EffectTarget { + FunctionEffect( + this.function, + super.controller, { + super.onComplete, + super.key, + }); + + void Function(T target, double progress) function; + + @override + void apply(double progress) { + function(target, progress); + } +} diff --git a/packages/flame/test/effects/function_effect_test.dart b/packages/flame/test/effects/function_effect_test.dart new file mode 100644 index 000000000..09281691d --- /dev/null +++ b/packages/flame/test/effects/function_effect_test.dart @@ -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( + (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( + (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( + (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( + (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); + }); + }); +}