From b99e35120dc4fe81ebfedc89a666286ec489384c Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Mon, 25 Apr 2022 10:52:21 -0700 Subject: [PATCH] feat: Added componentsAtPoint() iterable (#1518) --- doc/flame/components.md | 28 +++++++- .../camera_component_properties_example.dart | 49 +++++++++++++- packages/flame/lib/components.dart | 1 + .../flame/lib/src/components/component.dart | 65 +++++++++++++++++-- .../src/components/component_point_pair.dart | 27 ++++++++ .../mixins/coordinate_transform.dart | 24 +++++++ .../src/components/position_component.dart | 28 ++++++-- .../src/experimental/camera_component.dart | 21 +++++- .../src/experimental/circular_viewport.dart | 9 ++- .../fixed_aspect_ratio_viewport.dart | 7 +- .../src/experimental/fixed_size_viewport.dart | 5 ++ .../lib/src/experimental/max_viewport.dart | 3 + .../lib/src/experimental/viewfinder.dart | 2 +- .../flame/lib/src/experimental/viewport.dart | 8 +++ .../flame/lib/src/experimental/world.dart | 12 ++++ packages/flame/lib/src/game/flame_game.dart | 4 +- .../lib/src/geometry/circle_component.dart | 10 ++- .../lib/src/geometry/polygon_component.dart | 30 +++++++-- .../flame/test/components/component_test.dart | 50 ++++++++++++++ 19 files changed, 354 insertions(+), 29 deletions(-) create mode 100644 packages/flame/lib/src/components/component_point_pair.dart create mode 100644 packages/flame/lib/src/components/mixins/coordinate_transform.dart diff --git a/doc/flame/components.md b/doc/flame/components.md index 36157ece6a8..c0b3adae1ac 100644 --- a/doc/flame/components.md +++ b/doc/flame/components.md @@ -190,6 +190,32 @@ 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. + +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(); + } + }); +} +``` + + ### PositionType If you want to create a HUD (Head-up display) or another component that isn't positioned in relation to the game coordinates, you can change the `PositionType` of the component. @@ -370,7 +396,7 @@ use `animation.completed`. Example: ```dart -await animation.completed; +await animation.completed; doSomething(); // or alternatively 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 f3dbbd788c6..832233d8ecc 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 @@ -3,8 +3,9 @@ import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame/experimental.dart'; import 'package:flame/game.dart' hide Viewport; +import 'package:flame/input.dart'; -class CameraComponentPropertiesExample extends FlameGame { +class CameraComponentPropertiesExample extends FlameGame with HasTappables { static const description = ''' This example uses FixedSizeViewport which is dynamically sized and positioned based on the size of the game widget. @@ -13,6 +14,8 @@ class CameraComponentPropertiesExample extends FlameGame { green dot being the origin. The viewfinder uses custom anchor in order to declare its "center" half-way between the bottom left corner and the true center. + + Click at any point within the viewport to create a circle there. '''; CameraComponent? _camera; @@ -41,6 +44,19 @@ class CameraComponentPropertiesExample extends FlameGame { _camera?.viewport.size = size * 0.7; _camera?.viewport.position = size * 0.6; } + + @override + // 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()), + ); + } + } + } } class ViewportFrame extends Component { @@ -64,7 +80,7 @@ class ViewportFrame extends Component { class Background extends Component { final bgPaint = Paint()..color = const Color(0xffff0000); - final originPaint = Paint()..color = const Color(0xff2f8750); + final originPaint = Paint()..color = const Color(0xff19bf57); final axisPaint = Paint() ..strokeWidth = 1 ..style = PaintingStyle.stroke @@ -85,4 +101,33 @@ class Background extends Component { canvas.drawLine(Offset.zero, const Offset(10, 0), axisPaint); canvas.drawCircle(Offset.zero, 1.0, originPaint); } + + @override + bool containsLocalPoint(Vector2 point) => true; +} + +class ExpandingCircle extends CircleComponent { + ExpandingCircle(Offset center) + : super( + position: Vector2(center.dx, center.dy), + anchor: Anchor.center, + radius: 0, + paint: Paint() + ..color = const Color(0xffffffff) + ..style = PaintingStyle.stroke + ..strokeWidth = 1, + ); + + static const maxRadius = 50; + + @override + void update(double dt) { + radius += dt * 10; + if (radius >= maxRadius) { + removeFromParent(); + } else { + final opacity = 1 - radius / maxRadius; + paint.color = const Color(0xffffffff).withOpacity(opacity); + } + } } diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart index 72177ef23fd..b669a60fe91 100644 --- a/packages/flame/lib/components.dart +++ b/packages/flame/lib/components.dart @@ -3,6 +3,7 @@ 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/input/joystick_component.dart'; diff --git a/packages/flame/lib/src/components/component.dart b/packages/flame/lib/src/components/component.dart index d1ef7f0acbc..c222aea0379 100644 --- a/packages/flame/lib/src/components/component.dart +++ b/packages/flame/lib/src/components/component.dart @@ -3,11 +3,16 @@ import 'dart:collection'; import 'package:flutter/painting.dart'; import 'package:meta/meta.dart'; +import 'package:vector_math/vector_math_64.dart'; -import '../../components.dart'; -import '../../game.dart'; -import '../../input.dart'; import '../cache/value_cache.dart'; +import '../game/mixins/game.dart'; +import '../gestures/events.dart'; +import '../text.dart'; +import 'component_point_pair.dart'; +import 'component_set.dart'; +import 'mixins/coordinate_transform.dart'; +import 'position_type.dart'; /// [Component]s are the basic building blocks for your game. /// @@ -613,10 +618,56 @@ class Component { return (parent is T ? parent : parent?.findParent()) as T?; } - /// Called to check whether the point is to be counted as within the component - /// It needs to be overridden to have any effect, like it is in - /// PositionComponent. - bool containsPoint(Vector2 point) => false; + /// Checks whether the [point] is within this component's bounds. + /// + /// This method should be implemented for any component that has a visual + /// representation and non-zero size. The [point] is in the local coordinate + /// space. + bool containsLocalPoint(Vector2 point) => false; + + /// Same as [containsLocalPoint], but for a "global" [point]. + /// + /// This will be deprecated in the future, due to the notion of "global" point + /// not being well-defined. + bool containsPoint(Vector2 point) => containsLocalPoint(point); + + /// An iterable of descendant components intersecting the given point. The + /// [point] is in the local coordinate space. + /// + /// More precisely, imagine a ray originating at a certain point (x, y) on + /// the screen, and extending perpendicularly to the screen's surface into + /// your game's world. The purpose of this method is to find all components + /// 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 default implementation relies on the [CoordinateTransform] interface + /// to translate from the parent's coordinate system into the local one. Make + /// sure that your component implements this interface if it alters the + /// coordinate system when rendering. + /// + /// 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* { + 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); + } + } + } + if (containsLocalPoint(point)) { + yield ComponentPointPair(this, point); + } + } /// Usually this is not something that the user would want to call since the /// component list isn't re-ordered when it is called. diff --git a/packages/flame/lib/src/components/component_point_pair.dart b/packages/flame/lib/src/components/component_point_pair.dart new file mode 100644 index 00000000000..233a3cf8492 --- /dev/null +++ b/packages/flame/lib/src/components/component_point_pair.dart @@ -0,0 +1,27 @@ +import 'dart:ui'; + +import 'package:meta/meta.dart'; +import 'package:vector_math/vector_math_64.dart'; + +import 'component.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/components/mixins/coordinate_transform.dart b/packages/flame/lib/src/components/mixins/coordinate_transform.dart new file mode 100644 index 00000000000..1eb44372dff --- /dev/null +++ b/packages/flame/lib/src/components/mixins/coordinate_transform.dart @@ -0,0 +1,24 @@ +import 'package:vector_math/vector_math_64.dart'; +import '../component.dart'; + +/// Interface to be implemented by components that perform a coordinate change. +/// +/// Any [Component] that does any coordinate transformation of the canvas during +/// rendering should consider implementing this interface in order to describe +/// how the points from the parent's coordinate system relate to the component's +/// local coordinate system. +/// +/// This interface assumes that the component performs a "uniform" coordinate +/// transformation, that is, the transform applies to all children of the +/// component equally. If that is not the case (for example, the component does +/// different transformations for some of its children), then that component +/// must implement [Component.componentsAtPoint] method instead. +/// +/// The two methods of this interface convert between the parent's coordinate +/// space and the local coordinates. The methods may also return `null`, +/// indicating that the given cannot be mapped to any local/parent point. +abstract class CoordinateTransform { + Vector2? parentToLocal(Vector2 point); + + Vector2? localToParent(Vector2 point); +} diff --git a/packages/flame/lib/src/components/position_component.dart b/packages/flame/lib/src/components/position_component.dart index 1e1efb0f910..685a9decd0e 100644 --- a/packages/flame/lib/src/components/position_component.dart +++ b/packages/flame/lib/src/components/position_component.dart @@ -8,6 +8,7 @@ import '../extensions/vector2.dart'; import '../game/notifying_vector2.dart'; import '../game/transform2d.dart'; import 'component.dart'; +import 'mixins/coordinate_transform.dart'; /// A [Component] implementation that represents an object that can be /// freely moved around the screen, rotated, and scaled. @@ -59,7 +60,12 @@ import 'component.dart'; /// do not specify the size of a PositionComponent, then it will be /// equal to zero and the component won't be able to respond to taps. class PositionComponent extends Component - implements AnchorProvider, AngleProvider, PositionProvider, ScaleProvider { + implements + AnchorProvider, + AngleProvider, + PositionProvider, + ScaleProvider, + CoordinateTransform { PositionComponent({ Vector2? position, Vector2? size, @@ -213,15 +219,25 @@ class PositionComponent extends Component /// Test whether the `point` (given in global coordinates) lies within this /// component. The top and the left borders of the component are inclusive, /// while the bottom and the right borders are exclusive. + @override + bool containsLocalPoint(Vector2 point) { + return (point.x >= 0) && + (point.y >= 0) && + (point.x < _size.x) && + (point.y < _size.y); + } + @override bool containsPoint(Vector2 point) { - final local = absoluteToLocal(point); - return (local.x >= 0) && - (local.y >= 0) && - (local.x < _size.x) && - (local.y < _size.y); + return containsLocalPoint(absoluteToLocal(point)); } + @override + Vector2 parentToLocal(Vector2 point) => transform.globalToLocal(point); + + @override + Vector2 localToParent(Vector2 point) => transform.localToGlobal(point); + /// Convert local coordinates of a point [point] inside the component /// into the parent's coordinate space. Vector2 positionOf(Vector2 point) { diff --git a/packages/flame/lib/src/experimental/camera_component.dart b/packages/flame/lib/src/experimental/camera_component.dart index ca0dddd3706..7e2ac74c570 100644 --- a/packages/flame/lib/src/experimental/camera_component.dart +++ b/packages/flame/lib/src/experimental/camera_component.dart @@ -4,6 +4,7 @@ import 'package:meta/meta.dart'; import 'package:vector_math/vector_math_64.dart'; import '../components/component.dart'; +import '../components/component_point_pair.dart'; import '../components/position_component.dart'; import '../effects/controllers/effect_controller.dart'; import '../effects/move_effect.dart'; @@ -92,7 +93,7 @@ class CameraComponent extends Component { viewport.clip(canvas); try { currentCameras.add(this); - canvas.transform(viewfinder.transformMatrix.storage); + canvas.transform(viewfinder.transform.transformMatrix.storage); world.renderFromCamera(canvas); viewfinder.renderTree(canvas); } finally { @@ -105,6 +106,24 @@ class CameraComponent extends Component { canvas.restore(); } + @override + Iterable componentsAtPoint(Vector2 point) sync* { + final viewportPoint = point - viewport.position; + if (world.isMounted && currentCameras.length < maxCamerasDepth) { + if (viewport.containsPoint(viewportPoint)) { + try { + currentCameras.add(this); + final worldPoint = viewfinder.transform.globalToLocal(viewportPoint); + yield* world.componentsAtPointFromCamera(worldPoint); + yield* viewfinder.componentsAtPoint(worldPoint); + } finally { + currentCameras.removeLast(); + } + } + } + yield* viewport.componentsAtPoint(viewportPoint); + } + /// A camera that currently performs rendering. /// /// This variable is set to `this` when we begin rendering the world through diff --git a/packages/flame/lib/src/experimental/circular_viewport.dart b/packages/flame/lib/src/experimental/circular_viewport.dart index ff2fd44ad59..2f90be303b4 100644 --- a/packages/flame/lib/src/experimental/circular_viewport.dart +++ b/packages/flame/lib/src/experimental/circular_viewport.dart @@ -12,14 +12,19 @@ class CircularViewport extends Viewport { } Path _clipPath = Path(); + double _radiusSquared = 0; @override void clip(Canvas canvas) => canvas.clipPath(_clipPath, doAntiAlias: false); + @override + bool containsLocalPoint(Vector2 point) => point.length2 <= _radiusSquared; + @override void onViewportResize() { + assert(size.x == size.y, 'Viewport shape is not circular: $size'); final x = size.x / 2; - final y = size.y / 2; - _clipPath = Path()..addOval(Rect.fromLTRB(-x, -y, x, y)); + _clipPath = Path()..addOval(Rect.fromLTRB(-x, -x, x, x)); + _radiusSquared = x * x; } } diff --git a/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart index 3fed3954155..48de6818b3d 100644 --- a/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart +++ b/packages/flame/lib/src/experimental/fixed_aspect_ratio_viewport.dart @@ -23,7 +23,12 @@ class FixedAspectRatioViewport extends Viewport { } @override - void clip(Canvas canvas) => canvas.clipRect(_clipRect); + void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false); + + @override + bool containsLocalPoint(Vector2 point) { + return point.x.abs() <= size.x / 2 && point.y.abs() <= size.y / 2; + } @override void onViewportResize() { diff --git a/packages/flame/lib/src/experimental/fixed_size_viewport.dart b/packages/flame/lib/src/experimental/fixed_size_viewport.dart index 26dbbdf1904..3f4b0adcaed 100644 --- a/packages/flame/lib/src/experimental/fixed_size_viewport.dart +++ b/packages/flame/lib/src/experimental/fixed_size_viewport.dart @@ -24,6 +24,11 @@ class FixedSizeViewport extends Viewport { @override void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false); + @override + bool containsLocalPoint(Vector2 point) { + return point.x.abs() <= size.x / 2 && point.y.abs() <= size.y / 2; + } + @override void onViewportResize() { final x = size.x / 2; diff --git a/packages/flame/lib/src/experimental/max_viewport.dart b/packages/flame/lib/src/experimental/max_viewport.dart index 70357306081..2b296b4e6b8 100644 --- a/packages/flame/lib/src/experimental/max_viewport.dart +++ b/packages/flame/lib/src/experimental/max_viewport.dart @@ -21,6 +21,9 @@ class MaxViewport extends Viewport { @override void clip(Canvas canvas) {} + @override + bool containsLocalPoint(Vector2 point) => true; + @override void onViewportResize() {} } diff --git a/packages/flame/lib/src/experimental/viewfinder.dart b/packages/flame/lib/src/experimental/viewfinder.dart index 89d2732f946..7dc4ba5a70c 100644 --- a/packages/flame/lib/src/experimental/viewfinder.dart +++ b/packages/flame/lib/src/experimental/viewfinder.dart @@ -21,7 +21,7 @@ class Viewfinder extends Component final Transform2D _transform = Transform2D(); @internal - Matrix4 get transformMatrix => _transform.transformMatrix; + Transform2D get transform => _transform; /// The game coordinates of a point that is to be positioned at the center /// of the viewport. diff --git a/packages/flame/lib/src/experimental/viewport.dart b/packages/flame/lib/src/experimental/viewport.dart index 45c35eca017..5bf1b40e399 100644 --- a/packages/flame/lib/src/experimental/viewport.dart +++ b/packages/flame/lib/src/experimental/viewport.dart @@ -66,6 +66,14 @@ abstract class Viewport extends Component implements PositionProvider { /// This API must be implemented by all viewports. void clip(Canvas canvas); + /// Tests whether the given point lies within the viewport. + /// + /// This method must be consistent with the action of [clip], in the sense + /// that [containsLocalPoint] must return true if and only if that point on + /// the canvas is not clipped by [clip]. + @override + bool containsLocalPoint(Vector2 point); + /// Override in order to perform a custom action upon resize. /// /// A typical use-case would be to adjust the viewport's clip mask to match diff --git a/packages/flame/lib/src/experimental/world.dart b/packages/flame/lib/src/experimental/world.dart index 801b3390ef5..5b1f0ad694f 100644 --- a/packages/flame/lib/src/experimental/world.dart +++ b/packages/flame/lib/src/experimental/world.dart @@ -1,8 +1,10 @@ import 'dart:ui'; import 'package:meta/meta.dart'; +import 'package:vector_math/vector_math_64.dart'; import '../components/component.dart'; +import '../components/component_point_pair.dart'; import 'camera_component.dart'; /// The root component for all game world elements. @@ -20,4 +22,14 @@ class World extends Component { assert(CameraComponent.currentCamera != null); super.renderTree(canvas); } + + @override + Iterable componentsAtPoint(Vector2 point) { + return const Iterable.empty(); + } + + @internal + Iterable componentsAtPointFromCamera(Vector2 point) { + return super.componentsAtPoint(point); + } } diff --git a/packages/flame/lib/src/game/flame_game.dart b/packages/flame/lib/src/game/flame_game.dart index 9aa7ceb4d2a..4eec71e6a7a 100644 --- a/packages/flame/lib/src/game/flame_game.dart +++ b/packages/flame/lib/src/game/flame_game.dart @@ -131,8 +131,8 @@ class FlameGame extends Component with Game { /// Whether a point is within the boundaries of the visible part of the game. @override - bool containsPoint(Vector2 p) { - return p.x > 0 && p.y > 0 && p.x < size.x && p.y < size.y; + bool containsLocalPoint(Vector2 p) { + return p.x >= 0 && p.y >= 0 && p.x < size.x && p.y < size.y; } /// Returns the current time in seconds with microseconds precision. diff --git a/packages/flame/lib/src/geometry/circle_component.dart b/packages/flame/lib/src/geometry/circle_component.dart index d1be16a25c4..0a52386ddf9 100644 --- a/packages/flame/lib/src/geometry/circle_component.dart +++ b/packages/flame/lib/src/geometry/circle_component.dart @@ -85,6 +85,14 @@ class CircleComponent extends ShapeComponent { scaledRadius * scaledRadius; } + @override + bool containsLocalPoint(Vector2 point) { + final radius = size.x / 2; + final dx = point.x - radius; + final dy = point.y - radius; + return dx * dx + dy * dy <= radius * radius; + } + /// Returns the locus of points in which the provided line segment intersect /// the circle. /// @@ -94,7 +102,7 @@ class CircleComponent extends ShapeComponent { LineSegment line, { double epsilon = double.minPositive, }) { - double sq(double x) => pow(x, 2).toDouble(); + double sq(double x) => x * x; final cx = absoluteCenter.x; final cy = absoluteCenter.y; diff --git a/packages/flame/lib/src/geometry/polygon_component.dart b/packages/flame/lib/src/geometry/polygon_component.dart index 72bdef53a8a..ec96cf76bba 100644 --- a/packages/flame/lib/src/geometry/polygon_component.dart +++ b/packages/flame/lib/src/geometry/polygon_component.dart @@ -1,13 +1,16 @@ import 'dart:math'; -import 'dart:ui' hide Canvas; +import 'dart:ui'; import 'package:collection/collection.dart'; import 'package:meta/meta.dart'; -import '../../cache.dart'; -import '../../components.dart'; -import '../../extensions.dart'; -import '../../geometry.dart'; +import '../anchor.dart'; +import '../cache/value_cache.dart'; +import '../components/component.dart'; +import '../extensions/rect.dart'; +import '../extensions/vector2.dart'; +import 'line_segment.dart'; +import 'shape_component.dart'; class PolygonComponent extends ShapeComponent { final List _vertices; @@ -221,6 +224,23 @@ class PolygonComponent extends ShapeComponent { return true; } + @override + bool containsLocalPoint(Vector2 point) { + if (size.x == 0 || size.y == 0) { + return false; + } + for (var i = 0; i < _vertices.length; i++) { + final edge = getEdge(i, vertices: vertices); + final isOutside = (edge.to.x - edge.from.x) * (point.y - edge.from.y) - + (point.x - edge.from.x) * (edge.to.y - edge.from.y) > + 0; + if (isOutside) { + return false; + } + } + return true; + } + /// Return all vertices as [LineSegment]s that intersect [rect], if [rect] /// is null return all vertices as [LineSegment]s. List possibleIntersectionVertices(Rect? rect) { diff --git a/packages/flame/test/components/component_test.dart b/packages/flame/test/components/component_test.dart index 97ad8ddedf4..f37f8019d0b 100644 --- a/packages/flame/test/components/component_test.dart +++ b/packages/flame/test/components/component_test.dart @@ -501,6 +501,56 @@ void main() { }, ); }); + + group('componentsAtPoint', () { + testWithFlameGame('nested components', (game) async { + final componentA = PositionComponent() + ..size = Vector2(200, 150) + ..scale = Vector2.all(2) + ..position = Vector2(350, 50) + ..addToParent(game); + final componentB = CircleComponent(radius: 10) + ..position = Vector2(150, 75) + ..anchor = Anchor.center + ..addToParent(componentA); + 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)), + ], + ); + }); + }); }); }