mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-02 11:43:19 +08:00
feat: Added componentsAtPoint() iterable (#1518)
This commit is contained in:
@ -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
|
### PositionType
|
||||||
If you want to create a HUD (Head-up display) or another component that isn't positioned in relation
|
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.
|
to the game coordinates, you can change the `PositionType` of the component.
|
||||||
|
|||||||
@ -3,8 +3,9 @@ import 'dart:ui';
|
|||||||
import 'package:flame/components.dart';
|
import 'package:flame/components.dart';
|
||||||
import 'package:flame/experimental.dart';
|
import 'package:flame/experimental.dart';
|
||||||
import 'package:flame/game.dart' hide Viewport;
|
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 = '''
|
static const description = '''
|
||||||
This example uses FixedSizeViewport which is dynamically sized and
|
This example uses FixedSizeViewport which is dynamically sized and
|
||||||
positioned based on the size of the game widget.
|
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
|
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
|
declare its "center" half-way between the bottom left corner and the true
|
||||||
center.
|
center.
|
||||||
|
|
||||||
|
Click at any point within the viewport to create a circle there.
|
||||||
''';
|
''';
|
||||||
|
|
||||||
CameraComponent? _camera;
|
CameraComponent? _camera;
|
||||||
@ -41,6 +44,19 @@ class CameraComponentPropertiesExample extends FlameGame {
|
|||||||
_camera?.viewport.size = size * 0.7;
|
_camera?.viewport.size = size * 0.7;
|
||||||
_camera?.viewport.position = size * 0.6;
|
_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 {
|
class ViewportFrame extends Component {
|
||||||
@ -64,7 +80,7 @@ class ViewportFrame extends Component {
|
|||||||
|
|
||||||
class Background extends Component {
|
class Background extends Component {
|
||||||
final bgPaint = Paint()..color = const Color(0xffff0000);
|
final bgPaint = Paint()..color = const Color(0xffff0000);
|
||||||
final originPaint = Paint()..color = const Color(0xff2f8750);
|
final originPaint = Paint()..color = const Color(0xff19bf57);
|
||||||
final axisPaint = Paint()
|
final axisPaint = Paint()
|
||||||
..strokeWidth = 1
|
..strokeWidth = 1
|
||||||
..style = PaintingStyle.stroke
|
..style = PaintingStyle.stroke
|
||||||
@ -85,4 +101,33 @@ class Background extends Component {
|
|||||||
canvas.drawLine(Offset.zero, const Offset(10, 0), axisPaint);
|
canvas.drawLine(Offset.zero, const Offset(10, 0), axisPaint);
|
||||||
canvas.drawCircle(Offset.zero, 1.0, originPaint);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,6 +3,7 @@ export 'src/anchor.dart';
|
|||||||
export 'src/collisions/has_collision_detection.dart';
|
export 'src/collisions/has_collision_detection.dart';
|
||||||
export 'src/collisions/hitboxes/screen_hitbox.dart';
|
export 'src/collisions/hitboxes/screen_hitbox.dart';
|
||||||
export 'src/components/component.dart';
|
export 'src/components/component.dart';
|
||||||
|
export 'src/components/component_point_pair.dart';
|
||||||
export 'src/components/component_set.dart';
|
export 'src/components/component_set.dart';
|
||||||
export 'src/components/custom_painter_component.dart';
|
export 'src/components/custom_painter_component.dart';
|
||||||
export 'src/components/input/joystick_component.dart';
|
export 'src/components/input/joystick_component.dart';
|
||||||
|
|||||||
@ -3,11 +3,16 @@ import 'dart:collection';
|
|||||||
|
|
||||||
import 'package:flutter/painting.dart';
|
import 'package:flutter/painting.dart';
|
||||||
import 'package:meta/meta.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 '../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.
|
/// [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?;
|
return (parent is T ? parent : parent?.findParent<T>()) as T?;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Called to check whether the point is to be counted as within the component
|
/// Checks whether the [point] is within this component's bounds.
|
||||||
/// It needs to be overridden to have any effect, like it is in
|
///
|
||||||
/// PositionComponent.
|
/// This method should be implemented for any component that has a visual
|
||||||
bool containsPoint(Vector2 point) => false;
|
/// 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
|
/// Usually this is not something that the user would want to call since the
|
||||||
/// component list isn't re-ordered when it is called.
|
/// component list isn't re-ordered when it is called.
|
||||||
|
|||||||
27
packages/flame/lib/src/components/component_point_pair.dart
Normal file
27
packages/flame/lib/src/components/component_point_pair.dart
Normal 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>';
|
||||||
|
}
|
||||||
@ -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);
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ import '../extensions/vector2.dart';
|
|||||||
import '../game/notifying_vector2.dart';
|
import '../game/notifying_vector2.dart';
|
||||||
import '../game/transform2d.dart';
|
import '../game/transform2d.dart';
|
||||||
import 'component.dart';
|
import 'component.dart';
|
||||||
|
import 'mixins/coordinate_transform.dart';
|
||||||
|
|
||||||
/// A [Component] implementation that represents an object that can be
|
/// A [Component] implementation that represents an object that can be
|
||||||
/// freely moved around the screen, rotated, and scaled.
|
/// 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
|
/// 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.
|
/// equal to zero and the component won't be able to respond to taps.
|
||||||
class PositionComponent extends Component
|
class PositionComponent extends Component
|
||||||
implements AnchorProvider, AngleProvider, PositionProvider, ScaleProvider {
|
implements
|
||||||
|
AnchorProvider,
|
||||||
|
AngleProvider,
|
||||||
|
PositionProvider,
|
||||||
|
ScaleProvider,
|
||||||
|
CoordinateTransform {
|
||||||
PositionComponent({
|
PositionComponent({
|
||||||
Vector2? position,
|
Vector2? position,
|
||||||
Vector2? size,
|
Vector2? size,
|
||||||
@ -214,14 +220,24 @@ class PositionComponent extends Component
|
|||||||
/// component. The top and the left borders of the component are inclusive,
|
/// component. The top and the left borders of the component are inclusive,
|
||||||
/// while the bottom and the right borders are exclusive.
|
/// while the bottom and the right borders are exclusive.
|
||||||
@override
|
@override
|
||||||
bool containsPoint(Vector2 point) {
|
bool containsLocalPoint(Vector2 point) {
|
||||||
final local = absoluteToLocal(point);
|
return (point.x >= 0) &&
|
||||||
return (local.x >= 0) &&
|
(point.y >= 0) &&
|
||||||
(local.y >= 0) &&
|
(point.x < _size.x) &&
|
||||||
(local.x < _size.x) &&
|
(point.y < _size.y);
|
||||||
(local.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
|
/// Convert local coordinates of a point [point] inside the component
|
||||||
/// into the parent's coordinate space.
|
/// into the parent's coordinate space.
|
||||||
Vector2 positionOf(Vector2 point) {
|
Vector2 positionOf(Vector2 point) {
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import 'package:meta/meta.dart';
|
|||||||
import 'package:vector_math/vector_math_64.dart';
|
import 'package:vector_math/vector_math_64.dart';
|
||||||
|
|
||||||
import '../components/component.dart';
|
import '../components/component.dart';
|
||||||
|
import '../components/component_point_pair.dart';
|
||||||
import '../components/position_component.dart';
|
import '../components/position_component.dart';
|
||||||
import '../effects/controllers/effect_controller.dart';
|
import '../effects/controllers/effect_controller.dart';
|
||||||
import '../effects/move_effect.dart';
|
import '../effects/move_effect.dart';
|
||||||
@ -92,7 +93,7 @@ class CameraComponent extends Component {
|
|||||||
viewport.clip(canvas);
|
viewport.clip(canvas);
|
||||||
try {
|
try {
|
||||||
currentCameras.add(this);
|
currentCameras.add(this);
|
||||||
canvas.transform(viewfinder.transformMatrix.storage);
|
canvas.transform(viewfinder.transform.transformMatrix.storage);
|
||||||
world.renderFromCamera(canvas);
|
world.renderFromCamera(canvas);
|
||||||
viewfinder.renderTree(canvas);
|
viewfinder.renderTree(canvas);
|
||||||
} finally {
|
} finally {
|
||||||
@ -105,6 +106,24 @@ class CameraComponent extends Component {
|
|||||||
canvas.restore();
|
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.
|
/// A camera that currently performs rendering.
|
||||||
///
|
///
|
||||||
/// This variable is set to `this` when we begin rendering the world through
|
/// This variable is set to `this` when we begin rendering the world through
|
||||||
|
|||||||
@ -12,14 +12,19 @@ class CircularViewport extends Viewport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Path _clipPath = Path();
|
Path _clipPath = Path();
|
||||||
|
double _radiusSquared = 0;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void clip(Canvas canvas) => canvas.clipPath(_clipPath, doAntiAlias: false);
|
void clip(Canvas canvas) => canvas.clipPath(_clipPath, doAntiAlias: false);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool containsLocalPoint(Vector2 point) => point.length2 <= _radiusSquared;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewportResize() {
|
void onViewportResize() {
|
||||||
|
assert(size.x == size.y, 'Viewport shape is not circular: $size');
|
||||||
final x = size.x / 2;
|
final x = size.x / 2;
|
||||||
final y = size.y / 2;
|
_clipPath = Path()..addOval(Rect.fromLTRB(-x, -x, x, x));
|
||||||
_clipPath = Path()..addOval(Rect.fromLTRB(-x, -y, x, y));
|
_radiusSquared = x * x;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,12 @@ class FixedAspectRatioViewport extends Viewport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@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
|
@override
|
||||||
void onViewportResize() {
|
void onViewportResize() {
|
||||||
|
|||||||
@ -24,6 +24,11 @@ class FixedSizeViewport extends Viewport {
|
|||||||
@override
|
@override
|
||||||
void clip(Canvas canvas) => canvas.clipRect(_clipRect, doAntiAlias: false);
|
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
|
@override
|
||||||
void onViewportResize() {
|
void onViewportResize() {
|
||||||
final x = size.x / 2;
|
final x = size.x / 2;
|
||||||
|
|||||||
@ -21,6 +21,9 @@ class MaxViewport extends Viewport {
|
|||||||
@override
|
@override
|
||||||
void clip(Canvas canvas) {}
|
void clip(Canvas canvas) {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool containsLocalPoint(Vector2 point) => true;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void onViewportResize() {}
|
void onViewportResize() {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ class Viewfinder extends Component
|
|||||||
final Transform2D _transform = Transform2D();
|
final Transform2D _transform = Transform2D();
|
||||||
|
|
||||||
@internal
|
@internal
|
||||||
Matrix4 get transformMatrix => _transform.transformMatrix;
|
Transform2D get transform => _transform;
|
||||||
|
|
||||||
/// The game coordinates of a point that is to be positioned at the center
|
/// The game coordinates of a point that is to be positioned at the center
|
||||||
/// of the viewport.
|
/// of the viewport.
|
||||||
|
|||||||
@ -66,6 +66,14 @@ abstract class Viewport extends Component implements PositionProvider {
|
|||||||
/// This API must be implemented by all viewports.
|
/// This API must be implemented by all viewports.
|
||||||
void clip(Canvas canvas);
|
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.
|
/// 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
|
/// A typical use-case would be to adjust the viewport's clip mask to match
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import 'dart:ui';
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
import 'package:vector_math/vector_math_64.dart';
|
||||||
|
|
||||||
import '../components/component.dart';
|
import '../components/component.dart';
|
||||||
|
import '../components/component_point_pair.dart';
|
||||||
import 'camera_component.dart';
|
import 'camera_component.dart';
|
||||||
|
|
||||||
/// The root component for all game world elements.
|
/// The root component for all game world elements.
|
||||||
@ -20,4 +22,14 @@ class World extends Component {
|
|||||||
assert(CameraComponent.currentCamera != null);
|
assert(CameraComponent.currentCamera != null);
|
||||||
super.renderTree(canvas);
|
super.renderTree(canvas);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Iterable<ComponentPointPair> componentsAtPoint(Vector2 point) {
|
||||||
|
return const Iterable.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@internal
|
||||||
|
Iterable<ComponentPointPair> componentsAtPointFromCamera(Vector2 point) {
|
||||||
|
return super.componentsAtPoint(point);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -131,8 +131,8 @@ class FlameGame extends Component with Game {
|
|||||||
|
|
||||||
/// Whether a point is within the boundaries of the visible part of the game.
|
/// Whether a point is within the boundaries of the visible part of the game.
|
||||||
@override
|
@override
|
||||||
bool containsPoint(Vector2 p) {
|
bool containsLocalPoint(Vector2 p) {
|
||||||
return p.x > 0 && p.y > 0 && p.x < size.x && p.y < size.y;
|
return p.x >= 0 && p.y >= 0 && p.x < size.x && p.y < size.y;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the current time in seconds with microseconds precision.
|
/// Returns the current time in seconds with microseconds precision.
|
||||||
|
|||||||
@ -85,6 +85,14 @@ class CircleComponent extends ShapeComponent {
|
|||||||
scaledRadius * scaledRadius;
|
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
|
/// Returns the locus of points in which the provided line segment intersect
|
||||||
/// the circle.
|
/// the circle.
|
||||||
///
|
///
|
||||||
@ -94,7 +102,7 @@ class CircleComponent extends ShapeComponent {
|
|||||||
LineSegment line, {
|
LineSegment line, {
|
||||||
double epsilon = double.minPositive,
|
double epsilon = double.minPositive,
|
||||||
}) {
|
}) {
|
||||||
double sq(double x) => pow(x, 2).toDouble();
|
double sq(double x) => x * x;
|
||||||
|
|
||||||
final cx = absoluteCenter.x;
|
final cx = absoluteCenter.x;
|
||||||
final cy = absoluteCenter.y;
|
final cy = absoluteCenter.y;
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
import 'dart:math';
|
import 'dart:math';
|
||||||
import 'dart:ui' hide Canvas;
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../../cache.dart';
|
import '../anchor.dart';
|
||||||
import '../../components.dart';
|
import '../cache/value_cache.dart';
|
||||||
import '../../extensions.dart';
|
import '../components/component.dart';
|
||||||
import '../../geometry.dart';
|
import '../extensions/rect.dart';
|
||||||
|
import '../extensions/vector2.dart';
|
||||||
|
import 'line_segment.dart';
|
||||||
|
import 'shape_component.dart';
|
||||||
|
|
||||||
class PolygonComponent extends ShapeComponent {
|
class PolygonComponent extends ShapeComponent {
|
||||||
final List<Vector2> _vertices;
|
final List<Vector2> _vertices;
|
||||||
@ -221,6 +224,23 @@ class PolygonComponent extends ShapeComponent {
|
|||||||
return true;
|
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]
|
/// Return all vertices as [LineSegment]s that intersect [rect], if [rect]
|
||||||
/// is null return all vertices as [LineSegment]s.
|
/// is null return all vertices as [LineSegment]s.
|
||||||
List<LineSegment> possibleIntersectionVertices(Rect? rect) {
|
List<LineSegment> possibleIntersectionVertices(Rect? rect) {
|
||||||
|
|||||||
@ -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)),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user