diff --git a/examples/lib/stories/effects/effects.dart b/examples/lib/stories/effects/effects.dart index 0d69b6b98..83cb23340 100644 --- a/examples/lib/stories/effects/effects.dart +++ b/examples/lib/stories/effects/effects.dart @@ -9,6 +9,7 @@ import 'move_effect_example.dart'; import 'old_move_effect_example.dart'; import 'old_rotate_effect_example.dart'; import 'old_scale_effect_example.dart'; +import 'old_size_effect_example.dart'; import 'opacity_effect_example.dart'; import 'remove_effect_example.dart'; import 'rotate_effect_example.dart'; @@ -20,9 +21,9 @@ void addEffectsStories(Dashbook dashbook) { dashbook.storiesOf('Effects') ..add( 'Size Effect', - (_) => GameWidget(game: SizeEffectExample()), + (_) => GameWidget(game: OldSizeEffectExample()), codeLink: baseLink('effects/size_effect_example.dart'), - info: SizeEffectExample.description, + info: OldSizeEffectExample.description, ) ..add( 'Scale Effect', @@ -83,6 +84,12 @@ void addEffectsStories(Dashbook dashbook) { codeLink: baseLink('effects/rotate_effect_example.dart'), info: RotateEffectExample.description, ) + ..add( + 'Size Effect (v2)', + (_) => GameWidget(game: SizeEffectExample()), + codeLink: baseLink('effects/size_effect_example.dart'), + info: SizeEffectExample.description, + ) ..add( 'Scale Effect (v2)', (_) => GameWidget(game: ScaleEffectExample()), diff --git a/examples/lib/stories/effects/old_size_effect_example.dart b/examples/lib/stories/effects/old_size_effect_example.dart new file mode 100644 index 000000000..289710c4b --- /dev/null +++ b/examples/lib/stories/effects/old_size_effect_example.dart @@ -0,0 +1,47 @@ +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flame/extensions.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; +import 'package:flame/palette.dart'; +import 'package:flutter/material.dart'; + +import '../../commons/square_component.dart'; + +class OldSizeEffectExample extends FlameGame with TapDetector { + static const String description = ''' + The `SizeEffect` changes the size of the component, but the sizes of its + children will stay the same. + In this example, you can tap the screen and the component will size up or + down, depending on its current state. + '''; + + late SquareComponent square; + bool grow = true; + + @override + Future onLoad() async { + await super.onLoad(); + square = SquareComponent( + position: Vector2.all(200), + paint: BasicPalette.white.paint()..style = PaintingStyle.stroke, + ); + final childSquare = SquareComponent(position: Vector2.all(70), size: 20); + square.add(childSquare); + add(square); + } + + @override + void onTap() { + final s = grow ? 300.0 : 100.0; + + grow = !grow; + square.add( + SizeEffect( + size: Vector2.all(s), + speed: 250.0, + curve: Curves.bounceInOut, + ), + ); + } +} diff --git a/examples/lib/stories/effects/size_effect_example.dart b/examples/lib/stories/effects/size_effect_example.dart index 46f499abd..e1e2f6d16 100644 --- a/examples/lib/stories/effects/size_effect_example.dart +++ b/examples/lib/stories/effects/size_effect_example.dart @@ -1,9 +1,10 @@ import 'package:flame/components.dart'; -import 'package:flame/effects.dart'; import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame/input.dart'; import 'package:flame/palette.dart'; +import 'package:flame/src/effects2/size_effect.dart'; // ignore: implementation_imports +import 'package:flame/src/effects2/standard_effect_controller.dart'; // ignore: implementation_imports import 'package:flutter/material.dart'; import '../../commons/square_component.dart'; @@ -36,11 +37,14 @@ class SizeEffectExample extends FlameGame with TapDetector { final s = grow ? 300.0 : 100.0; grow = !grow; + square.add( - SizeEffect( - size: Vector2.all(s), - speed: 250.0, - curve: Curves.bounceInOut, + SizeEffect.to( + Vector2.all(s), + StandardEffectController( + duration: 1.5, + curve: Curves.bounceInOut, + ), ), ); } diff --git a/packages/flame/CHANGELOG.md b/packages/flame/CHANGELOG.md index 483a3efd9..323c806d8 100644 --- a/packages/flame/CHANGELOG.md +++ b/packages/flame/CHANGELOG.md @@ -38,6 +38,7 @@ - Rename `HasTappableComponents` to `HasTappables` - Rename `HasDraggableComponents` to `HasDraggables` - Rename `HasHoverableComponents` to `HasHoverableis` + - Added `SizeEffect` backed by the new effects engine - Added `ScaleEffect` backed by the new effects engine - Update `OrderedSet` to 4.1.0 - Update `OrderedSet` to 5.0.0 diff --git a/packages/flame/lib/src/effects2/component_effect.dart b/packages/flame/lib/src/effects2/component_effect.dart new file mode 100644 index 000000000..66c8e2fdf --- /dev/null +++ b/packages/flame/lib/src/effects2/component_effect.dart @@ -0,0 +1,44 @@ +import 'package:flutter/cupertino.dart'; + +import '../../components.dart'; +import 'effect.dart'; +import 'effect_controller.dart'; + +/// Base class for effects that target a [Component] of type [T]. +/// +/// A general abstraction for creating effects targeting [Component]s, currently +/// only used by `SizeEffect` and `Transform2DEffect`. +abstract class ComponentEffect extends Effect { + ComponentEffect(EffectController controller) : super(controller); + + late T target; + + /// The effect's `progress` variable as it was the last time that the + /// `apply()` method was called. Mostly used by the derived classes. + double get previousProgress => _lastProgress; + double _lastProgress = 0; + + @override + void onMount() { + super.onMount(); + assert(parent != null); + if (parent is T) { + target = parent! as T; + } else { + throw UnsupportedError('Can only apply this effect to $T'); + } + } + + // In the derived class, call `super.apply()` last. + @mustCallSuper + @override + void apply(double progress) { + _lastProgress = progress; + } + + @override + void reset() { + super.reset(); + _lastProgress = 0; + } +} diff --git a/packages/flame/lib/src/effects2/size_effect.dart b/packages/flame/lib/src/effects2/size_effect.dart new file mode 100644 index 000000000..373d75b4f --- /dev/null +++ b/packages/flame/lib/src/effects2/size_effect.dart @@ -0,0 +1,54 @@ +import '../../components.dart'; +import '../../extensions.dart'; +import 'component_effect.dart'; +import 'effect_controller.dart'; + +/// Change the size of a component over time. +/// +/// This effect applies incremental changes to the component's size, and +/// requires that any other effect or update logic applied to the same component +/// also used incremental updates. +class SizeEffect extends ComponentEffect { + /// This constructor will create an effect that sets the size in relation to + /// the [PositionComponent]'s current size, for example if the [offset] is + /// set to `Vector2(10, -10)` and the size of the affected component is + /// `Vector2(100, 100)` at the start of the affected the effect will peak when + /// the size is `Vector2(110, 90)`, if there is nothing else affecting the + /// size at the same time. + SizeEffect.by(Vector2 offset, EffectController controller) + : _offset = offset.clone(), + super(controller); + + /// This constructor will create an effect that sets the size to the absolute + /// size that is defined by [targetSize]. + /// For example if the [targetSize] is set to `Vector2(200, 200)` and the size + /// of the affected component is `Vector2(100, 100)` at the start of the + /// affected the effect will peak when the size is `Vector2(200, 100)`, if + /// there is nothing else affecting the size at the same time. + factory SizeEffect.to(Vector2 targetSize, EffectController controller) => + _SizeToEffect(targetSize, controller); + + Vector2 _offset; + + @override + void apply(double progress) { + final dProgress = progress - previousProgress; + target.size += _offset * dProgress; + target.size.clampScalar(0, double.infinity); + super.apply(progress); + } +} + +/// Implementation class for [SizeEffect.to] +class _SizeToEffect extends SizeEffect { + final Vector2 _targetSize; + + _SizeToEffect(Vector2 targetSize, EffectController controller) + : _targetSize = targetSize.clone(), + super.by(Vector2.zero(), controller); + + @override + void onStart() { + _offset = _targetSize - target.size; + } +} diff --git a/packages/flame/lib/src/effects2/transform2d_effect.dart b/packages/flame/lib/src/effects2/transform2d_effect.dart index 308d7c7c7..b18f182d9 100644 --- a/packages/flame/lib/src/effects2/transform2d_effect.dart +++ b/packages/flame/lib/src/effects2/transform2d_effect.dart @@ -1,8 +1,6 @@ -import 'package:flutter/cupertino.dart'; - import '../components/position_component.dart'; import '../game/transform2d.dart'; -import 'effect.dart'; +import 'component_effect.dart'; import 'effect_controller.dart'; /// Base class for effects that target a [Transform2D] property. @@ -14,41 +12,14 @@ import 'effect_controller.dart'; /// Currently this class only supports being attached to [PositionComponent]s, /// but in the future it will be extended to work with any [Transform2D]-based /// classes. -abstract class Transform2DEffect extends Effect { +abstract class Transform2DEffect extends ComponentEffect { Transform2DEffect(EffectController controller) : super(controller); - late Transform2D target; - - /// The effect's `progress` variable as it was the last time that the - /// `apply()` method was called. Mostly used by the derived classes. - double get previousProgress => _lastProgress; - double _lastProgress = 0; + late Transform2D transform; @override void onMount() { super.onMount(); - assert(parent != null); - if (parent is PositionComponent) { - target = (parent! as PositionComponent).transform; - } - // TODO(st-pasha): add Camera support once it uses Transform2D - else { - throw UnsupportedError( - 'Can only apply a Transform2DEffect to a PositionComponent class', - ); - } - } - - // In the derived class, call `super.apply()` last. - @mustCallSuper - @override - void apply(double progress) { - _lastProgress = progress; - } - - @override - void reset() { - super.reset(); - _lastProgress = 0; + transform = target.transform; } } diff --git a/packages/flame/test/effects2/size_effect_test.dart b/packages/flame/test/effects2/size_effect_test.dart new file mode 100644 index 000000000..8aff30f13 --- /dev/null +++ b/packages/flame/test/effects2/size_effect_test.dart @@ -0,0 +1,160 @@ +import 'dart:math'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/src/effects2/size_effect.dart'; +import 'package:flame/src/effects2/standard_effect_controller.dart'; +import 'package:flame_test/flame_test.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('SizeEffect', () { + flameGame.test('relative', (game) { + final component = PositionComponent(); + game.ensureAdd(component); + + component.size = Vector2.all(1.0); + component.add( + SizeEffect.by(Vector2.all(1.0), StandardEffectController(duration: 1)), + ); + game.update(0); + expectVector2(component.size, Vector2.all(1.0)); + expect(component.children.length, 1); + + game.update(0.5); + expectVector2(component.size, Vector2.all(1.5)); + + game.update(0.5); + expectVector2(component.size, Vector2.all(2.0)); + game.update(0); + expect(component.children.length, 0); + expectVector2(component.size, Vector2.all(2.0)); + }); + + flameGame.test('absolute', (game) { + final component = PositionComponent(); + game.ensureAdd(component); + + component.size = Vector2.all(1.0); + component.add( + SizeEffect.to(Vector2.all(3.0), StandardEffectController(duration: 1)), + ); + game.update(0); + expectVector2(component.size, Vector2.all(1.0)); + expect(component.children.length, 1); + + game.update(0.5); + expectVector2(component.size, Vector2.all(2.0)); + + game.update(0.5); + expectVector2(component.size, Vector2.all(3.0)); + game.update(0); + expect(component.children.length, 0); + expectVector2(component.size, Vector2.all(3.0)); + }); + + flameGame.test('reset relative', (game) { + final component = PositionComponent(); + game.ensureAdd(component); + + final effect = SizeEffect.by( + Vector2.all(1.0), + StandardEffectController(duration: 1), + ); + component.add(effect..removeOnFinish = false); + final expectedSize = Vector2.zero(); + for (var i = 0; i < 5; i++) { + expectVector2(component.size, expectedSize); + // After each reset the object will be sized up by Vector2(1, 1) + // relative to its size at the start of the effect. + effect.reset(); + game.update(1); + expectedSize.add(Vector2.all(1.0)); + expectVector2(component.size, expectedSize); + } + }); + + flameGame.test('reset absolute', (game) { + final component = PositionComponent(); + game.ensureAdd(component); + + final effect = SizeEffect.to( + Vector2.all(1.0), + StandardEffectController(duration: 1), + ); + component.add(effect..removeOnFinish = false); + for (var i = 0; i < 5; i++) { + component.size = Vector2.all(1 + 4.0 * i); + // After each reset the object will be sized to the value of + // `Vector2(1, 1)`, regardless of its initial orientation. + effect.reset(); + game.update(1); + expectVector2(component.size, Vector2.all(1.0)); + } + }); + + flameGame.test('size composition', (game) { + final component = PositionComponent(); + game.ensureAdd(component); + + component.add( + SizeEffect.by(Vector2.all(5), StandardEffectController(duration: 10)), + ); + component.add( + SizeEffect.by( + Vector2.all(0.5), + StandardEffectController( + duration: 1, + reverseDuration: 1, + repeatCount: 5, + ), + ), + ); + + game.update(1); + expectVector2( + component.size, + Vector2.all(1), + epsilon: 1e-15, + ); // 5*1/10 + 0.5*1 + game.update(1); + expectVector2( + component.size, + Vector2.all(1), + epsilon: 1e-15, + ); // 5*2/10 + 0.5*1 - 0.5*1 + for (var i = 0; i < 10; i++) { + game.update(1); + } + expectVector2(component.size, Vector2.all(5), epsilon: 1e-15); + expect(component.children.length, 0); + }); + + testRandom('a very long size change', (Random rng) { + final game = FlameGame()..onGameResize(Vector2(1, 1)); + final component = PositionComponent(); + game.ensureAdd(component); + + final effect = SizeEffect.by( + Vector2.all(1.0), + StandardEffectController( + duration: 1, + reverseDuration: 1, + infinite: true, + ), + ); + component.add(effect); + + var totalTime = 0.0; + while (totalTime < 999.9) { + final dt = rng.nextDouble() * 0.02; + totalTime += dt; + game.update(dt); + } + game.update(1000 - totalTime); + // Typically, `component.size` could accumulate numeric discrepancy on the + // order of 1e-11 .. 1e-12 by now. + expectVector2(component.size, Vector2.zero(), epsilon: 1e-10); + }); + }); +} diff --git a/packages/flame/test/effects2/transform2d_effect_test.dart b/packages/flame/test/effects2/transform2d_effect_test.dart index ba6ed4d0c..7fc6665ee 100644 --- a/packages/flame/test/effects2/transform2d_effect_test.dart +++ b/packages/flame/test/effects2/transform2d_effect_test.dart @@ -14,14 +14,14 @@ void main() { flameGame.test( 'onMount', (game) { - final obj = PositionComponent(); - game.add(obj); + final component = PositionComponent(); + game.add(component); game.update(0); final effect = _MyEffect(StandardEffectController(duration: 1)); - obj.add(effect); + component.add(effect); game.update(0); - expect(effect.target, obj.transform); + expect(effect.transform, component.transform); final effect2 = _MyEffect(StandardEffectController(duration: 1)); expect(