mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 11:43:19 +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_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()),
|
||||
|
||||
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/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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
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 '../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;
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
'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(
|
||||
|
||||
Reference in New Issue
Block a user