Added the SizeEffect and a general ComponentEffect (#1122)

* Added the `SizeEffect` and a general `ComponentEffect`

* Added changelog entry

* Format throw

* Fix analyze issue

* Fix Transform2DEffect test

* Apply suggestions from code review

Co-authored-by: Luan Nico <luanpotter27@gmail.com>

* Fix missing example parenthesis

* Fix according to comments

Co-authored-by: Luan Nico <luanpotter27@gmail.com>
This commit is contained in:
Lukas Klingsbo
2021-11-22 21:55:14 +01:00
committed by GitHub
parent 37710113bb
commit f249b43e2f
9 changed files with 332 additions and 44 deletions

View File

@ -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()),

View File

@ -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<void> 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,
),
);
}
}

View File

@ -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,
),
),
);
}

View File

@ -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

View File

@ -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<T extends Component> 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;
}
}

View File

@ -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<PositionComponent> {
/// 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;
}
}

View File

@ -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<PositionComponent> {
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;
}
}

View File

@ -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);
});
});
}

View File

@ -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(