diff --git a/doc/flame/examples/lib/drag_events.dart b/doc/flame/examples/lib/drag_events.dart index 1ae5701c0..0743954b8 100644 --- a/doc/flame/examples/lib/drag_events.dart +++ b/doc/flame/examples/lib/drag_events.dart @@ -83,7 +83,7 @@ class DragTarget extends PositionComponent with DragCallbacks { @override void onDragUpdate(DragUpdateEvent event) { - _trails[event.pointerId]!.addPoint(event.localPosition); + _trails[event.pointerId]!.addPoint(event.localEndPosition); } @override @@ -240,6 +240,6 @@ class Star extends PositionComponent with DragCallbacks { @override void onDragUpdate(DragUpdateEvent event) { - position += event.delta; + position += event.localDelta; } } diff --git a/doc/tutorials/klondike/app/lib/step4/components/card.dart b/doc/tutorials/klondike/app/lib/step4/components/card.dart index 821b02533..7ddf4f5cb 100644 --- a/doc/tutorials/klondike/app/lib/step4/components/card.dart +++ b/doc/tutorials/klondike/app/lib/step4/components/card.dart @@ -241,8 +241,7 @@ class Card extends PositionComponent with DragCallbacks { if (!_isDragging) { return; } - final cameraZoom = findGame()!.camera.viewfinder.zoom; - final delta = event.delta / cameraZoom; + final delta = event.localDelta; position.add(delta); attachedCards.forEach((card) => card.position.add(delta)); } diff --git a/doc/tutorials/klondike/app/lib/step5/components/card.dart b/doc/tutorials/klondike/app/lib/step5/components/card.dart index 49e3dd5e0..5ffbfeb70 100644 --- a/doc/tutorials/klondike/app/lib/step5/components/card.dart +++ b/doc/tutorials/klondike/app/lib/step5/components/card.dart @@ -265,8 +265,7 @@ class Card extends PositionComponent if (!_isDragging) { return; } - final cameraZoom = findGame()!.camera.viewfinder.zoom; - final delta = event.delta / cameraZoom; + final delta = event.localDelta; position.add(delta); attachedCards.forEach((card) => card.position.add(delta)); } diff --git a/doc/tutorials/klondike/step4.md b/doc/tutorials/klondike/step4.md index 1143a5ddc..2b84a339c 100644 --- a/doc/tutorials/klondike/step4.md +++ b/doc/tutorials/klondike/step4.md @@ -502,19 +502,13 @@ the card, so that it is rendered above all others. Without this, the card would During the drag, the `onDragUpdate` event will be called continuously. Using this callback we will be updating the position of the card so that it follows the movement of the finger (or the mouse). The `event` object passed to this callback contains the most recent coordinate of the point of -touch, and also the `delta` property -- which is the displacement vector since the previous call of -`onDragUpdate`. The only problem is that this delta is measured in screen pixels, whereas we want -it to be in game world units. The conversion between the two is given by the camera zoom level, so -we will add an extra method to determine the zoom level: +touch, and also the `localDelta` property -- which is the displacement vector since the previous +call of `onDragUpdate`, considering the camera zoom. ```dart @override void onDragUpdate(DragUpdateEvent event) { - final cameraZoom = (findGame()! as FlameGame) - .camera - .viewfinder - .zoom; - position += event.delta / cameraZoom; + position += event.delta; } ``` @@ -606,11 +600,7 @@ to `false`: if (!isDragged) { return; } - final cameraZoom = (findGame()! as FlameGame) - .camera - .viewfinder - .zoom; - position += event.delta / cameraZoom; + position += event.delta; } @override @@ -940,11 +930,7 @@ the `onDragUpdate` method: if (!isDragged) { return; } - final cameraZoom = (findGame()! as FlameGame) - .camera - .viewfinder - .zoom; - final delta = event.delta / cameraZoom; + final delta = event.delta; position.add(delta); attachedCards.forEach((card) => card.position.add(delta)); } diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart index e75bb8eed..090b6e8fc 100644 --- a/examples/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/drag_callbacks_example.dart @@ -36,7 +36,7 @@ class DraggableBall extends Ball with DragCallbacks { @override void onDragUpdate(DragUpdateEvent event) { - body.applyLinearImpulse(event.delta * 1000); + body.applyLinearImpulse(event.localDelta * 1000); } @override diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart index 9fb059fa1..51f324791 100644 --- a/examples/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/joints/mouse_joint.dart @@ -55,7 +55,7 @@ class MouseJointWorld extends Forge2DWorld @override void onDragUpdate(DragUpdateEvent info) { - mouseJoint?.setTarget(info.localPosition); + mouseJoint?.setTarget(info.localEndPosition); } @override diff --git a/examples/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart b/examples/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart index 6ded867e6..d78046273 100644 --- a/examples/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart +++ b/examples/lib/stories/bridge_libraries/flame_forge2d/utils/boxes.dart @@ -64,10 +64,7 @@ class DraggableBox extends Box with DragCallbacks { @override bool onDragUpdate(DragUpdateEvent info) { - final target = info.localPosition; - if (target.isNaN) { - return false; - } + final target = info.localEndPosition; final mouseJointDef = MouseJointDef() ..maxForce = body.mass * 300 ..dampingRatio = 0 diff --git a/examples/lib/stories/input/drag_callbacks_example.dart b/examples/lib/stories/input/drag_callbacks_example.dart index 67babc77f..025023c0a 100644 --- a/examples/lib/stories/input/drag_callbacks_example.dart +++ b/examples/lib/stories/input/drag_callbacks_example.dart @@ -39,7 +39,6 @@ class DraggableEmber extends Ember with DragCallbacks { @override void onDragUpdate(DragUpdateEvent event) { - event.continuePropagation = true; - return; + position += event.localDelta; } } diff --git a/packages/flame/lib/src/camera/camera_component.dart b/packages/flame/lib/src/camera/camera_component.dart index bd1023dfd..76f63a657 100644 --- a/packages/flame/lib/src/camera/camera_component.dart +++ b/packages/flame/lib/src/camera/camera_component.dart @@ -1,3 +1,4 @@ +import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; import 'package:flame/src/camera/behaviors/bounded_position_behavior.dart'; import 'package:flame/src/camera/behaviors/follow_behavior.dart'; @@ -6,9 +7,6 @@ import 'package:flame/src/camera/viewfinder.dart'; import 'package:flame/src/camera/viewport.dart'; import 'package:flame/src/camera/viewports/fixed_resolution_viewport.dart'; import 'package:flame/src/camera/viewports/max_viewport.dart'; -import 'package:flame/src/camera/world.dart'; -import 'package:flame/src/components/core/component.dart'; -import 'package:flame/src/components/position_component.dart'; import 'package:flame/src/effects/controllers/effect_controller.dart'; import 'package:flame/src/effects/move_by_effect.dart'; import 'package:flame/src/effects/move_effect.dart'; @@ -223,22 +221,44 @@ class CameraComponent extends Component { return viewport.localToGlobal(viewfinderPosition, output: output); } - final _viewportPoint = Vector2.zero(); - @override - Iterable componentsAtPoint( - Vector2 point, [ - List? nestedPoints, - ]) sync* { - final viewportPoint = viewport.globalToLocal(point, output: _viewportPoint); - yield* viewport.componentsAtPoint(viewportPoint, nestedPoints); + Iterable componentsAtLocation( + T locationContext, + List? nestedContexts, + T? Function(CoordinateTransform, T) transformContext, + bool Function(Component, T) checkContains, + ) sync* { + final viewportPoint = transformContext(viewport, locationContext); + if (viewportPoint == null) { + return; + } + + yield* viewport.componentsAtLocation( + viewportPoint, + nestedContexts, + transformContext, + checkContains, + ); if ((world?.isMounted ?? false) && currentCameras.length < maxCamerasDepth) { - if (viewport.containsLocalPoint(_viewportPoint)) { + if (checkContains(viewport, viewportPoint)) { currentCameras.add(this); - final worldPoint = viewfinder.transform.globalToLocal(_viewportPoint); - yield* viewfinder.componentsAtPoint(worldPoint, nestedPoints); - yield* world!.componentsAtPoint(worldPoint, nestedPoints); + final worldPoint = transformContext(viewfinder, viewportPoint); + if (worldPoint == null) { + return; + } + yield* viewfinder.componentsAtLocation( + worldPoint, + nestedContexts, + transformContext, + checkContains, + ); + yield* world!.componentsAtLocation( + worldPoint, + nestedContexts, + transformContext, + checkContains, + ); currentCameras.removeLast(); } } diff --git a/packages/flame/lib/src/components/core/component.dart b/packages/flame/lib/src/components/core/component.dart index 0ca23bdc0..2f00ed46e 100644 --- a/packages/flame/lib/src/components/core/component.dart +++ b/packages/flame/lib/src/components/core/component.dart @@ -63,8 +63,8 @@ import 'package:meta/meta.dart'; /// [update]d, and then all the components will be [render]ed. /// /// You may also need to override [containsLocalPoint] if the component needs to -/// respond to tap events or similar; the [componentsAtPoint] may also need to -/// be overridden if you have reimplemented [renderTree]. +/// respond to tap events or similar; the [componentsAtLocation] may also need +/// to be overridden if you have reimplemented [renderTree]. class Component { Component({ Iterable? children, @@ -704,28 +704,65 @@ class Component { Iterable componentsAtPoint( Vector2 point, [ List? nestedPoints, - ]) sync* { - nestedPoints?.add(point); + ]) { + return componentsAtLocation( + point, + nestedPoints, + (transform, point) => transform.parentToLocal(point), + (component, point) => component.containsLocalPoint(point), + ); + } + + /// This is a generic implementation of [componentsAtPoint]; refer to those + /// docs for context. + /// + /// This will find components intersecting a given location context [T]. The + /// context can be a single point or a more complicated structure. How to + /// interpret the structure T is determined by the provided lambdas, + /// [transformContext] and [checkContains]. + /// + /// A simple choice of T would be a simple point (i.e. Vector2). In that case + /// transformContext needs to be able to transform a Vector2 on the parent + /// coordinate space into the coordinate space of a provided + /// [CoordinateTransform]; and [checkContains] must be able to determine if + /// a given [Component] "contains" the Vector2 (the definition of "contains" + /// will vary and shall be determined by the nature of the chosen location + /// context [T]). + Iterable componentsAtLocation( + T locationContext, + List? nestedContexts, + T? Function(CoordinateTransform, T) transformContext, + bool Function(Component, T) checkContains, + ) sync* { + nestedContexts?.add(locationContext); if (_children != null) { for (final child in _children!.reversed()) { if (child is IgnoreEvents && child.ignoreEvents) { continue; } - Vector2? childPoint = point; + T? childPoint = locationContext; if (child is CoordinateTransform) { - childPoint = (child as CoordinateTransform).parentToLocal(point); + childPoint = transformContext( + child as CoordinateTransform, + locationContext, + ); } if (childPoint != null) { - yield* child.componentsAtPoint(childPoint, nestedPoints); + yield* child.componentsAtLocation( + childPoint, + nestedContexts, + transformContext, + checkContains, + ); } } } final shouldIgnoreEvents = this is IgnoreEvents && (this as IgnoreEvents).ignoreEvents; - if (containsLocalPoint(point) && !shouldIgnoreEvents) { + if (checkContains(this, locationContext) && !shouldIgnoreEvents) { yield this; } - nestedPoints?.removeLast(); + nestedContexts?.removeLast(); } //#endregion diff --git a/packages/flame/lib/src/components/input/joystick_component.dart b/packages/flame/lib/src/components/input/joystick_component.dart index 104e9d0d4..4f8ff5d06 100644 --- a/packages/flame/lib/src/components/input/joystick_component.dart +++ b/packages/flame/lib/src/components/input/joystick_component.dart @@ -113,7 +113,7 @@ class JoystickComponent extends HudMarginComponent with DragCallbacks { @override bool onDragUpdate(DragUpdateEvent event) { - _unscaledDelta.add(event.delta); + _unscaledDelta.add(event.localDelta); return false; } diff --git a/packages/flame/lib/src/components/mixins/coordinate_transform.dart b/packages/flame/lib/src/components/mixins/coordinate_transform.dart index 1f59f0ae5..8bc451908 100644 --- a/packages/flame/lib/src/components/mixins/coordinate_transform.dart +++ b/packages/flame/lib/src/components/mixins/coordinate_transform.dart @@ -12,7 +12,7 @@ import 'package:vector_math/vector_math_64.dart'; /// 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. +/// must implement [Component.componentsAtLocation] 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`, diff --git a/packages/flame/lib/src/components/mixins/ignore_events.dart b/packages/flame/lib/src/components/mixins/ignore_events.dart index eb23abea3..5accbc8fd 100644 --- a/packages/flame/lib/src/components/mixins/ignore_events.dart +++ b/packages/flame/lib/src/components/mixins/ignore_events.dart @@ -3,7 +3,7 @@ import 'package:flame/src/components/core/component.dart'; /// This mixin allows a component and all it's descendants to ignore events. /// /// Do note that this will also ignore the component and its descendants in -/// calls to [Component.componentsAtPoint]. +/// calls to [Component.componentsAtLocation]. /// /// If you want to dynamically use this mixin, you can add it and set /// [ignoreEvents] true or false at runtime. diff --git a/packages/flame/lib/src/components/route.dart b/packages/flame/lib/src/components/route.dart index dc16b1205..cc0ed6660 100644 --- a/packages/flame/lib/src/components/route.dart +++ b/packages/flame/lib/src/components/route.dart @@ -1,13 +1,10 @@ import 'dart:ui'; -import 'package:flame/src/components/core/component.dart'; -import 'package:flame/src/components/mixins/parent_is_a.dart'; -import 'package:flame/src/components/position_component.dart'; +import 'package:flame/components.dart'; import 'package:flame/src/components/router_component.dart'; import 'package:flame/src/effects/effect.dart'; import 'package:flame/src/rendering/decorator.dart'; import 'package:meta/meta.dart'; -import 'package:vector_math/vector_math_64.dart'; /// [Route] is a light-weight component that builds and manages a page. /// @@ -166,12 +163,19 @@ class Route extends PositionComponent with ParentIsA { } @override - Iterable componentsAtPoint( - Vector2 point, [ - List? nestedPoints, - ]) { + Iterable componentsAtLocation( + T locationContext, + List? nestedContexts, + T? Function(CoordinateTransform, T) transformContext, + bool Function(Component, T) checkContains, + ) { if (isRendered) { - return super.componentsAtPoint(point, nestedPoints); + return super.componentsAtLocation( + locationContext, + nestedContexts, + transformContext, + checkContains, + ); } else { return const Iterable.empty(); } diff --git a/packages/flame/lib/src/events/messages/displacement_event.dart b/packages/flame/lib/src/events/messages/displacement_event.dart new file mode 100644 index 000000000..108523bc2 --- /dev/null +++ b/packages/flame/lib/src/events/messages/displacement_event.dart @@ -0,0 +1,151 @@ +import 'package:flame/components.dart'; +import 'package:flame/src/events/messages/location_context_event.dart'; +import 'package:flame/src/game/game.dart'; + +/// Location context for the Displacement Event. +/// +/// This represents a user event that has a start and end locations, as the +/// ones that are represented by a [DisplacementEvent]. +typedef DisplacementContext = ({ + Vector2 start, + Vector2 end, +}); + +extension DisplacementContextDelta on DisplacementContext { + /// Displacement delta + Vector2 get delta => end - start; +} + +/// Base class for events that contain two points on the screen, representing +/// some sort of movement (from a "start" to and "end") and having a "delta". +/// These include: drag events and (in the future) pointer move events. +/// +/// This class includes properties that describe both positions where the event +/// has occurred (start and end) and the delta (i.e. displacement) represented +/// by the event. +abstract class DisplacementEvent + extends LocationContextEvent { + DisplacementEvent( + this._game, { + required this.deviceStartPosition, + required this.deviceEndPosition, + }); + final Game _game; + + @Deprecated( + 'use deviceStartPosition instead; will be removed in version 1.12.0', + ) + late final Vector2 devicePosition = deviceStartPosition; + + @Deprecated( + 'use canvasStartPosition instead; will be removed in version 1.12.0', + ) + late final Vector2 canvasPosition = canvasStartPosition; + + @Deprecated( + 'use localStartPosition instead; will be removed in version 1.12.0', + ) + Vector2 get localPosition => localStartPosition; + + @Deprecated('use localDelta instead; will be removed in version 1.12.0') + Vector2 get delta => localDelta; + + /// Event start position in the coordinate space of the device -- either the + /// phone, or the browser window, or the app. + /// + /// If the game runs in a full-screen mode, then this would be equal to the + /// [canvasStartPosition]. Otherwise, the [deviceStartPosition] is the + /// Flutter-level global position. + final Vector2 deviceStartPosition; + + /// Event start position in the coordinate space of the game widget, i.e. + /// relative to the game canvas. + /// + /// This could be considered the Flame-level global position. + late final Vector2 canvasStartPosition = + _game.convertGlobalToLocalCoordinate(deviceStartPosition); + + /// Event start position in the local coordinate space of the current + /// component. + /// + /// This property is only accessible when the event is being propagated to + /// the components via [deliverAtPoint]. It is an error to try to read this + /// property at other times. + Vector2 get localStartPosition => renderingTrace.last.start; + + /// Event end position in the coordinate space of the device -- either the + /// phone, or the browser window, or the app. + /// + /// If the game runs in a full-screen mode, then this would be equal to the + /// [canvasEndPosition]. Otherwise, the [deviceEndPosition] is the + /// Flutter-level global position. + final Vector2 deviceEndPosition; + + /// Event end position in the coordinate space of the game widget, i.e. + /// relative to the game canvas. + /// + /// This could be considered the Flame-level global position. + late final Vector2 canvasEndPosition = + _game.convertGlobalToLocalCoordinate(deviceEndPosition); + + /// Event end position in the local coordinate space of the current + /// component. + /// + /// This property is only accessible when the event is being propagated to + /// the components via [deliverAtPoint]. It is an error to try to read this + /// property at other times. + Vector2 get localEndPosition => renderingTrace.last.end; + + /// Event delta in the coordinate space of the device -- either the + /// phone, or the browser window, or the app. + /// + /// If the game runs in a full-screen mode, then this would be equal to the + /// [canvasDelta]. Otherwise, the [deviceDelta] is the + /// Flutter-level global position. + late final Vector2 deviceDelta = deviceEndPosition - deviceStartPosition; + + /// Event delta in the coordinate space of the game widget, i.e. + /// relative to the game canvas. + /// + /// This could be considered the Flame-level global position. + late final Vector2 canvasDelta = canvasEndPosition - canvasStartPosition; + + /// Event delta in the local coordinate space of the current component. + /// + /// This property is only accessible when the event is being propagated to + /// the components via [deliverAtPoint]. It is an error to try to read this + /// property at other times. + Vector2 get localDelta => localEndPosition - localStartPosition; + + @override + Iterable collectApplicableChildren({ + required Component rootComponent, + }) { + return rootComponent.componentsAtLocation( + ( + start: canvasStartPosition, + end: canvasEndPosition, + ), + renderingTrace, + (transform, context) { + final start = context.start; + final transformedStart = transform.parentToLocal(start); + + final end = context.end; + final transformedEnd = transform.parentToLocal(end); + + if (transformedStart == null || transformedEnd == null) { + return null; + } + return ( + start: transformedStart, + end: transformedEnd, + ); + }, + // we only trigger the drag start if the component check passes, but + // as the user drags the cursor it could end up outside the component + // bounds; we don't want the event to be lost, so we bypass the check. + (component, context) => true, + ); + } +} diff --git a/packages/flame/lib/src/events/messages/drag_update_event.dart b/packages/flame/lib/src/events/messages/drag_update_event.dart index a8dcae3dd..c88b67867 100644 --- a/packages/flame/lib/src/events/messages/drag_update_event.dart +++ b/packages/flame/lib/src/events/messages/drag_update_event.dart @@ -1,29 +1,25 @@ import 'package:flame/extensions.dart'; -import 'package:flame/src/events/messages/position_event.dart'; +import 'package:flame/src/events/messages/displacement_event.dart'; import 'package:flutter/gestures.dart'; -class DragUpdateEvent extends PositionEvent { +class DragUpdateEvent extends DisplacementEvent { DragUpdateEvent(this.pointerId, super.game, DragUpdateDetails details) : timestamp = details.sourceTimeStamp ?? Duration.zero, - delta = details.delta.toVector2(), super( - devicePosition: details.globalPosition.toVector2(), + deviceStartPosition: details.globalPosition.toVector2(), + deviceEndPosition: + details.globalPosition.toVector2() + details.delta.toVector2(), ); final int pointerId; final Duration timestamp; - final Vector2 delta; - - static final _nanPoint = Vector2.all(double.nan); @override - Vector2 get localPosition { - return renderingTrace.isEmpty ? _nanPoint : renderingTrace.last; - } - - @override - String toString() => 'DragUpdateEvent(devicePosition: $devicePosition, ' - 'canvasPosition: $canvasPosition, ' - 'delta: $delta, ' - 'pointerId: $pointerId, timestamp: $timestamp)'; + String toString() => 'DragUpdateEvent(' + 'devicePosition: $deviceStartPosition, ' + 'canvasPosition: $canvasStartPosition, ' + 'delta: $localDelta, ' + 'pointerId: $pointerId, ' + 'timestamp: $timestamp' + ')'; } diff --git a/packages/flame/lib/src/events/messages/location_context_event.dart b/packages/flame/lib/src/events/messages/location_context_event.dart new file mode 100644 index 000000000..a273451fa --- /dev/null +++ b/packages/flame/lib/src/events/messages/location_context_event.dart @@ -0,0 +1,50 @@ +import 'package:flame/components.dart'; +import 'package:flame/src/events/messages/event.dart'; +import 'package:meta/meta.dart'; + +/// A base event that includes a location context, i.e. a position or set of +/// positions in which the event happens. +abstract class LocationContextEvent extends Event { + /// The stacktrace of coordinates of the event within the components in their + /// rendering order. + /// + /// This represents the stack of transformed versions of the same location + /// context -- which represents the event point -- but in the coordinate space + /// of each parent component until the root. + final List renderingTrace = []; + + /// The context in the parent's coordinate space, containing start and end + /// points. + C? get parentContext { + if (renderingTrace.length >= 2) { + final lastIndex = renderingTrace.length - 1; + return renderingTrace[lastIndex]; + } else { + return null; + } + } + + @internal + Iterable collectApplicableChildren({ + required Component rootComponent, + }); + + @internal + void deliverAtPoint({ + required Component rootComponent, + required void Function(T component) eventHandler, + bool deliverToAll = false, + }) { + final children = collectApplicableChildren( + rootComponent: rootComponent, + ); + for (final child in children.whereType()) { + continuePropagation = deliverToAll; + eventHandler(child); + if (!continuePropagation) { + CameraComponent.currentCameras.clear(); + break; + } + } + } +} diff --git a/packages/flame/lib/src/events/messages/position_event.dart b/packages/flame/lib/src/events/messages/position_event.dart index 2ed122d34..e3844ea81 100644 --- a/packages/flame/lib/src/events/messages/position_event.dart +++ b/packages/flame/lib/src/events/messages/position_event.dart @@ -1,24 +1,16 @@ import 'package:flame/components.dart'; -import 'package:flame/src/events/messages/event.dart'; +import 'package:flame/src/events/messages/location_context_event.dart'; import 'package:flame/src/game/game.dart'; -import 'package:meta/meta.dart'; /// Base class for events that originate at some point on the screen. These -/// include: tap events, drag events, scale events, etc. +/// include: tap events, scale events, etc. /// /// This class includes properties that describe the position where the event /// has occurred. -abstract class PositionEvent extends Event { +abstract class PositionEvent extends LocationContextEvent { PositionEvent(this._game, {required this.devicePosition}); final Game _game; - /// Event position in the coordinate space of the game widget, i.e. relative - /// to the game canvas. - /// - /// This could be considered the Flame-level global position. - late final Vector2 canvasPosition = - _game.convertGlobalToLocalCoordinate(devicePosition); - /// Event position in the coordinate space of the device -- either the phone, /// or the browser window, or the app. /// @@ -27,6 +19,13 @@ abstract class PositionEvent extends Event { /// global position. final Vector2 devicePosition; + /// Event position in the coordinate space of the game widget, i.e. relative + /// to the game canvas. + /// + /// This could be considered the Flame-level global position. + late final Vector2 canvasPosition = + _game.convertGlobalToLocalCoordinate(devicePosition); + /// Event position in the local coordinate space of the current component. /// /// This property is only accessible when the event is being propagated to @@ -34,35 +33,10 @@ abstract class PositionEvent extends Event { /// property at other times. Vector2 get localPosition => renderingTrace.last; - /// The stacktrace of coordinates of the event within the components in their - /// rendering order. - final List renderingTrace = []; - - Vector2? get parentPosition { - if (renderingTrace.length >= 2) { - return renderingTrace[renderingTrace.length - 2]; - } else { - return null; - } - } - - /// Sends the event to components of type that are currently rendered at - /// the [canvasPosition]. - @internal - void deliverAtPoint({ + @override + Iterable collectApplicableChildren({ required Component rootComponent, - required void Function(T component) eventHandler, - bool deliverToAll = false, }) { - for (final child in rootComponent - .componentsAtPoint(canvasPosition, renderingTrace) - .whereType()) { - continuePropagation = deliverToAll; - eventHandler(child); - if (!continuePropagation) { - CameraComponent.currentCameras.clear(); - break; - } - } + return rootComponent.componentsAtPoint(canvasPosition, renderingTrace); } } diff --git a/packages/flame/test/camera/viewports/circular_viewport_test.dart b/packages/flame/test/camera/viewports/circular_viewport_test.dart index e64d03222..c959a4a72 100644 --- a/packages/flame/test/camera/viewports/circular_viewport_test.dart +++ b/packages/flame/test/camera/viewports/circular_viewport_test.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:flame/camera.dart'; import 'package:flame/components.dart'; +import 'package:flame/game.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -112,42 +113,48 @@ void main() { size: Vector2(50, 50), ); - testWithFlameGame('hit testing', (game) async { - final world = _MyWorld(); - final viewport = CircularViewport.ellipse(80, 20) - ..position = Vector2(20, 30); - final camera = CameraComponent( - world: world, - viewport: viewport, - ); - game.addAll([world, camera]); - await game.ready(); + testWithGame( + 'hit testing', + () => FlameGame( + camera: CameraComponent( + viewport: CircularViewport.ellipse(80, 20) + ..position = Vector2(20, 30), + world: _MyWorld(), + ), + ), + (game) async { + await game.ready(); + final viewport = game.camera.viewport; - bool hit(double x, double y) { - final components = game.componentsAtPoint(Vector2(x, y)).toList(); - return components.first == viewport && components[1] == world; - } - - expect(hit(10, 20), false); - expect(hit(20, 30), false); - expect(hit(25, 35), false); - expect(hit(100, 35), true); - expect(hit(100, 50), true); - expect(hit(180, 50), true); - expect(hit(180, 49), false); - expect(hit(180, 51), false); - - final nestedPoints = []; - final center = Vector2(100, 50); - for (final component in game.componentsAtPoint(center, nestedPoints)) { - if (component == viewport) { - continue; + bool hit(double x, double y) { + final components = game.componentsAtPoint(Vector2(x, y)).toList(); + return components.first == viewport && components[1] == game.world; } - expect(component, world); - expect(nestedPoints.last, Vector2.zero()); - break; - } - }); + + expect(hit(10, 20), false); + expect(hit(20, 30), false); + expect(hit(25, 35), false); + expect(hit(100, 35), true); + expect(hit(100, 50), true); + expect(hit(180, 50), true); + expect(hit(180, 49), false); + expect(hit(180, 51), false); + + final nestedPoints = []; + final center = Vector2(100, 50); + for (final component in game.componentsAtPoint( + center, + nestedPoints, + )) { + if (component == viewport) { + continue; + } + expect(component, game.world); + expect(nestedPoints.last, Vector2.zero()); + break; + } + }, + ); test('set wrong size', () { expect( diff --git a/packages/flame/test/components/route_test.dart b/packages/flame/test/components/route_test.dart index f6096bcf6..8d9bfe019 100644 --- a/packages/flame/test/components/route_test.dart +++ b/packages/flame/test/components/route_test.dart @@ -443,34 +443,37 @@ void main() { ); }); - testWithFlameGame('componentsAtPoint for transparent route', (game) async { - final initialComponent = PositionComponent(size: Vector2.all(100)); - final newComponent = PositionComponent(size: Vector2.all(100)); - final router = RouterComponent( - initialRoute: 'initial', - routes: { - 'initial': Route( - () => initialComponent, - ), - 'new': Route( - () => newComponent, - transparent: true, - ), - }, - )..addToParent(game); - await game.ready(); + testWithFlameGame( + 'componentsAtPoint for transparent route', + (game) async { + final initialComponent = PositionComponent(size: Vector2.all(100)); + final newComponent = PositionComponent(size: Vector2.all(100)); + final router = RouterComponent( + initialRoute: 'initial', + routes: { + 'initial': Route( + () => initialComponent, + ), + 'new': Route( + () => newComponent, + transparent: true, + ), + }, + )..addToParent(game); + await game.ready(); - router.pushNamed('new'); - await game.ready(); - expect( - game.componentsAtPoint(Vector2(50, 50)).contains(newComponent), - isTrue, - ); - expect( - game.componentsAtPoint(Vector2(50, 50)).contains(initialComponent), - isTrue, - ); - }); + router.pushNamed('new'); + await game.ready(); + expect( + game.componentsAtPoint(Vector2(50, 50)).contains(newComponent), + isTrue, + ); + expect( + game.componentsAtPoint(Vector2(50, 50)).contains(initialComponent), + isTrue, + ); + }, + ); }); } diff --git a/packages/flame/test/events/component_mixins/drag_callbacks_test.dart b/packages/flame/test/events/component_mixins/drag_callbacks_test.dart index afe3ad06c..666fb5a01 100644 --- a/packages/flame/test/events/component_mixins/drag_callbacks_test.dart +++ b/packages/flame/test/events/component_mixins/drag_callbacks_test.dart @@ -3,7 +3,7 @@ import 'package:flame/events.dart'; import 'package:flame/game.dart'; import 'package:flame/src/events/flame_game_mixins/multi_drag_dispatcher.dart'; import 'package:flame_test/flame_test.dart'; -import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { @@ -211,7 +211,7 @@ void main() { expect(game.children.elementAt(1), isA<_DragWithCallbacksComponent>()); expect(game.children.elementAt(2), isA()); -// regular drag + // regular drag await tester.timedDragFrom( const Offset(50, 50), const Offset(20, 0), @@ -221,7 +221,7 @@ void main() { expect(nDragUpdateCalled, 8); expect(nDragEndCalled, 1); -// cancelled drag + // cancelled drag final gesture = await tester.startGesture(const Offset(50, 50)); await gesture.moveBy(const Offset(10, 10)); await gesture.cancel(); @@ -262,7 +262,7 @@ void main() { ); testWidgets( - 'drag event can move outside the component bounds', + 'drag event can move outside the component bounds and still fire', (tester) async { final points = []; final game = FlameGame( @@ -270,7 +270,7 @@ void main() { _DragWithCallbacksComponent( size: Vector2.all(95), position: Vector2.all(5), - onDragUpdate: (e) => points.add(e.localPosition), + onDragUpdate: (e) => points.add(e.localStartPosition), ), ], ); @@ -289,16 +289,110 @@ void main() { expect(points.length, 42); expect(points.first, Vector2(75, 75)); expect( - points.skip(1).take(20), - List.generate(20, (i) => Vector2(75.0, 75.0 + i)), - ); - expect( - points.skip(21), - everyElement(predicate((Vector2 v) => v.isNaN)), + points.skip(1), + List.generate(41, (i) => Vector2(75.0, 75.0 + i)), ); }, ); }); + + testWidgets( + 'drag event delta respects camera & zoom', + (tester) async { + // canvas size is 800x600 so this means a 10x logical scale across + // both dimensions + final resolution = Vector2(80, 60); + final game = FlameGame( + camera: CameraComponent.withFixedResolution( + width: resolution.x, + height: resolution.y, + ), + ); + + game.camera.viewfinder.zoom = 2; + + final deltas = []; + await game.world.add( + _DragWithCallbacksComponent( + position: Vector2.all(-5), + size: Vector2.all(10), + onDragUpdate: (event) => deltas.add(event.localDelta), + ), + ); + await tester.pumpWidget(GameWidget(game: game)); + await tester.pump(); + await tester.pump(); + + final canvasSize = game.canvasSize; + await tester.dragFrom( + (canvasSize / 2).toOffset(), + Offset(canvasSize.x / 10, 0), + ); + final totalDelta = deltas.reduce((a, b) => a + b); + expect(totalDelta, Vector2(4, 0)); + }, + ); + + testWidgets( + 'drag event delta respects widget positioning', + (tester) async { + // canvas size is 800x600 so this means a 10x logical scale across + // both dimensions + final resolution = Vector2(80, 60); + final game = FlameGame( + camera: CameraComponent.withFixedResolution( + width: resolution.x, + height: resolution.y, + ), + ); + + game.camera.viewfinder.zoom = 1 / 2; + + final deltas = []; + await game.world.add( + _DragWithCallbacksComponent( + position: Vector2.all(-5), + size: Vector2.all(10), + onDragUpdate: (event) => deltas.add(event.localDelta), + ), + ); + await tester.pumpWidget( + MaterialApp( + home: Stack( + children: [ + Positioned( + left: 100.0, + top: 200.0, + width: 800, + height: 600, + child: GameWidget(game: game), + ), + ], + ), + ), + ); + await tester.pump(); + await tester.pump(); + + final canvasSize = game.canvasSize; + + // no offset + await tester.dragFrom( + (canvasSize / 2).toOffset(), + Offset(canvasSize.x / 10, 0), + ); + expect(deltas, isEmpty); + + // accounting for offset + await tester.dragFrom( + (canvasSize / 2 + Vector2(100, 200)).toOffset(), + Offset(canvasSize.x / 10, 0), + ); + expect(deltas, isNotEmpty); + final totalDelta = deltas.reduce((a, b) => a + b); + expect(totalDelta, Vector2(16, 0)); + }, + ); } mixin _DragCounter on DragCallbacks {