Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added componentsAtPoint() iterable #1518

Merged
merged 33 commits into from
Apr 25, 2022
Merged
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7c8a7ab
Add CoordinateTransform interface
st-pasha Apr 6, 2022
d9c9f0f
componentsAtPoint() method
st-pasha Apr 6, 2022
8e2b51b
use ComponentPoint
st-pasha Apr 6, 2022
cb2c460
update doc-comments
st-pasha Apr 6, 2022
3aeb594
componentsAtPoint for CameraComponent
st-pasha Apr 6, 2022
c65507d
format
st-pasha Apr 6, 2022
40ac29e
example
st-pasha Apr 6, 2022
a410075
Added a test
st-pasha Apr 7, 2022
5b88f33
fix example
st-pasha Apr 7, 2022
9d76969
undeprecate containsPoint()
st-pasha Apr 7, 2022
b3b8014
Add a docstring
st-pasha Apr 7, 2022
47f451e
docs for CoordinateTransform
st-pasha Apr 7, 2022
b7b430a
"localMethods on viewports
st-pasha Apr 7, 2022
520e548
format
st-pasha Apr 7, 2022
02fa2f8
format
st-pasha Apr 7, 2022
f372bd4
Avoid creating extra vectors in CircleComponent
st-pasha Apr 7, 2022
ff8d020
Simplify sq() function
st-pasha Apr 7, 2022
43eb191
add @internal
st-pasha Apr 7, 2022
fee5c95
Move ComponentPoint to separate file
st-pasha Apr 7, 2022
4e9c73c
Merge branch 'main' into ps/components-at-point
st-pasha Apr 7, 2022
4694c2a
Merge branch 'main' into ps/components-at-point
st-pasha Apr 8, 2022
1029ab6
containsLocalPoint() for PolygonComponent
st-pasha Apr 8, 2022
cc99b81
fix imports
st-pasha Apr 8, 2022
c89862a
Merge branch 'main' into ps/components-at-point
st-pasha Apr 21, 2022
c30a0e8
Use CircleComponent
st-pasha Apr 21, 2022
5f63bab
Merge branch 'main' into ps/components-at-point
st-pasha Apr 22, 2022
6ea3fd1
rename ComponentPoint -> ComponentPointPair
st-pasha Apr 22, 2022
57e36a1
docs
st-pasha Apr 22, 2022
674dfb2
Merge branch 'main' into ps/components-at-point
st-pasha Apr 22, 2022
f68400d
proofread docs
st-pasha Apr 22, 2022
3d3c373
Merge branch 'main' into ps/components-at-point
st-pasha Apr 22, 2022
ddec92a
Merge branch 'main' into ps/components-at-point
st-pasha Apr 25, 2022
62bcb37
Merge branch 'main' into ps/components-at-point
st-pasha Apr 25, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions 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 which components have been rendered at a specific
st-pasha marked this conversation as resolved.
Show resolved Hide resolved
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 coordinate spaces. 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 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
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)) {
spydon marked this conversation as resolved.
Show resolved Hide resolved
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
22 changes: 21 additions & 1 deletion packages/flame/lib/src/experimental/camera_component.dart
Original file line number Diff line number Diff line change
@@ -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 'max_viewport.dart';
import 'viewfinder.dart';
import 'viewport.dart';
Expand Down Expand Up @@ -85,7 +87,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 @@ -98,6 +100,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