feat: Added componentsAtPoint() iterable (#1518)

This commit is contained in:
Pasha Stetsenko
2022-04-25 10:52:21 -07:00
committed by GitHub
parent 5591c10943
commit b99e35120d
19 changed files with 355 additions and 30 deletions

View File

@ -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

View File

@ -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);
}
}
}

View File

@ -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';

View File

@ -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<T>()) 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<ComponentPointPair> 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.

View File

@ -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>';
}

View File

@ -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);
}

View File

@ -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,
@ -214,14 +220,24 @@ class PositionComponent extends Component
/// component. The top and the left borders of the component are inclusive,
/// while the bottom and the right borders are exclusive.
@override
bool containsPoint(Vector2 point) {
final local = absoluteToLocal(point);
return (local.x >= 0) &&
(local.y >= 0) &&
(local.x < _size.x) &&
(local.y < _size.y);
bool containsLocalPoint(Vector2 point) {
return (point.x >= 0) &&
(point.y >= 0) &&
(point.x < _size.x) &&
(point.y < _size.y);
}
@override
bool containsPoint(Vector2 point) {
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) {

View File

@ -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<ComponentPointPair> 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

View File

@ -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;
}
}

View File

@ -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() {

View File

@ -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;

View File

@ -21,6 +21,9 @@ class MaxViewport extends Viewport {
@override
void clip(Canvas canvas) {}
@override
bool containsLocalPoint(Vector2 point) => true;
@override
void onViewportResize() {}
}

View File

@ -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.

View File

@ -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

View File

@ -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<ComponentPointPair> componentsAtPoint(Vector2 point) {
return const Iterable.empty();
}
@internal
Iterable<ComponentPointPair> componentsAtPointFromCamera(Vector2 point) {
return super.componentsAtPoint(point);
}
}

View File

@ -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.

View File

@ -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;

View File

@ -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<Vector2> _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<LineSegment> possibleIntersectionVertices(Rect? rect) {

View File

@ -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)),
],
);
});
});
});
}