Skip to content

Commit

Permalink
feat: Method componentsAtPoint now reports the "stacktrace" of poin…
Browse files Browse the repository at this point in the history
…ts (#1615)
  • Loading branch information
st-pasha authored May 17, 2022
1 parent aeaf999 commit e239896
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 97 deletions.
23 changes: 12 additions & 11 deletions doc/flame/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ scheduled for addition.

### Ensuring a component has a given parent

When a component requires to be added to a specific parent type the
When a component requires to be added to a specific parent type the
`ParentIsA` mixin can be used to enforce a strongly typed parent.

Example:
Expand All @@ -175,7 +175,7 @@ class MyComponent extends Component with ParentIsA<MyParentComponent> {
}
```

If you try to add `MyComponent` to a parent that is not `MyParentComponent`,
If you try to add `MyComponent` to a parent that is not `MyParentComponent`,
an assertion error will be thrown.

### Querying child components
Expand Down Expand Up @@ -211,24 +211,25 @@ void update(double dt) {

### Querying components at a specific point on the screen

The method `componentsAtPoint()` allows you to check which components have been rendered at a
specific point on the screen. The returned value is an iterable which contains both the components
and the coordinates of the query point in those components' local coordinates. The iterable
retrieves the components in the front-to-back order, i.e. first the components in the front,
followed by the components in the back.
The method `componentsAtPoint()` allows you to check which components were rendered at some point
on the screen. The returned value is an iterable of components, but you can also obtain the
coordinates of the initial point in each component's local coordinate space by providing a writable
`List<Vector2>` as a second parameter.

The iterable retrieves the components in the front-to-back order, i.e. first the components in the
front, followed by the components in the back.

This method can only return components that implement the method `containsLocalPoint()`. The
`PositionComponent` (which is the base class for many components in Flame) provides such an
implementation. However, if you're defining a custom class that derives from `Component`, you'd have
to implement the `containsLocalPoint()` method yourself.

Here is an example of how `componentsAtPoint()` can be used:

```dart
void onDragUpdate(DragUpdateInfo info) {
game.componentsAtPoint(info.widget).forEach((p) {
if (p.component is DropTarget) {
p.component.highlight();
game.componentsAtPoint(info.widget).forEach((component) {
if (component is DropTarget) {
component.highlight();
}
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,11 @@ class CameraComponentPropertiesExample extends FlameGame with HasTappables {
// ignore: must_call_super
void onTapDown(int pointerId, TapDownInfo info) {
final canvasPoint = info.eventPosition.widget;
for (final cp in componentsAtPoint(canvasPoint)) {
if (cp.component is Background) {
cp.component.add(
ExpandingCircle(cp.point.toOffset()),
final nested = <Vector2>[];
for (final component in componentsAtPoint(canvasPoint, nested)) {
if (component is Background) {
component.add(
ExpandingCircle(nested.last.toOffset()),
);
}
}
Expand Down
1 change: 0 additions & 1 deletion packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ export 'src/anchor.dart';
export 'src/collisions/has_collision_detection.dart';
export 'src/collisions/hitboxes/screen_hitbox.dart';
export 'src/components/component.dart';
export 'src/components/component_point_pair.dart';
export 'src/components/component_set.dart';
export 'src/components/custom_painter_component.dart';
export 'src/components/fps_component.dart';
Expand Down
22 changes: 15 additions & 7 deletions packages/flame/lib/src/components/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:collection';

import 'package:flame/src/cache/value_cache.dart';
import 'package:flame/src/components/component_point_pair.dart';
import 'package:flame/src/components/component_set.dart';
import 'package:flame/src/components/mixins/coordinate_transform.dart';
import 'package:flame/src/components/position_type.dart';
Expand Down Expand Up @@ -641,9 +640,13 @@ class Component {
/// that intersect with this ray, in the order from those that are closest to
/// the user to those that are farthest.
///
/// The return value is an [Iterable] of `(component, point)` pairs, which
/// gives not only the components themselves, but also the points of
/// intersection, in their respective local coordinates.
/// The return value is an [Iterable] of components. If the [nestedPoints]
/// parameter is given, then it will also report the points of intersection
/// for each component in its local coordinate space. Specifically, the last
/// element in the list is the point in the coordinate space of the returned
/// component, the element before the last is in that component's parent's
/// coordinate space, and so on. The [nestedPoints] list must be growable and
/// modifiable.
///
/// The default implementation relies on the [CoordinateTransform] interface
/// to translate from the parent's coordinate system into the local one. Make
Expand All @@ -653,21 +656,26 @@ class Component {
/// If your component overrides [renderTree], then it almost certainly needs
/// to override this method as well, so that this method can find all rendered
/// components wherever they are.
Iterable<ComponentPointPair> componentsAtPoint(Vector2 point) sync* {
Iterable<Component> componentsAtPoint(
Vector2 point, [
List<Vector2>? nestedPoints,
]) sync* {
nestedPoints?.add(point);
if (_children != null) {
for (final child in _children!.reversed()) {
Vector2? childPoint = point;
if (child is CoordinateTransform) {
childPoint = (child as CoordinateTransform).parentToLocal(point);
}
if (childPoint != null) {
yield* child.componentsAtPoint(childPoint);
yield* child.componentsAtPoint(childPoint, nestedPoints);
}
}
}
if (containsLocalPoint(point)) {
yield ComponentPointPair(this, point);
yield this;
}
nestedPoints?.removeLast();
}

//#endregion
Expand Down
26 changes: 0 additions & 26 deletions packages/flame/lib/src/components/component_point_pair.dart

This file was deleted.

14 changes: 8 additions & 6 deletions packages/flame/lib/src/experimental/camera_component.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:ui';

import 'package:flame/src/components/component.dart';
import 'package:flame/src/components/component_point_pair.dart';
import 'package:flame/src/components/position_component.dart';
import 'package:flame/src/effects/controllers/effect_controller.dart';
import 'package:flame/src/effects/move_effect.dart';
Expand Down Expand Up @@ -111,24 +110,27 @@ class CameraComponent extends Component {
}

@override
Iterable<ComponentPointPair> componentsAtPoint(Vector2 point) sync* {
Iterable<Component> componentsAtPoint(
Vector2 point, [
List<Vector2>? nestedPoints,
]) sync* {
final viewportPoint = Vector2(
point.x - viewport.position.x + viewport.anchor.x * viewport.size.x,
point.y - viewport.position.y + viewport.anchor.y * viewport.size.y,
);
if (world.isMounted && currentCameras.length < maxCamerasDepth) {
if (viewport.containsPoint(viewportPoint)) {
if (viewport.containsLocalPoint(viewportPoint)) {
try {
currentCameras.add(this);
final worldPoint = viewfinder.transform.globalToLocal(viewportPoint);
yield* world.componentsAtPointFromCamera(worldPoint);
yield* viewfinder.componentsAtPoint(worldPoint);
yield* world.componentsAtPointFromCamera(worldPoint, nestedPoints);
yield* viewfinder.componentsAtPoint(worldPoint, nestedPoints);
} finally {
currentCameras.removeLast();
}
}
}
yield* viewport.componentsAtPoint(viewportPoint);
yield* viewport.componentsAtPoint(viewportPoint, nestedPoints);
}

/// A camera that currently performs rendering.
Expand Down
16 changes: 12 additions & 4 deletions packages/flame/lib/src/experimental/world.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:ui';

import 'package:flame/src/components/component.dart';
import 'package:flame/src/components/component_point_pair.dart';
import 'package:flame/src/experimental/camera_component.dart';
import 'package:meta/meta.dart';
import 'package:vector_math/vector_math_64.dart';
Expand All @@ -23,12 +22,21 @@ class World extends Component {
}

@override
Iterable<ComponentPointPair> componentsAtPoint(Vector2 point) {
bool containsLocalPoint(Vector2 point) => true;

@override
Iterable<Component> componentsAtPoint(
Vector2 point, [
List<Vector2>? nestedPoints,
]) {
return const Iterable.empty();
}

@internal
Iterable<ComponentPointPair> componentsAtPointFromCamera(Vector2 point) {
return super.componentsAtPoint(point);
Iterable<Component> componentsAtPointFromCamera(
Vector2 point,
List<Vector2>? nestedPoints,
) {
return super.componentsAtPoint(point, nestedPoints);
}
}
79 changes: 42 additions & 37 deletions packages/flame/test/components/component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -609,51 +609,50 @@ void main() {

group('componentsAtPoint', () {
testWithFlameGame('nested components', (game) async {
final componentA = PositionComponent()
final compA = PositionComponent()
..size = Vector2(200, 150)
..scale = Vector2.all(2)
..position = Vector2(350, 50)
..addToParent(game);
final componentB = CircleComponent(radius: 10)
final compB = CircleComponent(radius: 10)
..position = Vector2(150, 75)
..anchor = Anchor.center
..addToParent(componentA);
..addToParent(compA);
await game.ready();

expect(
game.componentsAtPoint(Vector2.zero()).toList(),
[ComponentPointPair(game, Vector2.zero())],
);
expect(
game.componentsAtPoint(Vector2(400, 100)).toList(),
[
ComponentPointPair(componentA, Vector2(25, 25)),
ComponentPointPair(game, Vector2(400, 100)),
],
);
expect(
game.componentsAtPoint(Vector2(650, 200)).toList(),
[
ComponentPointPair(componentB, Vector2(10, 10)),
ComponentPointPair(componentA, Vector2(150, 75)),
ComponentPointPair(game, Vector2(650, 200)),
],
);
expect(
game.componentsAtPoint(Vector2(664, 214)).toList(),
[
ComponentPointPair(componentB, Vector2(17, 17)),
ComponentPointPair(componentA, Vector2(157, 82)),
ComponentPointPair(game, Vector2(664, 214)),
],
);
expect(
game.componentsAtPoint(Vector2(664, 216)).toList(),
[
ComponentPointPair(componentA, Vector2(157, 83)),
ComponentPointPair(game, Vector2(664, 216)),
],
);
void matchComponentsAtPoint(Vector2 point, List<_Pair> expected) {
final nested = <Vector2>[];
var i = 0;
for (final component in game.componentsAtPoint(point, nested)) {
expect(i, lessThan(expected.length));
expect(component, expected[i].component);
expect(nested, expected[i].points);
i++;
}
expect(i, expected.length);
}

matchComponentsAtPoint(Vector2(0, 0), [
_Pair(game, [Vector2(0, 0)])
]);
matchComponentsAtPoint(Vector2(400, 100), [
_Pair(compA, [Vector2(400, 100), Vector2(25, 25)]),
_Pair(game, [Vector2(400, 100)]),
]);
matchComponentsAtPoint(Vector2(650, 200), [
_Pair(compB, [Vector2(650, 200), Vector2(150, 75), Vector2(10, 10)]),
_Pair(compA, [Vector2(650, 200), Vector2(150, 75)]),
_Pair(game, [Vector2(650, 200)]),
]);
matchComponentsAtPoint(Vector2(664, 214), [
_Pair(compB, [Vector2(664, 214), Vector2(157, 82), Vector2(17, 17)]),
_Pair(compA, [Vector2(664, 214), Vector2(157, 82)]),
_Pair(game, [Vector2(664, 214)]),
]);
matchComponentsAtPoint(Vector2(664, 216), [
_Pair(compA, [Vector2(664, 216), Vector2(157, 83)]),
_Pair(game, [Vector2(664, 216)]),
]);
});
});
});
Expand Down Expand Up @@ -721,3 +720,9 @@ class _SelfRemovingOnMountComponent extends Component {
removeFromParent();
}
}

class _Pair {
_Pair(this.component, this.points);
final Component component;
final List<Vector2> points;
}
36 changes: 35 additions & 1 deletion packages/flame/test/experimental/camera_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ void main() {
expect(camera.viewfinder.children.length, 1);
});

testWithFlameGame('setBound', (game) async {
testWithFlameGame('setBounds', (game) async {
final world = World()..addToParent(game);
final camera = CameraComponent(world: world)..addToParent(game);
await game.ready();
Expand Down Expand Up @@ -106,5 +106,39 @@ void main() {
isNull,
);
});

testWithFlameGame('componentsAtPoint', (game) async {
final world = World();
final camera = CameraComponent(
world: world,
viewport: FixedSizeViewport(600, 400),
)
..viewport.anchor = Anchor.center
..viewport.position = Vector2(400, 300)
..viewfinder.position = Vector2(100, 50);
final component = PositionComponent(
size: Vector2(300, 100),
position: Vector2(50, 30),
);
world.add(component);
game.addAll([world, camera]);
await game.ready();

final nested = <Vector2>[];
final it = game.componentsAtPoint(Vector2(400, 300), nested).iterator;
expect(it.moveNext(), true);
expect(it.current, component);
expect(nested, [Vector2(400, 300), Vector2(100, 50), Vector2(50, 20)]);
expect(it.moveNext(), true);
expect(it.current, world);
expect(nested, [Vector2(400, 300), Vector2(100, 50)]);
expect(it.moveNext(), true);
expect(it.current, camera.viewport);
expect(nested, [Vector2(400, 300), Vector2(300, 200)]);
expect(it.moveNext(), true);
expect(it.current, game);
expect(nested, [Vector2(400, 300)]);
expect(it.moveNext(), false);
});
});
}

0 comments on commit e239896

Please sign in to comment.