mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-03 12:28:03 +08:00
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:
@ -9,6 +9,7 @@ import 'move_effect_example.dart';
|
|||||||
import 'old_move_effect_example.dart';
|
import 'old_move_effect_example.dart';
|
||||||
import 'old_rotate_effect_example.dart';
|
import 'old_rotate_effect_example.dart';
|
||||||
import 'old_scale_effect_example.dart';
|
import 'old_scale_effect_example.dart';
|
||||||
|
import 'old_size_effect_example.dart';
|
||||||
import 'opacity_effect_example.dart';
|
import 'opacity_effect_example.dart';
|
||||||
import 'remove_effect_example.dart';
|
import 'remove_effect_example.dart';
|
||||||
import 'rotate_effect_example.dart';
|
import 'rotate_effect_example.dart';
|
||||||
@ -20,9 +21,9 @@ void addEffectsStories(Dashbook dashbook) {
|
|||||||
dashbook.storiesOf('Effects')
|
dashbook.storiesOf('Effects')
|
||||||
..add(
|
..add(
|
||||||
'Size Effect',
|
'Size Effect',
|
||||||
(_) => GameWidget(game: SizeEffectExample()),
|
(_) => GameWidget(game: OldSizeEffectExample()),
|
||||||
codeLink: baseLink('effects/size_effect_example.dart'),
|
codeLink: baseLink('effects/size_effect_example.dart'),
|
||||||
info: SizeEffectExample.description,
|
info: OldSizeEffectExample.description,
|
||||||
)
|
)
|
||||||
..add(
|
..add(
|
||||||
'Scale Effect',
|
'Scale Effect',
|
||||||
@ -83,6 +84,12 @@ void addEffectsStories(Dashbook dashbook) {
|
|||||||
codeLink: baseLink('effects/rotate_effect_example.dart'),
|
codeLink: baseLink('effects/rotate_effect_example.dart'),
|
||||||
info: RotateEffectExample.description,
|
info: RotateEffectExample.description,
|
||||||
)
|
)
|
||||||
|
..add(
|
||||||
|
'Size Effect (v2)',
|
||||||
|
(_) => GameWidget(game: SizeEffectExample()),
|
||||||
|
codeLink: baseLink('effects/size_effect_example.dart'),
|
||||||
|
info: SizeEffectExample.description,
|
||||||
|
)
|
||||||
..add(
|
..add(
|
||||||
'Scale Effect (v2)',
|
'Scale Effect (v2)',
|
||||||
(_) => GameWidget(game: ScaleEffectExample()),
|
(_) => GameWidget(game: ScaleEffectExample()),
|
||||||
|
|||||||
47
examples/lib/stories/effects/old_size_effect_example.dart
Normal file
47
examples/lib/stories/effects/old_size_effect_example.dart
Normal 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,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,9 +1,10 @@
|
|||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/effects.dart';
|
|
||||||
import 'package:flame/extensions.dart';
|
import 'package:flame/extensions.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flame/input.dart';
|
import 'package:flame/input.dart';
|
||||||
import 'package:flame/palette.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 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../../commons/square_component.dart';
|
import '../../commons/square_component.dart';
|
||||||
@ -36,12 +37,15 @@ class SizeEffectExample extends FlameGame with TapDetector {
|
|||||||
final s = grow ? 300.0 : 100.0;
|
final s = grow ? 300.0 : 100.0;
|
||||||
|
|
||||||
grow = !grow;
|
grow = !grow;
|
||||||
|
|
||||||
square.add(
|
square.add(
|
||||||
SizeEffect(
|
SizeEffect.to(
|
||||||
size: Vector2.all(s),
|
Vector2.all(s),
|
||||||
speed: 250.0,
|
StandardEffectController(
|
||||||
|
duration: 1.5,
|
||||||
curve: Curves.bounceInOut,
|
curve: Curves.bounceInOut,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,6 +38,7 @@
|
|||||||
- Rename `HasTappableComponents` to `HasTappables`
|
- Rename `HasTappableComponents` to `HasTappables`
|
||||||
- Rename `HasDraggableComponents` to `HasDraggables`
|
- Rename `HasDraggableComponents` to `HasDraggables`
|
||||||
- Rename `HasHoverableComponents` to `HasHoverableis`
|
- Rename `HasHoverableComponents` to `HasHoverableis`
|
||||||
|
- Added `SizeEffect` backed by the new effects engine
|
||||||
- Added `ScaleEffect` backed by the new effects engine
|
- Added `ScaleEffect` backed by the new effects engine
|
||||||
- Update `OrderedSet` to 4.1.0
|
- Update `OrderedSet` to 4.1.0
|
||||||
- Update `OrderedSet` to 5.0.0
|
- Update `OrderedSet` to 5.0.0
|
||||||
|
|||||||
44
packages/flame/lib/src/effects2/component_effect.dart
Normal file
44
packages/flame/lib/src/effects2/component_effect.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
54
packages/flame/lib/src/effects2/size_effect.dart
Normal file
54
packages/flame/lib/src/effects2/size_effect.dart
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import 'package:flutter/cupertino.dart';
|
|
||||||
|
|
||||||
import '../components/position_component.dart';
|
import '../components/position_component.dart';
|
||||||
import '../game/transform2d.dart';
|
import '../game/transform2d.dart';
|
||||||
import 'effect.dart';
|
import 'component_effect.dart';
|
||||||
import 'effect_controller.dart';
|
import 'effect_controller.dart';
|
||||||
|
|
||||||
/// Base class for effects that target a [Transform2D] property.
|
/// 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,
|
/// Currently this class only supports being attached to [PositionComponent]s,
|
||||||
/// but in the future it will be extended to work with any [Transform2D]-based
|
/// but in the future it will be extended to work with any [Transform2D]-based
|
||||||
/// classes.
|
/// classes.
|
||||||
abstract class Transform2DEffect extends Effect {
|
abstract class Transform2DEffect extends ComponentEffect<PositionComponent> {
|
||||||
Transform2DEffect(EffectController controller) : super(controller);
|
Transform2DEffect(EffectController controller) : super(controller);
|
||||||
|
|
||||||
late Transform2D target;
|
late Transform2D transform;
|
||||||
|
|
||||||
/// 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
|
@override
|
||||||
void onMount() {
|
void onMount() {
|
||||||
super.onMount();
|
super.onMount();
|
||||||
assert(parent != null);
|
transform = target.transform;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
160
packages/flame/test/effects2/size_effect_test.dart
Normal file
160
packages/flame/test/effects2/size_effect_test.dart
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -14,14 +14,14 @@ void main() {
|
|||||||
flameGame.test(
|
flameGame.test(
|
||||||
'onMount',
|
'onMount',
|
||||||
(game) {
|
(game) {
|
||||||
final obj = PositionComponent();
|
final component = PositionComponent();
|
||||||
game.add(obj);
|
game.add(component);
|
||||||
game.update(0);
|
game.update(0);
|
||||||
|
|
||||||
final effect = _MyEffect(StandardEffectController(duration: 1));
|
final effect = _MyEffect(StandardEffectController(duration: 1));
|
||||||
obj.add(effect);
|
component.add(effect);
|
||||||
game.update(0);
|
game.update(0);
|
||||||
expect(effect.target, obj.transform);
|
expect(effect.transform, component.transform);
|
||||||
|
|
||||||
final effect2 = _MyEffect(StandardEffectController(duration: 1));
|
final effect2 = _MyEffect(StandardEffectController(duration: 1));
|
||||||
expect(
|
expect(
|
||||||
|
|||||||
Reference in New Issue
Block a user