From d9984c7bda27eee76fcf228be2dde09f8a23aba1 Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Tue, 2 Nov 2021 01:59:35 -0700 Subject: [PATCH] RemoveEffect (#1063) * Added FlameAnimationController class * Added MainAnimationController class * Update doc comments * formatting * rename MainAnimationController * Added tests for StandardAnimationController * Added more tests * comment * Added changelog note * Export StandardAnimationController * formatting * Use a default for 'curve' parameter * rename onsetDelay -> startDelay * Added Transofm2DEffect * Added EffectComponent * Added .transform getter * formatting * Rename EffectComponent -> Effect * Add documentation for the Effect class * minor * Added a test for Effect class * Adding tests for removeOnFinish * Adding tests for onStart and onFinish * Also check the effect after reset * Fix-up merge * formatting * added doc-comments * changelog note * Added test for transform2DEffect * Adjusted comments * Make PositionComponent._transform public * change changelog * Added SimpleEffectController * Added DestroyEffect * Changelog note * Rename DestroyEffect -> RemoveEffect * Added example for RemoveEffect * flutter format * Move description of the RemoveEffectExample game * move the description again * minor --- examples/lib/commons/circle_component.dart | 7 + examples/lib/stories/effects/effects.dart | 7 + .../effects/remove_effect_example.dart | 41 ++++++ packages/flame/CHANGELOG.md | 1 + .../flame/lib/src/effects2/remove_effect.dart | 16 +++ .../effects2/simple_effect_controller.dart | 54 ++++++++ .../test/effects2/remove_effect_test.dart | 45 ++++++ .../simple_effect_controller_test.dart | 129 ++++++++++++++++++ 8 files changed, 300 insertions(+) create mode 100644 examples/lib/stories/effects/remove_effect_example.dart create mode 100644 packages/flame/lib/src/effects2/remove_effect.dart create mode 100644 packages/flame/lib/src/effects2/simple_effect_controller.dart create mode 100644 packages/flame/test/effects2/remove_effect_test.dart create mode 100644 packages/flame/test/effects2/simple_effect_controller_test.dart diff --git a/examples/lib/commons/circle_component.dart b/examples/lib/commons/circle_component.dart index cea0fad36..cffb3ccb7 100644 --- a/examples/lib/commons/circle_component.dart +++ b/examples/lib/commons/circle_component.dart @@ -18,4 +18,11 @@ class CircleComponent extends PositionComponent { super.render(canvas); canvas.drawCircle(Offset(radius, radius), radius, paint); } + + @override + bool containsPoint(Vector2 point) { + final local = absoluteToLocal(point); + final center = Vector2.all(radius); + return local.distanceToSquared(center) <= radius * radius; + } } diff --git a/examples/lib/stories/effects/effects.dart b/examples/lib/stories/effects/effects.dart index 43f4444dc..05d827c7a 100644 --- a/examples/lib/stories/effects/effects.dart +++ b/examples/lib/stories/effects/effects.dart @@ -7,6 +7,7 @@ import 'combined_effect.dart'; import 'infinite_effect.dart'; import 'move_effect.dart'; import 'opacity_effect.dart'; +import 'remove_effect_example.dart'; import 'rotate_effect.dart'; import 'scale_effect.dart'; import 'sequence_effect.dart'; @@ -67,5 +68,11 @@ void addEffectsStories(Dashbook dashbook) { 'Color Effect', (_) => GameWidget(game: ColorEffectGame()), codeLink: baseLink('effects/color_effect.dart'), + ) + ..add( + 'Remove Effect', + (_) => GameWidget(game: RemoveEffectExample()), + codeLink: baseLink('effects/remove_effect_example.dart'), + info: RemoveEffectExample.description, ); } diff --git a/examples/lib/stories/effects/remove_effect_example.dart b/examples/lib/stories/effects/remove_effect_example.dart new file mode 100644 index 000000000..17b69f00d --- /dev/null +++ b/examples/lib/stories/effects/remove_effect_example.dart @@ -0,0 +1,41 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/src/effects2/remove_effect.dart'; // ignore: implementation_imports +import 'package:flutter/material.dart'; +import '../../commons/circle_component.dart'; + +class RemoveEffectExample extends FlameGame with HasTappableComponents { + static const description = ''' + Click on any circle to apply a RemoveEffect, which will make the circle + disappear after a 0.5 second delay. + '''; + + @override + void onMount() { + super.onMount(); + camera.viewport = FixedResolutionViewport(Vector2(400, 600)); + final rng = Random(); + for (var i = 0; i < 20; i++) { + add(_RandomCircle(rng)); + } + } +} + +class _RandomCircle extends CircleComponent with Tappable { + _RandomCircle(Random rng) : super(radius: rng.nextDouble() * 30 + 10) { + position.setValues( + rng.nextDouble() * 320 + 40, + rng.nextDouble() * 520 + 40, + ); + paint.color = Colors.primaries[rng.nextInt(Colors.primaries.length)]; + } + + @override + bool onTapDown(TapDownInfo info) { + add(RemoveEffect(delay: 0.5)); + return false; + } +} diff --git a/packages/flame/CHANGELOG.md b/packages/flame/CHANGELOG.md index 13a22e6b6..388d5e153 100644 --- a/packages/flame/CHANGELOG.md +++ b/packages/flame/CHANGELOG.md @@ -4,6 +4,7 @@ - Added `StandardEffectController` class - Refactored `Effect` class to use `EffectController`, added `Transform2DEffect` class - Clarified `TimerComponent` example + - Added `RemoveEffect` and `SimpleEffectController` ## [1.0.0-releasecandidate.16] - `changePriority` no longer breaks game loop iteration diff --git a/packages/flame/lib/src/effects2/remove_effect.dart b/packages/flame/lib/src/effects2/remove_effect.dart new file mode 100644 index 000000000..b174e0612 --- /dev/null +++ b/packages/flame/lib/src/effects2/remove_effect.dart @@ -0,0 +1,16 @@ +import 'effect.dart'; +import 'simple_effect_controller.dart'; + +/// This simple effect, when attached to a component, will cause that component +/// to be removed from the game tree after `delay` seconds. +class RemoveEffect extends Effect { + RemoveEffect({double delay = 0.0}) + : super(SimpleEffectController(delay: delay)); + + @override + void apply(double progress) { + if (progress == 1) { + parent?.removeFromParent(); + } + } +} diff --git a/packages/flame/lib/src/effects2/simple_effect_controller.dart b/packages/flame/lib/src/effects2/simple_effect_controller.dart new file mode 100644 index 000000000..9360a8d95 --- /dev/null +++ b/packages/flame/lib/src/effects2/simple_effect_controller.dart @@ -0,0 +1,54 @@ +import 'effect_controller.dart'; +import 'standard_effect_controller.dart'; + +/// Simplest possible [EffectController], which supports an effect progressing +/// linearly over [duration] seconds. +/// +/// The [duration] can be 0, in which case the effect will jump from 0 to 1 +/// instantaneously. +/// +/// The [delay] parameter allows to delay the start of the effect by the +/// specified number of seconds. +/// +/// See also: [StandardEffectController] +class SimpleEffectController extends EffectController { + SimpleEffectController({ + this.duration = 0.0, + this.delay = 0.0, + }) : assert(duration >= 0, 'duration cannot be negative: $duration'), + assert(delay >= 0, 'delay cannot be negative: $delay'); + + final double duration; + final double delay; + double _timer = 0.0; + + @override + bool get started => _timer >= delay; + + @override + bool get completed => _timer >= delay + duration; + + @override + bool get isInfinite => false; + + @override + double get progress { + // If duration == 0, then `completed == started`, and the middle case + // (which divides by duration) cannot occur. + return completed + ? 1 + : started + ? (_timer - delay) / duration + : 0; + } + + @override + void update(double dt) { + _timer += dt; + } + + @override + void reset() { + _timer = 0; + } +} diff --git a/packages/flame/test/effects2/remove_effect_test.dart b/packages/flame/test/effects2/remove_effect_test.dart new file mode 100644 index 000000000..09e2ce4c8 --- /dev/null +++ b/packages/flame/test/effects2/remove_effect_test.dart @@ -0,0 +1,45 @@ +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/src/effects2/remove_effect.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('RemoveEffect', () { + test('no delay', () { + final game = FlameGame(); + game.onGameResize(Vector2.all(1)); + expect(game.children.length, 0); + final obj = Component(); + game.add(obj); + game.update(0); + expect(game.children.length, 1); + + // First `game.update()` invokes the destroy effect and schedules `obj` + // for deletion; second `game.update()` processes the deletion queue and + // actually removes the component + obj.add(RemoveEffect()); + game.update(0); + game.update(0); + expect(game.children.length, 0); + }); + + test('delayed', () { + final game = FlameGame(); + game.onGameResize(Vector2.all(1)); + expect(game.children.length, 0); + final obj = Component(); + game.add(obj); + game.update(0); + expect(game.children.length, 1); + + obj.add(RemoveEffect(delay: 1)); + game.update(0.5); + game.update(0); + expect(game.children.length, 1); + + game.update(0.5); + game.update(0); + expect(game.children.length, 0); + }); + }); +} diff --git a/packages/flame/test/effects2/simple_effect_controller_test.dart b/packages/flame/test/effects2/simple_effect_controller_test.dart new file mode 100644 index 000000000..ea5f5e7b6 --- /dev/null +++ b/packages/flame/test/effects2/simple_effect_controller_test.dart @@ -0,0 +1,129 @@ +import 'package:flame/src/effects2/simple_effect_controller.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SimpleEffectController', () { + test('default', () { + final ec = SimpleEffectController(); + expect(ec.duration, 0); + expect(ec.delay, 0); + expect(ec.isInfinite, false); + expect(ec.started, true); + expect(ec.completed, true); + expect(ec.progress, 1); + }); + + test('simple with duration', () { + final ec = SimpleEffectController(duration: 1); + expect(ec.delay, 0); + expect(ec.duration, 1); + expect(ec.progress, 0); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.isInfinite, false); + + ec.update(0.5); + expect(ec.progress, 0.5); + expect(ec.started, true); + expect(ec.completed, false); + + ec.update(0.5); + expect(ec.progress, 1); + expect(ec.started, true); + expect(ec.completed, true); + + ec.update(0.00001); + expect(ec.progress, 1); + expect(ec.started, true); + expect(ec.completed, true); + }); + + test('simple with delay', () { + final ec = SimpleEffectController(delay: 1); + expect(ec.isInfinite, false); + expect(ec.started, false); + expect(ec.completed, false); + expect(ec.progress, 0); + expect(ec.delay, 1); + expect(ec.duration, 0); + + ec.update(0.5); + expect(ec.started, false); + expect(ec.completed, false); + expect(ec.progress, 0); + + ec.update(0.5); + expect(ec.started, true); + expect(ec.completed, true); + expect(ec.progress, 1); + }); + + test('duration + delay', () { + final ec = SimpleEffectController(duration: 1, delay: 2); + expect(ec.isInfinite, false); + expect(ec.started, false); + expect(ec.completed, false); + expect(ec.duration, 1); + expect(ec.delay, 2); + expect(ec.progress, 0); + + ec.update(0.5); + expect(ec.started, false); + expect(ec.progress, 0); + + ec.update(0.5); + expect(ec.started, false); + expect(ec.progress, 0); + + ec.update(1); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, 0); + + ec.update(0.5); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, 0.5); + + ec.update(0.5); + expect(ec.started, true); + expect(ec.completed, true); + expect(ec.progress, 1); + }); + + test('reset', () { + final ec = SimpleEffectController(); + ec.reset(); + expect(ec.started, true); + expect(ec.completed, true); + expect(ec.progress, 1); + }); + + test('reset 2', () { + final ec = SimpleEffectController(duration: 2, delay: 1); + ec.update(3); + expect(ec.completed, true); + expect(ec.progress, 1); + + ec.reset(); + expect(ec.started, false); + expect(ec.completed, false); + expect(ec.progress, 0); + + ec.update(1); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, 0); + + ec.update(1); + expect(ec.started, true); + expect(ec.completed, false); + expect(ec.progress, closeTo(0.5, 1e-15)); + + ec.update(1); + expect(ec.started, true); + expect(ec.completed, true); + expect(ec.progress, 1); + }); + }); +}