Skip to content

Commit

Permalink
feat: Added componentsAtPoint() iterable (#1518)
Browse files Browse the repository at this point in the history
  • Loading branch information
st-pasha authored Apr 25, 2022
1 parent 5591c10 commit b99e351
Show file tree
Hide file tree
Showing 19 changed files with 354 additions and 29 deletions.
28 changes: 27 additions & 1 deletion doc/flame/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -370,7 +396,7 @@ use `animation.completed`.
Example:

```dart
await animation.completed;
await animation.completed;
doSomething();
// or alternatively
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand All @@ -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);
}
}
}
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
65 changes: 58 additions & 7 deletions packages/flame/lib/src/components/component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
///
Expand Down Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions packages/flame/lib/src/components/component_point_pair.dart
Original file line number Diff line number Diff line change
@@ -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>';
}
24 changes: 24 additions & 0 deletions packages/flame/lib/src/components/mixins/coordinate_transform.dart
Original file line number Diff line number Diff line change
@@ -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);
}
28 changes: 22 additions & 6 deletions packages/flame/lib/src/components/position_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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) {
Expand Down
21 changes: 20 additions & 1 deletion packages/flame/lib/src/experimental/camera_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down
9 changes: 7 additions & 2 deletions packages/flame/lib/src/experimental/circular_viewport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Loading

0 comments on commit b99e351

Please sign in to comment.