mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 03:15:43 +08:00
MoveAlongPathEffect (#1172)
* MoveAlongPathEffect separated from MoveEffect * rename _lastPosition -> _lastOffset * Added absolute= flag in MoveAlongPathEffect * added test for absolute path * add oriented= flag * added test * changelog Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
This commit is contained in:
@ -89,14 +89,16 @@ class MoveEffectExample extends FlameGame {
|
|||||||
for (var i = 0; i < 40; i++) {
|
for (var i = 0; i < 40; i++) {
|
||||||
add(
|
add(
|
||||||
CircleComponent(radius: 5)
|
CircleComponent(radius: 5)
|
||||||
|
..position = Vector2(0, -1000)
|
||||||
..add(
|
..add(
|
||||||
MoveEffect.along(
|
MoveAlongPathEffect(
|
||||||
path1,
|
path1,
|
||||||
EffectController(
|
EffectController(
|
||||||
duration: 10,
|
duration: 10,
|
||||||
startDelay: i * 0.2,
|
startDelay: i * 0.2,
|
||||||
infinite: true,
|
infinite: true,
|
||||||
),
|
),
|
||||||
|
absolute: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -108,13 +110,14 @@ class MoveEffectExample extends FlameGame {
|
|||||||
RectangleComponent.square(size: 10)
|
RectangleComponent.square(size: 10)
|
||||||
..paint = (Paint()..color = Colors.tealAccent)
|
..paint = (Paint()..color = Colors.tealAccent)
|
||||||
..add(
|
..add(
|
||||||
MoveEffect.along(
|
MoveAlongPathEffect(
|
||||||
path2,
|
path2,
|
||||||
EffectController(
|
EffectController(
|
||||||
duration: 6,
|
duration: 6,
|
||||||
startDelay: i * 0.3,
|
startDelay: i * 0.3,
|
||||||
infinite: true,
|
infinite: true,
|
||||||
),
|
),
|
||||||
|
oriented: true,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@ -11,6 +11,7 @@
|
|||||||
- Fix render order of components and add tests
|
- Fix render order of components and add tests
|
||||||
- `isHud` renamed to `respectCamera`
|
- `isHud` renamed to `respectCamera`
|
||||||
- Fix `HitboxCircle` when component is flipped
|
- Fix `HitboxCircle` when component is flipped
|
||||||
|
- `MoveAlongPathEffect` can now be absolute, and can auto-orient the object along the path
|
||||||
- `ScaleEffect.by` now applies multiplicatively instead of additively
|
- `ScaleEffect.by` now applies multiplicatively instead of additively
|
||||||
|
|
||||||
## [1.0.0-releasecandidate.18]
|
## [1.0.0-releasecandidate.18]
|
||||||
|
|||||||
@ -11,6 +11,7 @@ export 'src/effects/controllers/reverse_curved_effect_controller.dart';
|
|||||||
export 'src/effects/controllers/reverse_linear_effect_controller.dart';
|
export 'src/effects/controllers/reverse_linear_effect_controller.dart';
|
||||||
export 'src/effects/controllers/sequence_effect_controller.dart';
|
export 'src/effects/controllers/sequence_effect_controller.dart';
|
||||||
export 'src/effects/effect.dart';
|
export 'src/effects/effect.dart';
|
||||||
|
export 'src/effects/move_along_path_effect.dart';
|
||||||
export 'src/effects/move_effect.dart';
|
export 'src/effects/move_effect.dart';
|
||||||
export 'src/effects/opacity_effect.dart';
|
export 'src/effects/opacity_effect.dart';
|
||||||
export 'src/effects/remove_effect.dart';
|
export 'src/effects/remove_effect.dart';
|
||||||
|
|||||||
98
packages/flame/lib/src/effects/move_along_path_effect.dart
Normal file
98
packages/flame/lib/src/effects/move_along_path_effect.dart
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:vector_math/vector_math_64.dart';
|
||||||
|
|
||||||
|
import 'controllers/effect_controller.dart';
|
||||||
|
import 'transform2d_effect.dart';
|
||||||
|
|
||||||
|
/// This effect will move the target along the specified path, which may
|
||||||
|
/// contain curved segments, but must be simply-connected.
|
||||||
|
///
|
||||||
|
/// If `absolute` is false (default), the `path` argument will be taken as
|
||||||
|
/// relative to the target's position at the start of the effect. It is
|
||||||
|
/// recommended in this case to have a path that starts at the origin in order
|
||||||
|
/// to avoid sudden jumps in the target's position.
|
||||||
|
///
|
||||||
|
/// If `absolute` flag is true, then the `path` will be assumed to be given in
|
||||||
|
/// absolute coordinate space and the target will be placed at the beginning of
|
||||||
|
/// the path when the effect starts.
|
||||||
|
///
|
||||||
|
/// The `oriented` flag controls the direction of the target as it follows the
|
||||||
|
/// path. If this flag is false (default), the target keeps its original
|
||||||
|
/// orientation. If the flag is true, the target is automatically rotated as it
|
||||||
|
/// follows the path so that it is always oriented tangent to the path.
|
||||||
|
class MoveAlongPathEffect extends Transform2DEffect {
|
||||||
|
MoveAlongPathEffect(
|
||||||
|
Path path,
|
||||||
|
EffectController controller, {
|
||||||
|
bool absolute = false,
|
||||||
|
bool oriented = false,
|
||||||
|
}) : _isAbsolute = absolute,
|
||||||
|
_followDirection = oriented,
|
||||||
|
super(controller) {
|
||||||
|
final metrics = path.computeMetrics().toList();
|
||||||
|
if (metrics.length != 1) {
|
||||||
|
throw ArgumentError(
|
||||||
|
'Only single-contour paths are allowed in MoveAlongPathEffect',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_pathMetric = metrics[0];
|
||||||
|
_pathLength = _pathMetric.length;
|
||||||
|
assert(_pathLength > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// If true, the path is considered _absolute_, i.e. the component will be
|
||||||
|
/// put onto the start of the path and then follow that path. If false, the
|
||||||
|
/// path is considered _relative_, i.e. this path is added as an offset to
|
||||||
|
/// the current position of the target.
|
||||||
|
final bool _isAbsolute;
|
||||||
|
|
||||||
|
/// If true, then not only the target's position will follow the path, but
|
||||||
|
/// also the target's angle of rotation.
|
||||||
|
final bool _followDirection;
|
||||||
|
|
||||||
|
/// The path that the target will follow.
|
||||||
|
late final PathMetric _pathMetric;
|
||||||
|
|
||||||
|
/// Pre-computed length of the path.
|
||||||
|
late final double _pathLength;
|
||||||
|
|
||||||
|
/// Position offset that was applied to the target on the previous iteration.
|
||||||
|
/// This is needed in order to make updates to `target.position` incremental
|
||||||
|
/// (which in turn is necessary in order to allow multiple effects to be able
|
||||||
|
/// to apply to the same target simultaneously).
|
||||||
|
late Vector2 _lastOffset;
|
||||||
|
|
||||||
|
/// Target's angle of rotation on the previous iteration.
|
||||||
|
late double _lastAngle;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onStart() {
|
||||||
|
_lastOffset = Vector2.zero();
|
||||||
|
_lastAngle = 0;
|
||||||
|
if (_isAbsolute) {
|
||||||
|
final start = _pathMetric.getTangentForOffset(0)!;
|
||||||
|
target.position.x = _lastOffset.x = start.position.dx;
|
||||||
|
target.position.y = _lastOffset.y = start.position.dy;
|
||||||
|
if (_followDirection) {
|
||||||
|
target.angle = _lastAngle = -start.angle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void apply(double progress) {
|
||||||
|
final distance = progress * _pathLength;
|
||||||
|
final tangent = _pathMetric.getTangentForOffset(distance)!;
|
||||||
|
final offset = tangent.position;
|
||||||
|
target.position.x += offset.dx - _lastOffset.x;
|
||||||
|
target.position.y += offset.dy - _lastOffset.y;
|
||||||
|
_lastOffset.x = offset.dx;
|
||||||
|
_lastOffset.y = offset.dy;
|
||||||
|
if (_followDirection) {
|
||||||
|
target.angle += -tangent.angle - _lastAngle;
|
||||||
|
_lastAngle = -tangent.angle;
|
||||||
|
}
|
||||||
|
super.apply(progress);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:vector_math/vector_math_64.dart';
|
import 'package:vector_math/vector_math_64.dart';
|
||||||
|
|
||||||
import 'controllers/effect_controller.dart';
|
import 'controllers/effect_controller.dart';
|
||||||
@ -16,11 +14,6 @@ import 'transform2d_effect.dart';
|
|||||||
/// - [MoveEffect.to] will move the target in a straight line to the specified
|
/// - [MoveEffect.to] will move the target in a straight line to the specified
|
||||||
/// coordinates;
|
/// coordinates;
|
||||||
///
|
///
|
||||||
/// - [MoveEffect.along] will move the target along the specified path, which
|
|
||||||
/// may contain curved segments, but must be simply-connected. The `path`
|
|
||||||
/// argument is taken as relative to the target's position at the start of
|
|
||||||
/// the effect.
|
|
||||||
///
|
|
||||||
/// This effect applies incremental changes to the component's position, and
|
/// This effect applies incremental changes to the component's position, and
|
||||||
/// requires that any other effect or update logic applied to the same component
|
/// requires that any other effect or update logic applied to the same component
|
||||||
/// also used incremental updates.
|
/// also used incremental updates.
|
||||||
@ -32,9 +25,6 @@ class MoveEffect extends Transform2DEffect {
|
|||||||
factory MoveEffect.to(Vector2 destination, EffectController controller) =>
|
factory MoveEffect.to(Vector2 destination, EffectController controller) =>
|
||||||
_MoveToEffect(destination, controller);
|
_MoveToEffect(destination, controller);
|
||||||
|
|
||||||
factory MoveEffect.along(Path path, EffectController controller) =>
|
|
||||||
_MoveAlongPathEffect(path, controller);
|
|
||||||
|
|
||||||
Vector2 _offset;
|
Vector2 _offset;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -58,41 +48,3 @@ class _MoveToEffect extends MoveEffect {
|
|||||||
_offset = _destination - target.position;
|
_offset = _destination - target.position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Implementation class for [MoveEffect.along].
|
|
||||||
class _MoveAlongPathEffect extends MoveEffect {
|
|
||||||
_MoveAlongPathEffect(Path path, EffectController controller)
|
|
||||||
: _lastPosition = Vector2.zero(),
|
|
||||||
super.by(Vector2.zero(), controller) {
|
|
||||||
final metrics = path.computeMetrics().toList();
|
|
||||||
if (metrics.length != 1) {
|
|
||||||
throw ArgumentError(
|
|
||||||
'Only single-contour paths are allowed in MoveEffect.along',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
_pathMetric = metrics[0];
|
|
||||||
_pathLength = _pathMetric.length;
|
|
||||||
assert(_pathLength > 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
late final PathMetric _pathMetric;
|
|
||||||
late final double _pathLength;
|
|
||||||
late Vector2 _lastPosition;
|
|
||||||
|
|
||||||
@override
|
|
||||||
void apply(double progress) {
|
|
||||||
final distance = progress * _pathLength;
|
|
||||||
final tangent = _pathMetric.getTangentForOffset(distance)!;
|
|
||||||
final offset = tangent.position;
|
|
||||||
target.position.x += offset.dx - _lastPosition.x;
|
|
||||||
target.position.y += offset.dy - _lastPosition.y;
|
|
||||||
_lastPosition.setValues(offset.dx, offset.dy);
|
|
||||||
super.apply(progress);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
void reset() {
|
|
||||||
super.reset();
|
|
||||||
_lastPosition = Vector2.zero();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
127
packages/flame/test/effects/move_along_path_effect_test.dart
Normal file
127
packages/flame/test/effects/move_along_path_effect_test.dart
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
import 'dart:math';
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
|
import 'package:flame/components.dart';
|
||||||
|
import 'package:flame/effects.dart';
|
||||||
|
import 'package:flame/game.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('MoveAlongPathEffect', () {
|
||||||
|
test('relative path', () {
|
||||||
|
const tau = Transform2D.tau;
|
||||||
|
const x0 = 32.5;
|
||||||
|
const y0 = 14.88;
|
||||||
|
final game = FlameGame();
|
||||||
|
game.onGameResize(Vector2(100, 100));
|
||||||
|
final object = PositionComponent()..position = Vector2(x0, y0);
|
||||||
|
game.add(object);
|
||||||
|
game.update(0);
|
||||||
|
|
||||||
|
object.add(
|
||||||
|
MoveAlongPathEffect(
|
||||||
|
Path()
|
||||||
|
..addOval(Rect.fromCircle(center: const Offset(6, 10), radius: 50)),
|
||||||
|
LinearEffectController(1),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
game.update(0);
|
||||||
|
for (var i = 0; i < 100; i++) {
|
||||||
|
final a = tau * i / 100;
|
||||||
|
// Apparently, in Flutter circle paths are not truly circles, but only
|
||||||
|
// appear circle-ish to an unsuspecting observer. Which is why the
|
||||||
|
// precision in `closeTo()` is so low: only 0.1 pixels.
|
||||||
|
expect(object.position.x, closeTo(x0 + 6 + 50 * cos(a), 0.1));
|
||||||
|
expect(object.position.y, closeTo(y0 + 10 + 50 * sin(a), 0.1));
|
||||||
|
game.update(0.01);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('absolute path', () {
|
||||||
|
final game = FlameGame()..onGameResize(Vector2(100, 100));
|
||||||
|
final component = PositionComponent()..position = Vector2(17, -5);
|
||||||
|
game.add(component);
|
||||||
|
game.update(0);
|
||||||
|
|
||||||
|
component.add(
|
||||||
|
MoveAlongPathEffect(
|
||||||
|
Path()
|
||||||
|
..moveTo(1000, 300)
|
||||||
|
..lineTo(1200, 500),
|
||||||
|
EffectController(duration: 1),
|
||||||
|
absolute: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
game.update(0);
|
||||||
|
for (var i = 0; i < 10; i++) {
|
||||||
|
expect(component.position.x, closeTo(1000 + 200 * (i / 10), 1e-10));
|
||||||
|
expect(component.position.y, closeTo(300 + 200 * (i / 10), 1e-10));
|
||||||
|
game.update(0.1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('absolute oriented path', () {
|
||||||
|
final game = FlameGame()..onGameResize(Vector2(100, 100));
|
||||||
|
final component = PositionComponent(
|
||||||
|
position: Vector2(17, -5),
|
||||||
|
angle: -30.5,
|
||||||
|
);
|
||||||
|
game.add(component);
|
||||||
|
game.update(0);
|
||||||
|
|
||||||
|
component.add(
|
||||||
|
MoveAlongPathEffect(
|
||||||
|
Path() // pythagorean triangle, perimeter=600
|
||||||
|
..moveTo(200, 200)
|
||||||
|
..lineTo(290, 80)
|
||||||
|
..lineTo(450, 200)
|
||||||
|
..lineTo(200, 200),
|
||||||
|
EffectController(duration: 6),
|
||||||
|
absolute: true,
|
||||||
|
oriented: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
game.update(0);
|
||||||
|
for (var i = 0; i < 60; i++) {
|
||||||
|
if (i <= 15) {
|
||||||
|
expect(component.position.x, closeTo(200 + 6 * i, 1e-10));
|
||||||
|
expect(component.position.y, closeTo(200 - 8 * i, 1e-10));
|
||||||
|
expect(component.angle, closeTo(-asin(0.8), 1e-7));
|
||||||
|
} else if (i <= 35) {
|
||||||
|
expect(component.position.x, closeTo(290 + 8 * (i - 15), 1e-10));
|
||||||
|
expect(component.position.y, closeTo(80 + 6 * (i - 15), 1e-10));
|
||||||
|
expect(component.angle, closeTo(asin(0.6), 1e-7));
|
||||||
|
} else {
|
||||||
|
expect(component.position.x, closeTo(450 - 10 * (i - 35), 1e-10));
|
||||||
|
expect(component.position.y, closeTo(200, 1e-10));
|
||||||
|
expect(component.angle, closeTo(pi, 1e-7));
|
||||||
|
}
|
||||||
|
game.update(0.1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('errors', () {
|
||||||
|
final controller = LinearEffectController(0);
|
||||||
|
expect(
|
||||||
|
() => MoveAlongPathEffect(Path(), controller),
|
||||||
|
throwsArgumentError,
|
||||||
|
);
|
||||||
|
|
||||||
|
final path2 = Path()
|
||||||
|
..moveTo(10, 10)
|
||||||
|
..lineTo(10, 10);
|
||||||
|
expect(
|
||||||
|
() => MoveAlongPathEffect(path2, controller),
|
||||||
|
throwsArgumentError,
|
||||||
|
);
|
||||||
|
|
||||||
|
final path3 = Path()
|
||||||
|
..addOval(const Rect.fromLTWH(0, 0, 1, 1))
|
||||||
|
..addOval(const Rect.fromLTWH(2, 2, 1, 1));
|
||||||
|
expect(
|
||||||
|
() => MoveAlongPathEffect(path3, controller),
|
||||||
|
throwsArgumentError,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,6 +1,3 @@
|
|||||||
import 'dart:math';
|
|
||||||
import 'dart:ui';
|
|
||||||
|
|
||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/game.dart';
|
import 'package:flame/game.dart';
|
||||||
import 'package:flame/src/effects/controllers/linear_effect_controller.dart';
|
import 'package:flame/src/effects/controllers/linear_effect_controller.dart';
|
||||||
@ -44,56 +41,5 @@ void main() {
|
|||||||
expect(object.position.x, closeTo(5, 1e-15));
|
expect(object.position.x, closeTo(5, 1e-15));
|
||||||
expect(object.position.y, closeTo(-1, 1e-15));
|
expect(object.position.y, closeTo(-1, 1e-15));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('#along', () {
|
|
||||||
const tau = Transform2D.tau;
|
|
||||||
final game = FlameGame();
|
|
||||||
game.onGameResize(Vector2(100, 100));
|
|
||||||
final object = PositionComponent()..position = Vector2(3, 4);
|
|
||||||
game.add(object);
|
|
||||||
game.update(0);
|
|
||||||
|
|
||||||
object.add(
|
|
||||||
MoveEffect.along(
|
|
||||||
Path()
|
|
||||||
..addOval(Rect.fromCircle(center: const Offset(6, 10), radius: 50)),
|
|
||||||
LinearEffectController(1),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
game.update(0);
|
|
||||||
for (var i = 0; i < 100; i++) {
|
|
||||||
final a = tau * i / 100;
|
|
||||||
// Apparently, in Flutter circle paths are not truly circles, but only
|
|
||||||
// appear circle-ish to an unsuspecting observer. Which is why the
|
|
||||||
// precision in `closeTo()` is so low: only 0.1 pixels.
|
|
||||||
expect(object.position.x, closeTo(3 + 6 + 50 * cos(a), 0.1));
|
|
||||||
expect(object.position.y, closeTo(4 + 10 + 50 * sin(a), 0.1));
|
|
||||||
game.update(0.01);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
test('#along wrong arguments', () {
|
|
||||||
final controller = LinearEffectController(0);
|
|
||||||
expect(
|
|
||||||
() => MoveEffect.along(Path(), controller),
|
|
||||||
throwsArgumentError,
|
|
||||||
);
|
|
||||||
|
|
||||||
final path2 = Path()
|
|
||||||
..moveTo(10, 10)
|
|
||||||
..lineTo(10, 10);
|
|
||||||
expect(
|
|
||||||
() => MoveEffect.along(path2, controller),
|
|
||||||
throwsArgumentError,
|
|
||||||
);
|
|
||||||
|
|
||||||
final path3 = Path()
|
|
||||||
..addOval(const Rect.fromLTWH(0, 0, 1, 1))
|
|
||||||
..addOval(const Rect.fromLTWH(2, 2, 1, 1));
|
|
||||||
expect(
|
|
||||||
() => MoveEffect.along(path3, controller),
|
|
||||||
throwsArgumentError,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user