diff --git a/doc/flame/components.md b/doc/flame/components.md index 0f04a2f676e..84cdf24d079 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -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: @@ -175,7 +175,7 @@ class MyComponent extends Component with ParentIsA { } ``` -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 @@ -211,11 +211,13 @@ 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` 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 @@ -223,12 +225,11 @@ implementation. However, if you're defining a custom class that derives from `Co 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(); } }); } diff --git a/examples/lib/stories/camera_and_viewport/camera_component_properties_example.dart b/examples/lib/stories/camera_and_viewport/camera_component_properties_example.dart index 7c1c91ef2fe..c3badc73674 100644 --- a/examples/lib/stories/camera_and_viewport/camera_component_properties_example.dart +++ b/examples/lib/stories/camera_and_viewport/camera_component_properties_example.dart @@ -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 = []; + for (final component in componentsAtPoint(canvasPoint, nested)) { + if (component is Background) { + component.add( + ExpandingCircle(nested.last.toOffset()), ); } } diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index 7779032b4aa..ca368115da2 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -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'; diff --git a/packages/flame/lib/src/components/component.dart b/packages/flame/lib/src/components/component.dart index 271aeb6b769..e2b7500f2f8 100644 --- a/packages/flame/lib/src/components/component.dart +++ b/packages/flame/lib/src/components/component.dart @@ -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'; @@ -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 @@ -653,7 +656,11 @@ 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 componentsAtPoint(Vector2 point) sync* { + Iterable componentsAtPoint( + Vector2 point, [ + List? nestedPoints, + ]) sync* { + nestedPoints?.add(point); if (_children != null) { for (final child in _children!.reversed()) { Vector2? childPoint = point; @@ -661,13 +668,14 @@ class Component { 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 diff --git a/packages/flame/lib/src/components/component_point_pair.dart b/packages/flame/lib/src/components/component_point_pair.dart deleted file mode 100644 index 92806d588dd..00000000000 --- a/packages/flame/lib/src/components/component_point_pair.dart +++ /dev/null @@ -1,26 +0,0 @@ -import 'dart:ui'; - -import 'package:flame/src/components/component.dart'; -import 'package:meta/meta.dart'; -import 'package:vector_math/vector_math_64.dart'; - -/// A simple tuple of a component and a point. This is a helper class for the -/// [Component.componentsAtPoint] method. -@immutable -class ComponentPointPair { - const ComponentPointPair(this.component, this.point); - final Component component; - final Vector2 point; - - @override - bool operator ==(Object other) => - other is ComponentPointPair && - other.component == component && - other.point == point; - - @override - int get hashCode => hashValues(component, point); - - @override - String toString() => '<$component, $point>'; -} diff --git a/packages/flame/lib/src/experimental/camera_component.dart b/packages/flame/lib/src/experimental/camera_component.dart index e6764764797..7bcdd6ca5bf 100644 --- a/packages/flame/lib/src/experimental/camera_component.dart +++ b/packages/flame/lib/src/experimental/camera_component.dart @@ -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'; @@ -111,24 +110,27 @@ class CameraComponent extends Component { } @override - Iterable componentsAtPoint(Vector2 point) sync* { + Iterable componentsAtPoint( + Vector2 point, [ + List? 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. diff --git a/packages/flame/lib/src/experimental/world.dart b/packages/flame/lib/src/experimental/world.dart index 1c13fec0f26..cc98f2555e4 100644 --- a/packages/flame/lib/src/experimental/world.dart +++ b/packages/flame/lib/src/experimental/world.dart @@ -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'; @@ -23,12 +22,21 @@ class World extends Component { } @override - Iterable componentsAtPoint(Vector2 point) { + bool containsLocalPoint(Vector2 point) => true; + + @override + Iterable componentsAtPoint( + Vector2 point, [ + List? nestedPoints, + ]) { return const Iterable.empty(); } @internal - Iterable componentsAtPointFromCamera(Vector2 point) { - return super.componentsAtPoint(point); + Iterable componentsAtPointFromCamera( + Vector2 point, + List? nestedPoints, + ) { + return super.componentsAtPoint(point, nestedPoints); } } diff --git a/packages/flame/test/components/component_test.dart b/packages/flame/test/components/component_test.dart index 7cd347438d9..45f00949e76 100644 --- a/packages/flame/test/components/component_test.dart +++ b/packages/flame/test/components/component_test.dart @@ -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 = []; + 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)]), + ]); }); }); }); @@ -721,3 +720,9 @@ class _SelfRemovingOnMountComponent extends Component { removeFromParent(); } } + +class _Pair { + _Pair(this.component, this.points); + final Component component; + final List points; +} diff --git a/packages/flame/test/experimental/camera_component_test.dart b/packages/flame/test/experimental/camera_component_test.dart index 8fc79d5b951..1d3ebed8cd3 100644 --- a/packages/flame/test/experimental/camera_component_test.dart +++ b/packages/flame/test/experimental/camera_component_test.dart @@ -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(); @@ -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 = []; + 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); + }); }); }