From 6b2f8032feb00f82dd5d87b254f472450abc8438 Mon Sep 17 00:00:00 2001 From: Erick Date: Thu, 15 Apr 2021 15:28:46 -0300 Subject: [PATCH] Making Flame events aware of camera (#755) * some initial refactoring * Fixing examples and joystick component * Fixing examples * Addressing comments from PR * Fixing tutorials * Updating docs * Fixing tests * PR followup * A big follow up * linting * doc nit * Changelog and fix tutorial * Addressing comments * Fixing BaseGame project and scale offset methods * Formatting * doc suggestions * Update packages/flame/lib/src/gestures/events.dart Co-authored-by: Luan Nico * Hopefully, the last follow up * Linting and adding dart-code-metrics again * fixing tutorial Co-authored-by: Luan Nico --- doc/input.md | 52 +- examples/lib/stories/animations/basic.dart | 5 +- .../stories/collision_detection/circles.dart | 4 +- .../collision_detection/multiple_shapes.dart | 7 +- examples/lib/stories/controls/draggables.dart | 11 +- .../lib/stories/controls/mouse_movement.dart | 5 +- examples/lib/stories/controls/multitap.dart | 5 +- .../stories/controls/multitap_advanced.dart | 11 +- .../controls/overlapping_tapables.dart | 4 +- examples/lib/stories/controls/scroll.dart | 5 +- examples/lib/stories/controls/tapables.dart | 4 +- .../lib/stories/effects/combined_effect.dart | 4 +- .../lib/stories/effects/infinite_effect.dart | 4 +- examples/lib/stories/effects/move_effect.dart | 4 +- .../lib/stories/effects/sequence_effect.dart | 4 +- .../stories/tile_maps/isometric_tile_map.dart | 5 +- .../stories/utils/camera_and_viewport.dart | 32 +- packages/flame/CHANGELOG.md | 1 + packages/flame/example/lib/main.dart | 7 +- packages/flame/lib/game.dart | 2 +- packages/flame/lib/gestures.dart | 3 +- .../components/joystick/joystick_action.dart | 12 +- .../joystick/joystick_directional.dart | 14 +- .../lib/src/components/mixins/draggable.dart | 14 +- .../lib/src/components/mixins/tapable.dart | 28 +- packages/flame/lib/src/game/base_game.dart | 12 + packages/flame/lib/src/game/game.dart | 14 + packages/flame/lib/src/game/game_widget.dart | 548 ------------------ .../lib/src/game/game_widget/game_widget.dart | 301 ++++++++++ .../lib/src/game/game_widget/gestures.dart | 294 ++++++++++ packages/flame/lib/src/gestures.dart | 90 --- .../flame/lib/src/gestures/detectors.dart | 89 +++ packages/flame/lib/src/gestures/events.dart | 185 ++++++ .../components/composed_component_test.dart | 14 +- packages/flame/test/game/base_game_test.dart | 6 +- .../flame/test/util/mock_gesture_events.dart | 18 + .../2_sprite_animations_gestures/README.md | 6 +- .../code/lib/main.dart | 6 +- 38 files changed, 1073 insertions(+), 757 deletions(-) delete mode 100644 packages/flame/lib/src/game/game_widget.dart create mode 100644 packages/flame/lib/src/game/game_widget/game_widget.dart create mode 100644 packages/flame/lib/src/game/game_widget/gestures.dart delete mode 100644 packages/flame/lib/src/gestures.dart create mode 100644 packages/flame/lib/src/gestures/detectors.dart create mode 100644 packages/flame/lib/src/gestures/events.dart create mode 100644 packages/flame/test/util/mock_gesture_events.dart diff --git a/doc/input.md b/doc/input.md index b2ed30c17..2b3ac1ebd 100644 --- a/doc/input.md +++ b/doc/input.md @@ -95,6 +95,28 @@ and [MouseRegion widget](https://api.flutter.dev/flutter/widgets/MouseRegion-cla also read more about Flutter's gestures [here](https://api.flutter.dev/flutter/gestures/gestures-library.html). +## Event coordinate system + +On events that have positions, like for example `Tap*` or `Drag`, you will notice that the `eventPosition` +attribute includes 3 fields: `game`, `widget` and `global`. Below you will find a brief explanation +about each one of them. + +### global + +The position where the event occurred considering the entire screen, same as +`globalPosition` in Flutter's native events. + +### widget + +The position where the event occurred relative to the `GameWidget` position and size +, same as `localPosition` in Flutter's native events. + +### game + +The position where the event ocurred relative to the `GameWidget` and with any +transformations that the game applied to the game (e.g. camera). If the game doesn't have any +transformations, this will be equal to the `widget` attribute. + ## Example ```dart @@ -102,13 +124,13 @@ class MyGame extends Game with TapDetector { // Other methods omitted @override - void onTapDown(TapDownDetails details) { - print("Player tap down on ${details.globalPosition.dx} - ${details.globalPosition.dy}"); + void onTapDown(TapDownInfo event) { + print("Player tap down on ${event.eventPosition.game}"); } @override - void onTapUp(TapUpDetails details) { - print("Player tap up on ${details.globalPosition.dx} - ${details.globalPosition.dy}"); + void onTapUp(TapUpInfo event) { + print("Player tap up on ${event.eventPosition.game}"); } } ``` @@ -137,8 +159,8 @@ components, you can override the following methods on your components: ```dart void onTapCancel() {} -void onTapDown(TapDownDetails details) {} -void onTapUp(TapUpDetails details) {} +void onTapDown(TapDownInfo event) {} +void onTapUp(TapUpInfo event) {} ``` Minimal component example: @@ -152,12 +174,12 @@ class TapableComponent extends PositionComponent with Tapable { // update and render omitted @override - void onTapUp(TapUpDetails details) { + void onTapUp(TapUpInfo event) { print("tap up"); } @override - void onTapDown(TapDownDetails details) { + void onTapDown(TapDownInfo event) { print("tap down"); } @@ -187,8 +209,8 @@ components. ```dart void onDragStart(int pointerId, Vector2 startPosition) {} - void onDragUpdate(int pointerId, DragUpdateDetails details) {} - void onDragEnd(int pointerId, DragEndDetails details) {} + void onDragUpdate(int pointerId, DragUpdateInfo event) {} + void onDragEnd(int pointerId, DragEndInfo event) {} void onDragCancel(int pointerId) {} ``` @@ -224,16 +246,14 @@ class DraggableComponent extends PositionComponent with Draggable { } @override - bool onDragUpdate(int pointerId, DragUpdateDetails details) { - final localCoords = gameRef.convertGlobalToLocalCoordinate( - details.globalPosition.toVector2(), - ); + bool onDragUpdate(int pointerId, DragUpdateInfo event) { + final localCoords = event.eventPosition.game; position = localCoords - dragDeltaPosition; return false; } @override - bool onDragEnd(int pointerId, DragEndDetails details) { + bool onDragEnd(int pointerId, DragEndInfo event) { dragDeltaPosition = null; return false; } @@ -433,4 +453,4 @@ You can also check a more complete example ## Gamepad Flame has a separate plugin for gamepad support, you can checkout the plugin -[here](https://github.com/flame-engine/flame_gamepad) for more information. \ No newline at end of file +[here](https://github.com/flame-engine/flame_gamepad) for more information. diff --git a/examples/lib/stories/animations/basic.dart b/examples/lib/stories/animations/basic.dart index 5549ff76b..d9467c76d 100644 --- a/examples/lib/stories/animations/basic.dart +++ b/examples/lib/stories/animations/basic.dart @@ -3,7 +3,6 @@ import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; import 'package:flame/gestures.dart'; -import 'package:flutter/gestures.dart' show TapDownDetails; class BasicAnimations extends BaseGame with TapDetector { late Image chopper; @@ -64,7 +63,7 @@ class BasicAnimations extends BaseGame with TapDetector { } @override - void onTapDown(TapDownDetails evt) { - addAnimation(Vector2(evt.globalPosition.dx, evt.globalPosition.dy)); + void onTapDown(TapDownInfo event) { + addAnimation(event.eventPosition.game); } } diff --git a/examples/lib/stories/collision_detection/circles.dart b/examples/lib/stories/collision_detection/circles.dart index fc9ab9e9d..603cc2e75 100644 --- a/examples/lib/stories/collision_detection/circles.dart +++ b/examples/lib/stories/collision_detection/circles.dart @@ -65,7 +65,7 @@ class Circles extends BaseGame with HasCollidables, TapDetector { } @override - void onTapDown(TapDownDetails details) { - add(MyCollidable(details.localPosition.toVector2())); + void onTapDown(TapDownInfo event) { + add(MyCollidable(event.eventPosition.game)); } } diff --git a/examples/lib/stories/collision_detection/multiple_shapes.dart b/examples/lib/stories/collision_detection/multiple_shapes.dart index abbdc91db..f26023cdf 100644 --- a/examples/lib/stories/collision_detection/multiple_shapes.dart +++ b/examples/lib/stories/collision_detection/multiple_shapes.dart @@ -5,6 +5,7 @@ import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame/geometry.dart'; +import 'package:flame/gestures.dart'; import 'package:flame/palette.dart'; import 'package:flutter/material.dart' hide Image, Draggable; @@ -81,14 +82,14 @@ abstract class MyCollidable extends PositionComponent } @override - bool onDragUpdate(int pointerId, DragUpdateDetails details) { + bool onDragUpdate(int pointerId, _) { _isDragged = true; return true; } @override - bool onDragEnd(int pointerId, DragEndDetails details) { - velocity.setFrom(details.velocity.pixelsPerSecond.toVector2() / 10); + bool onDragEnd(int pointerId, DragEndInfo event) { + velocity.setFrom(event.velocity / 10); _isDragged = false; return true; } diff --git a/examples/lib/stories/controls/draggables.dart b/examples/lib/stories/controls/draggables.dart index d029b387a..d6926f0e2 100644 --- a/examples/lib/stories/controls/draggables.dart +++ b/examples/lib/stories/controls/draggables.dart @@ -1,7 +1,7 @@ import 'package:flame/components.dart'; import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; -import 'package:flutter/gestures.dart'; +import 'package:flame/gestures.dart'; import 'package:flutter/material.dart' show Colors; // Note: this component does not consider the possibility of multiple @@ -33,21 +33,18 @@ class DraggableSquare extends PositionComponent } @override - bool onDragUpdate(int pointerId, DragUpdateDetails details) { + bool onDragUpdate(int pointerId, DragUpdateInfo event) { final dragDeltaPosition = this.dragDeltaPosition; if (dragDeltaPosition == null) { return false; } - final localCoords = gameRef.convertGlobalToLocalCoordinate( - details.globalPosition.toVector2(), - ); - position.setFrom(localCoords - dragDeltaPosition); + position.setFrom(event.eventPosition.game - dragDeltaPosition); return false; } @override - bool onDragEnd(int pointerId, DragEndDetails details) { + bool onDragEnd(int pointerId, _) { dragDeltaPosition = null; return false; } diff --git a/examples/lib/stories/controls/mouse_movement.dart b/examples/lib/stories/controls/mouse_movement.dart index db9337ae8..401adbd77 100644 --- a/examples/lib/stories/controls/mouse_movement.dart +++ b/examples/lib/stories/controls/mouse_movement.dart @@ -2,7 +2,6 @@ import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame/gestures.dart'; import 'package:flame/palette.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; class MouseMovementGame extends BaseGame with MouseMovementDetector { @@ -17,8 +16,8 @@ class MouseMovementGame extends BaseGame with MouseMovementDetector { bool onTarget = false; @override - void onMouseMove(PointerHoverEvent event) { - target = event.localPosition.toVector2(); + void onMouseMove(PointerHoverInfo event) { + target = event.eventPosition.game; } Rect _toRect() => position.toPositionedRect(objSize); diff --git a/examples/lib/stories/controls/multitap.dart b/examples/lib/stories/controls/multitap.dart index 0228d4483..eb2423623 100644 --- a/examples/lib/stories/controls/multitap.dart +++ b/examples/lib/stories/controls/multitap.dart @@ -12,9 +12,8 @@ class MultitapGame extends BaseGame with MultiTouchTapDetector { final Map taps = {}; @override - void onTapDown(int pointerId, TapDownDetails details) { - taps[pointerId] = - details.globalPosition.toVector2().toPositionedRect(tapSize); + void onTapDown(int pointerId, TapDownInfo event) { + taps[pointerId] = event.eventPosition.game.toPositionedRect(tapSize); } @override diff --git a/examples/lib/stories/controls/multitap_advanced.dart b/examples/lib/stories/controls/multitap_advanced.dart index c60173cd2..bbe2fd6c4 100644 --- a/examples/lib/stories/controls/multitap_advanced.dart +++ b/examples/lib/stories/controls/multitap_advanced.dart @@ -17,9 +17,8 @@ class MultitapAdvancedGame extends BaseGame Rect? panRect; @override - void onTapDown(int pointerId, TapDownDetails details) { - taps[pointerId] = - details.globalPosition.toVector2().toPositionedRect(tapSize); + void onTapDown(int pointerId, TapDownInfo event) { + taps[pointerId] = event.eventPosition.game.toPositionedRect(tapSize); } @override @@ -46,12 +45,12 @@ class MultitapAdvancedGame extends BaseGame } @override - void onDragUpdate(int pointerId, DragUpdateDetails details) { - end = details.localPosition.toVector2(); + void onDragUpdate(int pointerId, DragUpdateInfo event) { + end = event.eventPosition.game; } @override - void onDragEnd(int pointerId, DragEndDetails details) { + void onDragEnd(int pointerId, _) { final start = this.start, end = this.end; if (start != null && end != null) { panRect = start.toPositionedRect(end - start); diff --git a/examples/lib/stories/controls/overlapping_tapables.dart b/examples/lib/stories/controls/overlapping_tapables.dart index 3cd1ddcf1..ffea51edd 100644 --- a/examples/lib/stories/controls/overlapping_tapables.dart +++ b/examples/lib/stories/controls/overlapping_tapables.dart @@ -33,12 +33,12 @@ class TapableSquare extends PositionComponent with Tapable { } @override - bool onTapUp(TapUpDetails details) { + bool onTapUp(_) { return false; } @override - bool onTapDown(TapDownDetails details) { + bool onTapDown(_) { angle += 1.0; return false; } diff --git a/examples/lib/stories/controls/scroll.dart b/examples/lib/stories/controls/scroll.dart index fe3dc994a..d8e2c5d31 100644 --- a/examples/lib/stories/controls/scroll.dart +++ b/examples/lib/stories/controls/scroll.dart @@ -1,4 +1,3 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flame/game.dart'; import 'package:flame/gestures.dart'; @@ -14,8 +13,8 @@ class ScrollGame extends BaseGame with ScrollDetector { Vector2? target; @override - void onScroll(PointerScrollEvent event) { - target = position + event.scrollDelta.toVector2() * 5; + void onScroll(PointerScrollInfo event) { + target = position + event.scrollDelta * 5; } @override diff --git a/examples/lib/stories/controls/tapables.dart b/examples/lib/stories/controls/tapables.dart index a4a5e9948..821313da4 100644 --- a/examples/lib/stories/controls/tapables.dart +++ b/examples/lib/stories/controls/tapables.dart @@ -22,13 +22,13 @@ class TapableSquare extends PositionComponent with Tapable { } @override - bool onTapUp(TapUpDetails details) { + bool onTapUp(_) { _beenPressed = false; return true; } @override - bool onTapDown(TapDownDetails details) { + bool onTapDown(_) { _beenPressed = true; angle += 1.0; return true; diff --git a/examples/lib/stories/effects/combined_effect.dart b/examples/lib/stories/effects/combined_effect.dart index 2e9501fba..82fe90066 100644 --- a/examples/lib/stories/effects/combined_effect.dart +++ b/examples/lib/stories/effects/combined_effect.dart @@ -27,9 +27,9 @@ class CombinedEffectGame extends BaseGame with TapDetector { } @override - void onTapUp(TapUpDetails details) { + void onTapUp(TapUpInfo event) { greenSquare.clearEffects(); - final currentTap = details.localPosition.toVector2(); + final currentTap = event.eventPosition.game; final move = MoveEffect( path: [ diff --git a/examples/lib/stories/effects/infinite_effect.dart b/examples/lib/stories/effects/infinite_effect.dart index adcca9093..258c29034 100644 --- a/examples/lib/stories/effects/infinite_effect.dart +++ b/examples/lib/stories/effects/infinite_effect.dart @@ -31,8 +31,8 @@ class InfiniteEffectGame extends BaseGame with TapDetector { } @override - void onTapUp(TapUpDetails details) { - final p = details.localPosition.toVector2(); + void onTapUp(TapUpInfo event) { + final p = event.eventPosition.game; greenSquare.clearEffects(); redSquare.clearEffects(); diff --git a/examples/lib/stories/effects/move_effect.dart b/examples/lib/stories/effects/move_effect.dart index 6927de779..963c40925 100644 --- a/examples/lib/stories/effects/move_effect.dart +++ b/examples/lib/stories/effects/move_effect.dart @@ -16,11 +16,11 @@ class MoveEffectGame extends BaseGame with TapDetector { } @override - void onTapUp(TapUpDetails details) { + void onTapUp(TapUpInfo event) { square.addEffect( MoveEffect( path: [ - details.localPosition.toVector2(), + event.eventPosition.game, Vector2(100, 100), Vector2(50, 120), Vector2(200, 400), diff --git a/examples/lib/stories/effects/sequence_effect.dart b/examples/lib/stories/effects/sequence_effect.dart index 24ab785be..8b510eabf 100644 --- a/examples/lib/stories/effects/sequence_effect.dart +++ b/examples/lib/stories/effects/sequence_effect.dart @@ -20,8 +20,8 @@ class SequenceEffectGame extends BaseGame with TapDetector { } @override - void onTapUp(TapUpDetails details) { - final currentTap = details.localPosition.toVector2(); + void onTapUp(TapUpInfo event) { + final currentTap = event.eventPosition.game; greenSquare.clearEffects(); final move1 = MoveEffect( diff --git a/examples/lib/stories/tile_maps/isometric_tile_map.dart b/examples/lib/stories/tile_maps/isometric_tile_map.dart index 4906ac3c8..e6562467f 100644 --- a/examples/lib/stories/tile_maps/isometric_tile_map.dart +++ b/examples/lib/stories/tile_maps/isometric_tile_map.dart @@ -5,7 +5,6 @@ import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame/gestures.dart'; import 'package:flame/sprite.dart'; -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide Image; const x = 500.0; @@ -72,8 +71,8 @@ class IsometricTileMapGame extends BaseGame with MouseMovementDetector { } @override - void onMouseMove(PointerHoverEvent event) { - final screenPosition = event.localPosition.toVector2(); + void onMouseMove(PointerHoverInfo event) { + final screenPosition = event.eventPosition.game; final block = base.getBlock(screenPosition); selector.show = base.containsBlock(block); selector.position.setFrom(base.getBlockPosition(block) + topLeft); diff --git a/examples/lib/stories/utils/camera_and_viewport.dart b/examples/lib/stories/utils/camera_and_viewport.dart index 18b549a63..811d8e1ba 100644 --- a/examples/lib/stories/utils/camera_and_viewport.dart +++ b/examples/lib/stories/utils/camera_and_viewport.dart @@ -81,25 +81,49 @@ class Map extends Component { } } -class Rock extends SquareComponent with Hitbox, Collidable { +class Rock extends SquareComponent with Hitbox, Collidable, Tapable { + static final unpressedPaint = Paint()..color = const Color(0xFF2222FF); + static final pressedPaint = Paint()..color = const Color(0xFF414175); + Rock(Vector2 position) { this.position.setFrom(position); size.setValues(50, 50); - paint = Paint()..color = const Color(0xFF2222FF); + paint = unpressedPaint; addShape(HitboxRectangle()); } + @override + bool onTapDown(_) { + paint = pressedPaint; + return true; + } + + @override + bool onTapUp(_) { + paint = unpressedPaint; + return true; + } + + @override + bool onTapCancel() { + paint = unpressedPaint; + return true; + } + @override int get priority => 2; } class CameraAndViewportGame extends BaseGame - with KeyboardEvents, HasCollidables { + with KeyboardEvents, HasCollidables, HasTapableComponents { late MovableSquare square; @override Future onLoad() async { - viewport = FixedResolutionViewport(Vector2(500, 500)); + // TODO(erick) viewport is not considered by the camera when projecting + // coordinates, so this makes the clicks not work. commenting this while + // we don't fix that + //viewport = FixedResolutionViewport(Vector2(500, 500)); add(Map()); add(square = MovableSquare()); diff --git a/packages/flame/CHANGELOG.md b/packages/flame/CHANGELOG.md index d399e23a8..9d7a69303 100644 --- a/packages/flame/CHANGELOG.md +++ b/packages/flame/CHANGELOG.md @@ -3,6 +3,7 @@ ## [next] - Updated tutorial documentation to indicate use of new version - Fix bounding box check in collision detection + - Refactor on flame input system to correctly take camera into account ## [1.0.0-rc9] - Fix input bug with other anchors than center diff --git a/packages/flame/example/lib/main.dart b/packages/flame/example/lib/main.dart index f7c3284da..95c02fa8e 100644 --- a/packages/flame/example/lib/main.dart +++ b/packages/flame/example/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:math' as math; import 'dart:ui'; import 'package:flame/components.dart'; +import 'package:flame/extensions.dart'; import 'package:flame/game.dart'; import 'package:flame/gestures.dart'; import 'package:flame/palette.dart'; @@ -60,9 +61,9 @@ class MyGame extends BaseGame with DoubleTapDetector, TapDetector { } @override - void onTapUp(TapUpDetails details) { - final touchArea = Rect.fromCenter( - center: details.localPosition, + void onTapUp(TapUpInfo event) { + final touchArea = RectExtension.fromVector2Center( + center: event.eventPosition.game, width: 20, height: 20, ); diff --git a/packages/flame/lib/game.dart b/packages/flame/lib/game.dart index 4868c530b..a4e61c7a0 100644 --- a/packages/flame/lib/game.dart +++ b/packages/flame/lib/game.dart @@ -3,6 +3,6 @@ export 'src/fps_counter.dart'; export 'src/game/base_game.dart'; export 'src/game/camera.dart'; export 'src/game/game.dart'; -export 'src/game/game_widget.dart'; +export 'src/game/game_widget/game_widget.dart'; export 'src/game/viewport.dart'; export 'src/text_config.dart'; diff --git a/packages/flame/lib/gestures.dart b/packages/flame/lib/gestures.dart index ddd3c75c3..c7a15ea6e 100644 --- a/packages/flame/lib/gestures.dart +++ b/packages/flame/lib/gestures.dart @@ -1 +1,2 @@ -export 'src/gestures.dart'; +export 'src/gestures/detectors.dart'; +export 'src/gestures/events.dart'; diff --git a/packages/flame/lib/src/components/joystick/joystick_action.dart b/packages/flame/lib/src/components/joystick/joystick_action.dart index 860149b1c..38a5c139d 100644 --- a/packages/flame/lib/src/components/joystick/joystick_action.dart +++ b/packages/flame/lib/src/components/joystick/joystick_action.dart @@ -2,14 +2,14 @@ import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart' show Colors; -import 'package:flutter/widgets.dart' - show EdgeInsets, DragUpdateDetails, DragEndDetails; +import 'package:flutter/widgets.dart' show EdgeInsets; import '../../../components.dart'; import '../../../game.dart'; import '../../extensions/offset.dart'; import '../../extensions/rect.dart'; import '../../extensions/vector2.dart'; +import '../../gestures/events.dart'; import 'joystick_component.dart'; import 'joystick_element.dart'; import 'joystick_events.dart'; @@ -186,18 +186,16 @@ class JoystickAction extends BaseComponent with Draggable, HasGameRef { } @override - bool onDragUpdate(int pointerId, DragUpdateDetails details) { + bool onDragUpdate(int pointerId, DragUpdateInfo event) { if (_dragging) { - _dragPosition = gameRef.convertGlobalToLocalCoordinate( - details.globalPosition.toVector2(), - ); + _dragPosition = event.eventPosition.game; return true; } return false; } @override - bool onDragEnd(int pointerId, DragEndDetails p1) { + bool onDragEnd(_, __) { return _finishDrag(ActionEvent.up); } diff --git a/packages/flame/lib/src/components/joystick/joystick_directional.dart b/packages/flame/lib/src/components/joystick/joystick_directional.dart index bdfceb23e..437535af1 100644 --- a/packages/flame/lib/src/components/joystick/joystick_directional.dart +++ b/packages/flame/lib/src/components/joystick/joystick_directional.dart @@ -2,11 +2,11 @@ import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart' show Colors; -import 'package:flutter/widgets.dart' - show EdgeInsets, DragUpdateDetails, DragEndDetails; +import 'package:flutter/widgets.dart' show EdgeInsets; import '../../../components.dart'; import '../../../extensions.dart'; +import '../../gestures/events.dart'; import 'joystick_component.dart'; import 'joystick_element.dart'; import 'joystick_events.dart'; @@ -161,18 +161,16 @@ class JoystickDirectional extends BaseComponent with Draggable, HasGameRef { } @override - bool onDragUpdate(int pointerId, DragUpdateDetails details) { + bool onDragUpdate(_, DragUpdateInfo event) { if (_dragging) { - _dragPosition = gameRef.convertGlobalToLocalCoordinate( - details.globalPosition.toVector2(), - ); + _dragPosition = event.eventPosition.game; return false; } return true; } @override - bool onDragEnd(int pointerId, DragEndDetails details) { + bool onDragEnd(_, __) { _dragging = false; _dragPosition = background.center; joystickController.joystickChangeDirectional(JoystickDirectionalEvent( @@ -182,7 +180,7 @@ class JoystickDirectional extends BaseComponent with Draggable, HasGameRef { } @override - bool onDragCancel(int pointerId) { + bool onDragCancel(_) { _dragging = false; _dragPosition = background.center; joystickController.joystickChangeDirectional(JoystickDirectionalEvent( diff --git a/packages/flame/lib/src/components/mixins/draggable.dart b/packages/flame/lib/src/components/mixins/draggable.dart index 577c0ca19..c32fb6f32 100644 --- a/packages/flame/lib/src/components/mixins/draggable.dart +++ b/packages/flame/lib/src/components/mixins/draggable.dart @@ -1,8 +1,8 @@ -import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import '../../../extensions.dart'; import '../../game/base_game.dart'; +import '../../gestures/events.dart'; import '../base_component.dart'; mixin Draggable on BaseComponent { @@ -10,11 +10,11 @@ mixin Draggable on BaseComponent { return true; } - bool onDragUpdate(int pointerId, DragUpdateDetails details) { + bool onDragUpdate(int pointerId, DragUpdateInfo details) { return true; } - bool onDragEnd(int pointerId, DragEndDetails details) { + bool onDragEnd(int pointerId, DragEndInfo details) { return true; } @@ -33,14 +33,14 @@ mixin Draggable on BaseComponent { return true; } - bool handleDragUpdated(int pointerId, DragUpdateDetails details) { + bool handleDragUpdated(int pointerId, DragUpdateInfo details) { if (_checkPointerId(pointerId)) { return onDragUpdate(pointerId, details); } return true; } - bool handleDragEnded(int pointerId, DragEndDetails details) { + bool handleDragEnded(int pointerId, DragEndInfo details) { if (_checkPointerId(pointerId)) { _currentPointerIds.remove(pointerId); return onDragEnd(pointerId, details); @@ -64,12 +64,12 @@ mixin HasDraggableComponents on BaseGame { } @mustCallSuper - void onDragUpdate(int pointerId, DragUpdateDetails details) { + void onDragUpdate(int pointerId, DragUpdateInfo details) { _onGenericEventReceived((c) => c.handleDragUpdated(pointerId, details)); } @mustCallSuper - void onDragEnd(int pointerId, DragEndDetails details) { + void onDragEnd(int pointerId, DragEndInfo details) { _onGenericEventReceived((c) => c.handleDragEnded(pointerId, details)); } diff --git a/packages/flame/lib/src/components/mixins/tapable.dart b/packages/flame/lib/src/components/mixins/tapable.dart index 6260a24cd..880fe164e 100644 --- a/packages/flame/lib/src/components/mixins/tapable.dart +++ b/packages/flame/lib/src/components/mixins/tapable.dart @@ -1,9 +1,8 @@ -import 'package:flutter/gestures.dart'; import 'package:meta/meta.dart'; import '../../../game.dart'; -import '../../extensions/offset.dart'; import '../../game/base_game.dart'; +import '../../gestures/events.dart'; import '../base_component.dart'; mixin Tapable on BaseComponent { @@ -11,11 +10,11 @@ mixin Tapable on BaseComponent { return true; } - bool onTapDown(TapDownDetails details) { + bool onTapDown(TapDownInfo event) { return true; } - bool onTapUp(TapUpDetails details) { + bool onTapUp(TapUpInfo event) { return true; } @@ -23,19 +22,18 @@ mixin Tapable on BaseComponent { bool _checkPointerId(int pointerId) => _currentPointerId == pointerId; - bool handleTapDown(int pointerId, TapDownDetails details) { - if (containsPoint(details.localPosition.toVector2())) { + bool handleTapDown(int pointerId, TapDownInfo event) { + if (containsPoint(event.eventPosition.game)) { _currentPointerId = pointerId; - return onTapDown(details); + return onTapDown(event); } return true; } - bool handleTapUp(int pointerId, TapUpDetails details) { - if (_checkPointerId(pointerId) && - containsPoint(details.localPosition.toVector2())) { + bool handleTapUp(int pointerId, TapUpInfo event) { + if (_checkPointerId(pointerId) && containsPoint(event.eventPosition.game)) { _currentPointerId = null; - return onTapUp(details); + return onTapUp(event); } return true; } @@ -71,12 +69,12 @@ mixin HasTapableComponents on BaseGame { } @mustCallSuper - void onTapDown(int pointerId, TapDownDetails details) { - _handleTapEvent((Tapable child) => child.handleTapDown(pointerId, details)); + void onTapDown(int pointerId, TapDownInfo event) { + _handleTapEvent((Tapable child) => child.handleTapDown(pointerId, event)); } @mustCallSuper - void onTapUp(int pointerId, TapUpDetails details) { - _handleTapEvent((Tapable child) => child.handleTapUp(pointerId, details)); + void onTapUp(int pointerId, TapUpInfo event) { + _handleTapEvent((Tapable child) => child.handleTapUp(pointerId, event)); } } diff --git a/packages/flame/lib/src/game/base_game.dart b/packages/flame/lib/src/game/base_game.dart index 9464eeba7..3e0768e9b 100644 --- a/packages/flame/lib/src/game/base_game.dart +++ b/packages/flame/lib/src/game/base_game.dart @@ -240,4 +240,16 @@ class BaseGame extends Game with FPSCounter { return DateTime.now().microsecondsSinceEpoch.toDouble() / Duration.microsecondsPerSecond; } + + @override + Vector2 projectOffset(Offset value) { + final vector = value.toVector2(); + return camera.screenToWorld(vector); + } + + @override + Vector2 scaleOffset(Offset value) { + final vector = value.toVector2(); + return vector / camera.zoom; + } } diff --git a/packages/flame/lib/src/game/game.dart b/packages/flame/lib/src/game/game.dart index fd1aaa337..f2480004e 100644 --- a/packages/flame/lib/src/game/game.dart +++ b/packages/flame/lib/src/game/game.dart @@ -216,6 +216,20 @@ abstract class Game { /// - GameWidget /// - [Game.overlays] final overlays = ActiveOverlaysNotifier(); + + /// Use this method in case you need to project a coordinate received by a + /// gesture/pointer event to your game coordinate system. + /// + /// By default this just returns the same received coordinate transformed to + /// a Vector2, override this to add addtional logic to that projection + Vector2 projectOffset(Offset value) => value.toVector2(); + + /// Use this method in case you need to scale a coordinate received by a + /// gesture/pointer event to your game coordinate system. + /// + /// By default this just returns the same received coordinate transformed to + /// a Vector2, override this to add addtional logic to that scale + Vector2 scaleOffset(Offset value) => value.toVector2(); } /// A [ChangeNotifier] used to control the visibility of overlays on a [Game] instance. diff --git a/packages/flame/lib/src/game/game_widget.dart b/packages/flame/lib/src/game/game_widget.dart deleted file mode 100644 index e76154d57..000000000 --- a/packages/flame/lib/src/game/game_widget.dart +++ /dev/null @@ -1,548 +0,0 @@ -import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -import '../../extensions.dart'; -import '../components/mixins/draggable.dart'; -import '../components/mixins/tapable.dart'; -import '../extensions/offset.dart'; -import '../extensions/size.dart'; -import '../gestures.dart'; -import 'game.dart'; -import 'game_render_box.dart'; - -typedef GameLoadingWidgetBuilder = Widget Function( - BuildContext, -); - -typedef GameErrorWidgetBuilder = Widget Function( - BuildContext, - Object error, -); - -typedef OverlayWidgetBuilder = Widget Function( - BuildContext context, - T game, -); - -/// A [StatefulWidget] that is in charge of attaching a [Game] instance into the flutter tree -/// -class GameWidget extends StatefulWidget { - /// The game instance in which this widget will render - final T game; - - /// The text direction to be used in text elements in a game. - final TextDirection? textDirection; - - /// Builder to provide a widget tree to be built whilst the [Future] provided - /// via [Game.onLoad] is not resolved. By default this is an empty Container(). - final GameLoadingWidgetBuilder? loadingBuilder; - - /// If set, errors during the onLoad method will not be thrown - /// but instead this widget will be shown. If not provided, errors are - /// propagated up. - final GameErrorWidgetBuilder? errorBuilder; - - /// Builder to provide a widget tree to be built between the game elements and - /// the background color provided via [Game.backgroundColor] - final WidgetBuilder? backgroundBuilder; - - /// A map to show widgets overlay. - /// - /// See also: - /// - [new GameWidget] - /// - [Game.overlays] - final Map>? overlayBuilderMap; - - /// A List of the initially active overlays, this is used only on the first build of the widget. - /// To control the overlays that are active use [Game.overlays] - /// - /// See also: - /// - [new GameWidget] - /// - [Game.overlays] - final List? initialActiveOverlays; - - /// Renders a [game] in a flutter widget tree. - /// - /// Ex: - /// ``` - /// ... - /// Widget build(BuildContext context) { - /// return GameWidget( - /// game: MyGameClass(), - /// ) - /// } - /// ... - /// ``` - /// - /// It is also possible to render layers of widgets over the game surface with widget subtrees. - /// - /// To do that a [overlayBuilderMap] should be provided. The visibility of - /// these overlays are controlled by [Game.overlays] property - /// - /// Ex: - /// ``` - /// ... - /// - /// final game = MyGame(); - /// - /// Widget build(BuildContext context) { - /// return GameWidget( - /// game: game, - /// overlayBuilderMap: { - /// 'PauseMenu': (ctx) { - /// return Text('A pause menu'); - /// }, - /// }, - /// ) - /// } - /// ... - /// game.overlays.add('PauseMenu'); - /// ``` - const GameWidget({ - Key? key, - required this.game, - this.textDirection, - this.loadingBuilder, - this.errorBuilder, - this.backgroundBuilder, - this.overlayBuilderMap, - this.initialActiveOverlays, - }) : super(key: key); - - /// Renders a [game] in a flutter widget tree alongside widgets overlays. - /// - /// To use overlays, the game subclass has to be mixed with HasWidgetsOverlay. - @override - _GameWidgetState createState() => _GameWidgetState(); -} - -class _GameWidgetState extends State> { - Set initialActiveOverlays = {}; - - Future? _gameLoaderFuture; - Future get _gameLoaderFutureCache => - _gameLoaderFuture ?? (_gameLoaderFuture = widget.game.onLoad()); - - @override - void initState() { - super.initState(); - - // Add the initial overlays - _initActiveOverlays(); - - addOverlaysListener(widget.game); - } - - void _initActiveOverlays() { - if (widget.initialActiveOverlays == null) { - return; - } - _checkOverlays(widget.initialActiveOverlays!.toSet()); - widget.initialActiveOverlays!.forEach((key) { - widget.game.overlays.add(key); - }); - } - - @override - void didUpdateWidget(GameWidget oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.game != widget.game) { - removeOverlaysListener(oldWidget.game); - - // Reset the overlays - _initActiveOverlays(); - addOverlaysListener(widget.game); - - // Reset the loader future - _gameLoaderFuture = null; - } - } - - @override - void dispose() { - super.dispose(); - removeOverlaysListener(widget.game); - } - - // widget overlay stuff - void addOverlaysListener(T game) { - widget.game.overlays.addListener(onChangeActiveOverlays); - initialActiveOverlays = widget.game.overlays.value; - } - - void removeOverlaysListener(T game) { - game.overlays.removeListener(onChangeActiveOverlays); - } - - void _checkOverlays(Set overlays) { - overlays.forEach((overlayKey) { - assert( - widget.overlayBuilderMap?.containsKey(overlayKey) ?? false, - 'A non mapped overlay has been added: $overlayKey', - ); - }); - } - - void onChangeActiveOverlays() { - _checkOverlays(widget.game.overlays.value); - setState(() { - initialActiveOverlays = widget.game.overlays.value; - }); - } - - @override - Widget build(BuildContext context) { - Widget internalGameWidget = _GameRenderObjectWidget(widget.game); - - final hasBasicDetectors = _hasBasicGestureDetectors(widget.game); - final hasAdvancedDetectors = _hasAdvancedGesturesDetectors(widget.game); - - assert( - !(hasBasicDetectors && hasAdvancedDetectors), - ''' - WARNING: Both Advanced and Basic detectors detected. - Advanced detectors will override basic detectors and the later will not receive events - ''', - ); - - if (hasBasicDetectors) { - internalGameWidget = _applyBasicGesturesDetectors( - widget.game, - internalGameWidget, - ); - } else if (hasAdvancedDetectors) { - internalGameWidget = _applyAdvancedGesturesDetectors( - widget.game, - internalGameWidget, - ); - } - - if (_hasMouseDetectors(widget.game)) { - internalGameWidget = _applyMouseDetectors( - widget.game, - internalGameWidget, - ); - } - - final stackedWidgets = [internalGameWidget]; - _addBackground(context, stackedWidgets); - _addOverlays(context, stackedWidgets); - - // We can use Directionality.maybeOf when that method lands on stable - final textDir = widget.textDirection ?? TextDirection.ltr; - - return Directionality( - textDirection: textDir, - child: Container( - color: widget.game.backgroundColor(), - child: LayoutBuilder( - builder: (_, BoxConstraints constraints) { - widget.game.onResize(constraints.biggest.toVector2()); - return FutureBuilder( - future: _gameLoaderFutureCache, - builder: (_, snapshot) { - if (snapshot.hasError) { - if (widget.errorBuilder == null) { - throw snapshot.error!; - } else { - return widget.errorBuilder!(context, snapshot.error!); - } - } - if (snapshot.connectionState == ConnectionState.done) { - return Stack(children: stackedWidgets); - } - return widget.loadingBuilder?.call(context) ?? Container(); - }, - ); - }, - ), - ), - ); - } - - List _addBackground(BuildContext context, List stackWidgets) { - if (widget.backgroundBuilder == null) { - return stackWidgets; - } - final backgroundContent = KeyedSubtree( - key: ValueKey(widget.game), - child: widget.backgroundBuilder!(context), - ); - stackWidgets.insert(0, backgroundContent); - return stackWidgets; - } - - List _addOverlays(BuildContext context, List stackWidgets) { - if (widget.overlayBuilderMap == null) { - return stackWidgets; - } - final widgets = initialActiveOverlays.map((String overlayKey) { - final builder = widget.overlayBuilderMap![overlayKey]!; - return KeyedSubtree( - key: ValueKey(overlayKey), - child: builder(context, widget.game), - ); - }); - stackWidgets.addAll(widgets); - return stackWidgets; - } -} - -bool _hasBasicGestureDetectors(Game game) => - game is TapDetector || - game is SecondaryTapDetector || - game is DoubleTapDetector || - game is LongPressDetector || - game is VerticalDragDetector || - game is HorizontalDragDetector || - game is ForcePressDetector || - game is PanDetector || - game is ScaleDetector; - -bool _hasAdvancedGesturesDetectors(Game game) => - game is MultiTouchTapDetector || - game is MultiTouchDragDetector || - game is HasTapableComponents || - game is HasDraggableComponents; - -bool _hasMouseDetectors(Game game) => - game is MouseMovementDetector || game is ScrollDetector; - -Widget _applyBasicGesturesDetectors(Game game, Widget child) { - return GestureDetector( - key: const ObjectKey('BasicGesturesDetector'), - behavior: HitTestBehavior.opaque, - - // Taps - onTap: game is TapDetector ? () => game.onTap() : null, - onTapCancel: game is TapDetector ? () => game.onTapCancel() : null, - onTapDown: - game is TapDetector ? (TapDownDetails d) => game.onTapDown(d) : null, - onTapUp: game is TapDetector ? (TapUpDetails d) => game.onTapUp(d) : null, - - // Secondary taps - onSecondaryTapDown: game is SecondaryTapDetector - ? (TapDownDetails d) => game.onSecondaryTapDown(d) - : null, - onSecondaryTapUp: game is SecondaryTapDetector - ? (TapUpDetails d) => game.onSecondaryTapUp(d) - : null, - onSecondaryTapCancel: - game is SecondaryTapDetector ? () => game.onSecondaryTapCancel() : null, - - // Double tap - onDoubleTap: game is DoubleTapDetector ? () => game.onDoubleTap() : null, - - // Long presses - onLongPress: game is LongPressDetector ? () => game.onLongPress() : null, - onLongPressStart: game is LongPressDetector - ? (LongPressStartDetails d) => game.onLongPressStart(d) - : null, - onLongPressMoveUpdate: game is LongPressDetector - ? (LongPressMoveUpdateDetails d) => game.onLongPressMoveUpdate(d) - : null, - onLongPressUp: - game is LongPressDetector ? () => game.onLongPressUp() : null, - onLongPressEnd: game is LongPressDetector - ? (LongPressEndDetails d) => game.onLongPressEnd(d) - : null, - - // Vertical drag - onVerticalDragDown: game is VerticalDragDetector - ? (DragDownDetails d) => game.onVerticalDragDown(d) - : null, - onVerticalDragStart: game is VerticalDragDetector - ? (DragStartDetails d) => game.onVerticalDragStart(d) - : null, - onVerticalDragUpdate: game is VerticalDragDetector - ? (DragUpdateDetails d) => game.onVerticalDragUpdate(d) - : null, - onVerticalDragEnd: game is VerticalDragDetector - ? (DragEndDetails d) => game.onVerticalDragEnd(d) - : null, - onVerticalDragCancel: - game is VerticalDragDetector ? () => game.onVerticalDragCancel() : null, - - // Horizontal drag - onHorizontalDragDown: game is HorizontalDragDetector - ? (DragDownDetails d) => game.onHorizontalDragDown(d) - : null, - onHorizontalDragStart: game is HorizontalDragDetector - ? (DragStartDetails d) => game.onHorizontalDragStart(d) - : null, - onHorizontalDragUpdate: game is HorizontalDragDetector - ? (DragUpdateDetails d) => game.onHorizontalDragUpdate(d) - : null, - onHorizontalDragEnd: game is HorizontalDragDetector - ? (DragEndDetails d) => game.onHorizontalDragEnd(d) - : null, - onHorizontalDragCancel: game is HorizontalDragDetector - ? () => game.onHorizontalDragCancel() - : null, - - // Force presses - onForcePressStart: game is ForcePressDetector - ? (ForcePressDetails d) => game.onForcePressStart(d) - : null, - onForcePressPeak: game is ForcePressDetector - ? (ForcePressDetails d) => game.onForcePressPeak(d) - : null, - onForcePressUpdate: game is ForcePressDetector - ? (ForcePressDetails d) => game.onForcePressUpdate(d) - : null, - onForcePressEnd: game is ForcePressDetector - ? (ForcePressDetails d) => game.onForcePressEnd(d) - : null, - - // Pan - onPanDown: - game is PanDetector ? (DragDownDetails d) => game.onPanDown(d) : null, - onPanStart: - game is PanDetector ? (DragStartDetails d) => game.onPanStart(d) : null, - onPanUpdate: game is PanDetector - ? (DragUpdateDetails d) => game.onPanUpdate(d) - : null, - onPanEnd: - game is PanDetector ? (DragEndDetails d) => game.onPanEnd(d) : null, - onPanCancel: game is PanDetector ? () => game.onPanCancel() : null, - - // Scales - onScaleStart: game is ScaleDetector - ? (ScaleStartDetails d) => game.onScaleStart(d) - : null, - onScaleUpdate: game is ScaleDetector - ? (ScaleUpdateDetails d) => game.onScaleUpdate(d) - : null, - onScaleEnd: game is ScaleDetector - ? (ScaleEndDetails d) => game.onScaleEnd(d) - : null, - - child: child, - ); -} - -Widget _applyAdvancedGesturesDetectors(Game game, Widget child) { - final gestures = {}; - var lastGeneratedDragId = 0; - - void addAndConfigureRecognizer( - T Function() ts, - void Function(T) bindHandlers, - ) { - gestures[T] = GestureRecognizerFactoryWithHandlers( - ts, - bindHandlers, - ); - } - - void addTapRecognizer(void Function(MultiTapGestureRecognizer) config) { - addAndConfigureRecognizer( - () => MultiTapGestureRecognizer(), - config, - ); - } - - void addDragRecognizer(Drag Function(int, Vector2) config) { - addAndConfigureRecognizer( - () => ImmediateMultiDragGestureRecognizer(), - (ImmediateMultiDragGestureRecognizer instance) { - instance.onStart = (Offset o) { - final pointerId = lastGeneratedDragId++; - final position = game.convertGlobalToLocalCoordinate(o.toVector2()); - return config(pointerId, position); - }; - }, - ); - } - - if (game is MultiTouchTapDetector) { - addTapRecognizer((MultiTapGestureRecognizer instance) { - instance.onTapDown = game.onTapDown; - instance.onTapUp = game.onTapUp; - instance.onTapCancel = game.onTapCancel; - instance.onTap = game.onTap; - }); - } else if (game is HasTapableComponents) { - addAndConfigureRecognizer( - () => MultiTapGestureRecognizer(), - (MultiTapGestureRecognizer instance) { - instance.onTapDown = game.onTapDown; - instance.onTapUp = game.onTapUp; - instance.onTapCancel = game.onTapCancel; - }, - ); - } - - if (game is MultiTouchDragDetector) { - addDragRecognizer((int pointerId, Vector2 position) { - game.onDragStart(pointerId, position); - return _DragEvent() - ..onUpdate = ((details) => game.onDragUpdate(pointerId, details)) - ..onEnd = ((details) => game.onDragEnd(pointerId, details)) - ..onCancel = (() => game.onDragCancel(pointerId)); - }); - } else if (game is HasDraggableComponents) { - addDragRecognizer((int pointerId, Vector2 position) { - game.onDragStart(pointerId, position); - return _DragEvent() - ..onUpdate = ((details) => game.onDragUpdate(pointerId, details)) - ..onEnd = ((details) => game.onDragEnd(pointerId, details)) - ..onCancel = (() => game.onDragCancel(pointerId)); - }); - } - - return RawGestureDetector( - gestures: gestures, - behavior: HitTestBehavior.opaque, - child: child, - ); -} - -Widget _applyMouseDetectors(Game game, Widget child) { - return Listener( - child: MouseRegion( - child: child, - onHover: game is MouseMovementDetector ? game.onMouseMove : null, - ), - onPointerSignal: (event) => - game is ScrollDetector && event is PointerScrollEvent - ? game.onScroll(event) - : null, - ); -} - -class _GameRenderObjectWidget extends LeafRenderObjectWidget { - final Game game; - - const _GameRenderObjectWidget(this.game); - - @override - RenderBox createRenderObject(BuildContext context) { - return RenderConstrainedBox( - child: GameRenderBox(context, game), - additionalConstraints: const BoxConstraints.expand(), - ); - } -} - -class _DragEvent extends Drag { - void Function(DragUpdateDetails)? onUpdate; - VoidCallback? onCancel; - void Function(DragEndDetails)? onEnd; - - @override - void update(DragUpdateDetails details) { - onUpdate?.call(details); - } - - @override - void cancel() { - onCancel?.call(); - } - - @override - void end(DragEndDetails details) { - onEnd?.call(details); - } -} diff --git a/packages/flame/lib/src/game/game_widget/game_widget.dart b/packages/flame/lib/src/game/game_widget/game_widget.dart new file mode 100644 index 000000000..198207f85 --- /dev/null +++ b/packages/flame/lib/src/game/game_widget/game_widget.dart @@ -0,0 +1,301 @@ +import 'package:flutter/rendering.dart'; +import 'package:flutter/widgets.dart'; + +import '../../../extensions.dart'; +import '../../extensions/size.dart'; +import '../game.dart'; +import '../game_render_box.dart'; + +import 'gestures.dart'; + +typedef GameLoadingWidgetBuilder = Widget Function( + BuildContext, +); + +typedef GameErrorWidgetBuilder = Widget Function( + BuildContext, + Object error, +); + +typedef OverlayWidgetBuilder = Widget Function( + BuildContext context, + T game, +); + +/// A [StatefulWidget] that is in charge of attaching a [Game] instance into the flutter tree +/// +class GameWidget extends StatefulWidget { + /// The game instance in which this widget will render + final T game; + + /// The text direction to be used in text elements in a game. + final TextDirection? textDirection; + + /// Builder to provide a widget tree to be built whilst the [Future] provided + /// via [Game.onLoad] is not resolved. By default this is an empty Container(). + final GameLoadingWidgetBuilder? loadingBuilder; + + /// If set, errors during the onLoad method will not be thrown + /// but instead this widget will be shown. If not provided, errors are + /// propagated up. + final GameErrorWidgetBuilder? errorBuilder; + + /// Builder to provide a widget tree to be built between the game elements and + /// the background color provided via [Game.backgroundColor] + final WidgetBuilder? backgroundBuilder; + + /// A map to show widgets overlay. + /// + /// See also: + /// - [new GameWidget] + /// - [Game.overlays] + final Map>? overlayBuilderMap; + + /// A List of the initially active overlays, this is used only on the first build of the widget. + /// To control the overlays that are active use [Game.overlays] + /// + /// See also: + /// - [new GameWidget] + /// - [Game.overlays] + final List? initialActiveOverlays; + + /// Renders a [game] in a flutter widget tree. + /// + /// Ex: + /// ``` + /// ... + /// Widget build(BuildContext context) { + /// return GameWidget( + /// game: MyGameClass(), + /// ) + /// } + /// ... + /// ``` + /// + /// It is also possible to render layers of widgets over the game surface with widget subtrees. + /// + /// To do that a [overlayBuilderMap] should be provided. The visibility of + /// these overlays are controlled by [Game.overlays] property + /// + /// Ex: + /// ``` + /// ... + /// + /// final game = MyGame(); + /// + /// Widget build(BuildContext context) { + /// return GameWidget( + /// game: game, + /// overlayBuilderMap: { + /// 'PauseMenu': (ctx) { + /// return Text('A pause menu'); + /// }, + /// }, + /// ) + /// } + /// ... + /// game.overlays.add('PauseMenu'); + /// ``` + const GameWidget({ + Key? key, + required this.game, + this.textDirection, + this.loadingBuilder, + this.errorBuilder, + this.backgroundBuilder, + this.overlayBuilderMap, + this.initialActiveOverlays, + }) : super(key: key); + + /// Renders a [game] in a flutter widget tree alongside widgets overlays. + /// + /// To use overlays, the game subclass has to be mixed with HasWidgetsOverlay. + @override + _GameWidgetState createState() => _GameWidgetState(); +} + +class _GameWidgetState extends State> { + Set initialActiveOverlays = {}; + + Future? _gameLoaderFuture; + Future get _gameLoaderFutureCache => + _gameLoaderFuture ?? (_gameLoaderFuture = widget.game.onLoad()); + + @override + void initState() { + super.initState(); + + // Add the initial overlays + _initActiveOverlays(); + + addOverlaysListener(widget.game); + } + + void _initActiveOverlays() { + if (widget.initialActiveOverlays == null) { + return; + } + _checkOverlays(widget.initialActiveOverlays!.toSet()); + widget.initialActiveOverlays!.forEach((key) { + widget.game.overlays.add(key); + }); + } + + @override + void didUpdateWidget(GameWidget oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.game != widget.game) { + removeOverlaysListener(oldWidget.game); + + // Reset the overlays + _initActiveOverlays(); + addOverlaysListener(widget.game); + + // Reset the loader future + _gameLoaderFuture = null; + } + } + + @override + void dispose() { + super.dispose(); + removeOverlaysListener(widget.game); + } + + // widget overlay stuff + void addOverlaysListener(T game) { + widget.game.overlays.addListener(onChangeActiveOverlays); + initialActiveOverlays = widget.game.overlays.value; + } + + void removeOverlaysListener(T game) { + game.overlays.removeListener(onChangeActiveOverlays); + } + + void _checkOverlays(Set overlays) { + overlays.forEach((overlayKey) { + assert( + widget.overlayBuilderMap?.containsKey(overlayKey) ?? false, + 'A non mapped overlay has been added: $overlayKey', + ); + }); + } + + void onChangeActiveOverlays() { + _checkOverlays(widget.game.overlays.value); + setState(() { + initialActiveOverlays = widget.game.overlays.value; + }); + } + + @override + Widget build(BuildContext context) { + Widget internalGameWidget = _GameRenderObjectWidget(widget.game); + + final hasBasicDetectors = hasBasicGestureDetectors(widget.game); + final hasAdvancedDetectors = hasAdvancedGesturesDetectors(widget.game); + + assert( + !(hasBasicDetectors && hasAdvancedDetectors), + ''' + WARNING: Both Advanced and Basic detectors detected. + Advanced detectors will override basic detectors and the later will not receive events + ''', + ); + + if (hasBasicDetectors) { + internalGameWidget = applyBasicGesturesDetectors( + widget.game, + internalGameWidget, + ); + } else if (hasAdvancedDetectors) { + internalGameWidget = applyAdvancedGesturesDetectors( + widget.game, + internalGameWidget, + ); + } + + if (hasMouseDetectors(widget.game)) { + internalGameWidget = applyMouseDetectors( + widget.game, + internalGameWidget, + ); + } + + final stackedWidgets = [internalGameWidget]; + _addBackground(context, stackedWidgets); + _addOverlays(context, stackedWidgets); + + // We can use Directionality.maybeOf when that method lands on stable + final textDir = widget.textDirection ?? TextDirection.ltr; + + return Directionality( + textDirection: textDir, + child: Container( + color: widget.game.backgroundColor(), + child: LayoutBuilder( + builder: (_, BoxConstraints constraints) { + widget.game.onResize(constraints.biggest.toVector2()); + return FutureBuilder( + future: _gameLoaderFutureCache, + builder: (_, snapshot) { + if (snapshot.hasError) { + if (widget.errorBuilder == null) { + throw snapshot.error!; + } else { + return widget.errorBuilder!(context, snapshot.error!); + } + } + if (snapshot.connectionState == ConnectionState.done) { + return Stack(children: stackedWidgets); + } + return widget.loadingBuilder?.call(context) ?? Container(); + }, + ); + }, + ), + ), + ); + } + + List _addBackground(BuildContext context, List stackWidgets) { + if (widget.backgroundBuilder == null) { + return stackWidgets; + } + final backgroundContent = KeyedSubtree( + key: ValueKey(widget.game), + child: widget.backgroundBuilder!(context), + ); + stackWidgets.insert(0, backgroundContent); + return stackWidgets; + } + + List _addOverlays(BuildContext context, List stackWidgets) { + if (widget.overlayBuilderMap == null) { + return stackWidgets; + } + final widgets = initialActiveOverlays.map((String overlayKey) { + final builder = widget.overlayBuilderMap![overlayKey]!; + return KeyedSubtree( + key: ValueKey(overlayKey), + child: builder(context, widget.game), + ); + }); + stackWidgets.addAll(widgets); + return stackWidgets; + } +} + +class _GameRenderObjectWidget extends LeafRenderObjectWidget { + final Game game; + + const _GameRenderObjectWidget(this.game); + + @override + RenderBox createRenderObject(BuildContext context) { + return RenderConstrainedBox( + child: GameRenderBox(context, game), + additionalConstraints: const BoxConstraints.expand(), + ); + } +} diff --git a/packages/flame/lib/src/game/game_widget/gestures.dart b/packages/flame/lib/src/game/game_widget/gestures.dart new file mode 100644 index 000000000..0646e01df --- /dev/null +++ b/packages/flame/lib/src/game/game_widget/gestures.dart @@ -0,0 +1,294 @@ +import 'package:flutter/gestures.dart'; +import 'package:flutter/widgets.dart'; + +import '../../../extensions.dart'; +import '../../components/mixins/draggable.dart'; +import '../../components/mixins/tapable.dart'; +import '../../extensions/offset.dart'; +import '../../gestures/detectors.dart'; +import '../../gestures/events.dart'; +import '../game.dart'; + +bool hasBasicGestureDetectors(Game game) => + game is TapDetector || + game is SecondaryTapDetector || + game is DoubleTapDetector || + game is LongPressDetector || + game is VerticalDragDetector || + game is HorizontalDragDetector || + game is ForcePressDetector || + game is PanDetector || + game is ScaleDetector; + +bool hasAdvancedGesturesDetectors(Game game) => + game is MultiTouchTapDetector || + game is MultiTouchDragDetector || + game is HasTapableComponents || + game is HasDraggableComponents; + +bool hasMouseDetectors(Game game) => + game is MouseMovementDetector || game is ScrollDetector; + +Widget applyBasicGesturesDetectors(Game game, Widget child) { + return GestureDetector( + key: const ObjectKey('BasicGesturesDetector'), + behavior: HitTestBehavior.opaque, + + // Taps + onTap: game is TapDetector ? () => game.onTap() : null, + onTapCancel: game is TapDetector ? () => game.onTapCancel() : null, + onTapDown: game is TapDetector + ? (TapDownDetails d) => game.onTapDown(TapDownInfo.fromDetails(game, d)) + : null, + onTapUp: game is TapDetector + ? (TapUpDetails d) => game.onTapUp(TapUpInfo.fromDetails(game, d)) + : null, + + // Secondary taps + onSecondaryTapDown: game is SecondaryTapDetector + ? (TapDownDetails d) => + game.onSecondaryTapDown(TapDownInfo.fromDetails(game, d)) + : null, + onSecondaryTapUp: game is SecondaryTapDetector + ? (TapUpDetails d) => + game.onSecondaryTapUp(TapUpInfo.fromDetails(game, d)) + : null, + onSecondaryTapCancel: + game is SecondaryTapDetector ? () => game.onSecondaryTapCancel() : null, + + // Double tap + onDoubleTap: game is DoubleTapDetector ? () => game.onDoubleTap() : null, + + // Long presses + onLongPress: game is LongPressDetector ? () => game.onLongPress() : null, + onLongPressStart: game is LongPressDetector + ? (LongPressStartDetails d) => + game.onLongPressStart(LongPressStartInfo.fromDetails(game, d)) + : null, + onLongPressMoveUpdate: game is LongPressDetector + ? (LongPressMoveUpdateDetails d) => game + .onLongPressMoveUpdate(LongPressMoveUpdateInfo.fromDetails(game, d)) + : null, + onLongPressUp: + game is LongPressDetector ? () => game.onLongPressUp() : null, + onLongPressEnd: game is LongPressDetector + ? (LongPressEndDetails d) => + game.onLongPressEnd(LongPressEndInfo.fromDetails(game, d)) + : null, + + // Vertical drag + onVerticalDragDown: game is VerticalDragDetector + ? (DragDownDetails d) => + game.onVerticalDragDown(DragDownInfo.fromDetails(game, d)) + : null, + onVerticalDragStart: game is VerticalDragDetector + ? (DragStartDetails d) => + game.onVerticalDragStart(DragStartInfo.fromDetails(game, d)) + : null, + onVerticalDragUpdate: game is VerticalDragDetector + ? (DragUpdateDetails d) => + game.onVerticalDragUpdate(DragUpdateInfo.fromDetails(game, d)) + : null, + onVerticalDragEnd: game is VerticalDragDetector + ? (DragEndDetails d) => + game.onVerticalDragEnd(DragEndInfo.fromDetails(game, d)) + : null, + onVerticalDragCancel: + game is VerticalDragDetector ? () => game.onVerticalDragCancel() : null, + + // Horizontal drag + onHorizontalDragDown: game is HorizontalDragDetector + ? (DragDownDetails d) => + game.onHorizontalDragDown(DragDownInfo.fromDetails(game, d)) + : null, + onHorizontalDragStart: game is HorizontalDragDetector + ? (DragStartDetails d) => + game.onHorizontalDragStart(DragStartInfo.fromDetails(game, d)) + : null, + onHorizontalDragUpdate: game is HorizontalDragDetector + ? (DragUpdateDetails d) => + game.onHorizontalDragUpdate(DragUpdateInfo.fromDetails(game, d)) + : null, + onHorizontalDragEnd: game is HorizontalDragDetector + ? (DragEndDetails d) => + game.onHorizontalDragEnd(DragEndInfo.fromDetails(game, d)) + : null, + onHorizontalDragCancel: game is HorizontalDragDetector + ? () => game.onHorizontalDragCancel() + : null, + + // Force presses + onForcePressStart: game is ForcePressDetector + ? (ForcePressDetails d) => + game.onForcePressStart(ForcePressInfo.fromDetails(game, d)) + : null, + onForcePressPeak: game is ForcePressDetector + ? (ForcePressDetails d) => + game.onForcePressPeak(ForcePressInfo.fromDetails(game, d)) + : null, + onForcePressUpdate: game is ForcePressDetector + ? (ForcePressDetails d) => + game.onForcePressUpdate(ForcePressInfo.fromDetails(game, d)) + : null, + onForcePressEnd: game is ForcePressDetector + ? (ForcePressDetails d) => + game.onForcePressEnd(ForcePressInfo.fromDetails(game, d)) + : null, + + // Pan + onPanDown: game is PanDetector + ? (DragDownDetails d) => + game.onPanDown(DragDownInfo.fromDetails(game, d)) + : null, + onPanStart: game is PanDetector + ? (DragStartDetails d) => + game.onPanStart(DragStartInfo.fromDetails(game, d)) + : null, + onPanUpdate: game is PanDetector + ? (DragUpdateDetails d) => + game.onPanUpdate(DragUpdateInfo.fromDetails(game, d)) + : null, + onPanEnd: game is PanDetector + ? (DragEndDetails d) => game.onPanEnd(DragEndInfo.fromDetails(game, d)) + : null, + onPanCancel: game is PanDetector ? () => game.onPanCancel() : null, + + // Scales + onScaleStart: game is ScaleDetector + ? (ScaleStartDetails d) => + game.onScaleStart(ScaleStartInfo.fromDetails(game, d)) + : null, + onScaleUpdate: game is ScaleDetector + ? (ScaleUpdateDetails d) => + game.onScaleUpdate(ScaleUpdateInfo.fromDetails(game, d)) + : null, + onScaleEnd: game is ScaleDetector + ? (ScaleEndDetails d) => + game.onScaleEnd(ScaleEndInfo.fromDetails(game, d)) + : null, + + child: child, + ); +} + +Widget applyAdvancedGesturesDetectors(Game game, Widget child) { + final gestures = {}; + var lastGeneratedDragId = 0; + + void addAndConfigureRecognizer( + T Function() ts, + void Function(T) bindHandlers, + ) { + gestures[T] = GestureRecognizerFactoryWithHandlers( + ts, + bindHandlers, + ); + } + + void addTapRecognizer(void Function(MultiTapGestureRecognizer) config) { + addAndConfigureRecognizer( + () => MultiTapGestureRecognizer(), + config, + ); + } + + void addDragRecognizer(Drag Function(int, Vector2) config) { + addAndConfigureRecognizer( + () => ImmediateMultiDragGestureRecognizer(), + (ImmediateMultiDragGestureRecognizer instance) { + instance.onStart = (Offset o) { + final pointerId = lastGeneratedDragId++; + final position = game.convertGlobalToLocalCoordinate(o.toVector2()); + return config(pointerId, position); + }; + }, + ); + } + + if (game is MultiTouchTapDetector) { + addTapRecognizer((MultiTapGestureRecognizer instance) { + instance.onTapDown = + (i, d) => game.onTapDown(i, TapDownInfo.fromDetails(game, d)); + instance.onTapUp = + (i, d) => game.onTapUp(i, TapUpInfo.fromDetails(game, d)); + instance.onTapCancel = game.onTapCancel; + instance.onTap = game.onTap; + }); + } else if (game is HasTapableComponents) { + addAndConfigureRecognizer( + () => MultiTapGestureRecognizer(), + (MultiTapGestureRecognizer instance) { + instance.onTapDown = + (i, d) => game.onTapDown(i, TapDownInfo.fromDetails(game, d)); + instance.onTapUp = + (i, d) => game.onTapUp(i, TapUpInfo.fromDetails(game, d)); + instance.onTapCancel = (i) => game.onTapCancel(i); + }, + ); + } + + if (game is MultiTouchDragDetector) { + addDragRecognizer((int pointerId, Vector2 position) { + game.onDragStart(pointerId, position); + return _DragEvent(game) + ..onUpdate = ((details) => game.onDragUpdate(pointerId, details)) + ..onEnd = ((details) => game.onDragEnd(pointerId, details)) + ..onCancel = (() => game.onDragCancel(pointerId)); + }); + } else if (game is HasDraggableComponents) { + addDragRecognizer((int pointerId, Vector2 position) { + game.onDragStart(pointerId, position); + return _DragEvent(game) + ..onUpdate = ((details) => game.onDragUpdate(pointerId, details)) + ..onEnd = ((details) => game.onDragEnd(pointerId, details)) + ..onCancel = (() => game.onDragCancel(pointerId)); + }); + } + + return RawGestureDetector( + gestures: gestures, + behavior: HitTestBehavior.opaque, + child: child, + ); +} + +Widget applyMouseDetectors(Game game, Widget child) { + return Listener( + child: MouseRegion( + child: child, + onHover: game is MouseMovementDetector + ? (e) => game.onMouseMove(PointerHoverInfo.fromDetails(game, e)) + : null, + ), + onPointerSignal: (event) => + game is ScrollDetector && event is PointerScrollEvent + ? game.onScroll(PointerScrollInfo.fromDetails(game, event)) + : null, + ); +} + +class _DragEvent extends Drag { + final Game gameRef; + void Function(DragUpdateInfo)? onUpdate; + VoidCallback? onCancel; + void Function(DragEndInfo)? onEnd; + + _DragEvent(this.gameRef); + + @override + void update(DragUpdateDetails details) { + final event = DragUpdateInfo.fromDetails(gameRef, details); + onUpdate?.call(event); + } + + @override + void cancel() { + onCancel?.call(); + } + + @override + void end(DragEndDetails details) { + final event = DragEndInfo.fromDetails(gameRef, details); + onEnd?.call(event); + } +} diff --git a/packages/flame/lib/src/gestures.dart b/packages/flame/lib/src/gestures.dart deleted file mode 100644 index 82ac2c24a..000000000 --- a/packages/flame/lib/src/gestures.dart +++ /dev/null @@ -1,90 +0,0 @@ -import 'package:flutter/gestures.dart'; - -import '../extensions.dart'; -import 'game/game.dart'; - -mixin MultiTouchTapDetector on Game { - void onTap(int pointerId) {} - void onTapCancel(int pointerId) {} - void onTapDown(int pointerId, TapDownDetails details) {} - void onTapUp(int pointerId, TapUpDetails details) {} - void onLongTapDown(int pointerId, TapDownDetails details) {} -} - -mixin MultiTouchDragDetector on Game { - void onDragStart(int pointerId, Vector2 startPosition) {} - void onDragUpdate(int pointerId, DragUpdateDetails details) {} - void onDragEnd(int pointerId, DragEndDetails details) {} - void onDragCancel(int pointerId) {} -} - -// Basic touch detectors -mixin TapDetector on Game { - void onTap() {} - void onTapCancel() {} - void onTapDown(TapDownDetails details) {} - void onTapUp(TapUpDetails details) {} -} - -mixin SecondaryTapDetector on Game { - void onSecondaryTapDown(TapDownDetails details) {} - void onSecondaryTapUp(TapUpDetails details) {} - void onSecondaryTapCancel() {} -} - -mixin DoubleTapDetector on Game { - void onDoubleTap() {} -} - -mixin LongPressDetector on Game { - void onLongPress() {} - void onLongPressStart(LongPressStartDetails details) {} - void onLongPressMoveUpdate(LongPressMoveUpdateDetails details) {} - void onLongPressUp() {} - void onLongPressEnd(LongPressEndDetails details) {} -} - -mixin VerticalDragDetector on Game { - void onVerticalDragDown(DragDownDetails details) {} - void onVerticalDragStart(DragStartDetails details) {} - void onVerticalDragUpdate(DragUpdateDetails details) {} - void onVerticalDragEnd(DragEndDetails details) {} - void onVerticalDragCancel() {} -} - -mixin HorizontalDragDetector on Game { - void onHorizontalDragDown(DragDownDetails details) {} - void onHorizontalDragStart(DragStartDetails details) {} - void onHorizontalDragUpdate(DragUpdateDetails details) {} - void onHorizontalDragEnd(DragEndDetails details) {} - void onHorizontalDragCancel() {} -} - -mixin ForcePressDetector on Game { - void onForcePressStart(ForcePressDetails details) {} - void onForcePressPeak(ForcePressDetails details) {} - void onForcePressUpdate(ForcePressDetails details) {} - void onForcePressEnd(ForcePressDetails details) {} -} - -mixin PanDetector on Game { - void onPanDown(DragDownDetails details) {} - void onPanStart(DragStartDetails details) {} - void onPanUpdate(DragUpdateDetails details) {} - void onPanEnd(DragEndDetails details) {} - void onPanCancel() {} -} - -mixin ScaleDetector on Game { - void onScaleStart(ScaleStartDetails details) {} - void onScaleUpdate(ScaleUpdateDetails details) {} - void onScaleEnd(ScaleEndDetails details) {} -} - -mixin MouseMovementDetector on Game { - void onMouseMove(PointerHoverEvent event) {} -} - -mixin ScrollDetector on Game { - void onScroll(PointerScrollEvent event) {} -} diff --git a/packages/flame/lib/src/gestures/detectors.dart b/packages/flame/lib/src/gestures/detectors.dart new file mode 100644 index 000000000..50d640538 --- /dev/null +++ b/packages/flame/lib/src/gestures/detectors.dart @@ -0,0 +1,89 @@ +import '../../extensions.dart'; +import '../game/game.dart'; +import 'events.dart'; + +mixin MultiTouchTapDetector on Game { + void onTap(int pointerId) {} + void onTapCancel(int pointerId) {} + void onTapDown(int pointerId, TapDownInfo event) {} + void onTapUp(int pointerId, TapUpInfo event) {} + void onLongTapDown(int pointerId, TapDownInfo event) {} +} + +mixin MultiTouchDragDetector on Game { + void onDragStart(int pointerId, Vector2 startPosition) {} + void onDragUpdate(int pointerId, DragUpdateInfo info) {} + void onDragEnd(int pointerId, DragEndInfo info) {} + void onDragCancel(int pointerId) {} +} + +// Basic touch detectors +mixin TapDetector on Game { + void onTap() {} + void onTapCancel() {} + void onTapDown(TapDownInfo event) {} + void onTapUp(TapUpInfo event) {} +} + +mixin SecondaryTapDetector on Game { + void onSecondaryTapDown(TapDownInfo event) {} + void onSecondaryTapUp(TapUpInfo event) {} + void onSecondaryTapCancel() {} +} + +mixin DoubleTapDetector on Game { + void onDoubleTap() {} +} + +mixin LongPressDetector on Game { + void onLongPress() {} + void onLongPressStart(LongPressStartInfo event) {} + void onLongPressMoveUpdate(LongPressMoveUpdateInfo event) {} + void onLongPressUp() {} + void onLongPressEnd(LongPressEndInfo event) {} +} + +mixin VerticalDragDetector on Game { + void onVerticalDragDown(DragDownInfo info) {} + void onVerticalDragStart(DragStartInfo info) {} + void onVerticalDragUpdate(DragUpdateInfo info) {} + void onVerticalDragEnd(DragEndInfo info) {} + void onVerticalDragCancel() {} +} + +mixin HorizontalDragDetector on Game { + void onHorizontalDragDown(DragDownInfo info) {} + void onHorizontalDragStart(DragStartInfo info) {} + void onHorizontalDragUpdate(DragUpdateInfo info) {} + void onHorizontalDragEnd(DragEndInfo info) {} + void onHorizontalDragCancel() {} +} + +mixin ForcePressDetector on Game { + void onForcePressStart(ForcePressInfo info) {} + void onForcePressPeak(ForcePressInfo info) {} + void onForcePressUpdate(ForcePressInfo info) {} + void onForcePressEnd(ForcePressInfo info) {} +} + +mixin PanDetector on Game { + void onPanDown(DragDownInfo info) {} + void onPanStart(DragStartInfo info) {} + void onPanUpdate(DragUpdateInfo info) {} + void onPanEnd(DragEndInfo info) {} + void onPanCancel() {} +} + +mixin ScaleDetector on Game { + void onScaleStart(ScaleStartInfo info) {} + void onScaleUpdate(ScaleUpdateInfo info) {} + void onScaleEnd(ScaleEndInfo info) {} +} + +mixin MouseMovementDetector on Game { + void onMouseMove(PointerHoverInfo event) {} +} + +mixin ScrollDetector on Game { + void onScroll(PointerScrollInfo event) {} +} diff --git a/packages/flame/lib/src/gestures/events.dart b/packages/flame/lib/src/gestures/events.dart new file mode 100644 index 000000000..6edd50dc5 --- /dev/null +++ b/packages/flame/lib/src/gestures/events.dart @@ -0,0 +1,185 @@ +import 'package:flutter/gestures.dart'; + +import '../../extensions.dart'; +import '../game/game.dart'; + +/// [EventPosition] converts position based events to three different coordinate systems (global, local and game). +/// +/// global: coordinate system relative to the entire app; same as `globalPosition` in Flutter +/// widget: coordinate system relative to the GameWidget widget; same as `localPosition` in Flutter +/// game: same as `widget` but also applies any transformations from the camera or viewport to the coordinate system +class EventPosition { + final Game _game; + final Offset _localPosition; + final Offset? _globalPosition; + + /// Coordinates of the event relative to the game position/size and transformations + late final Vector2 game = _game.projectOffset(_localPosition); + + /// Coordinates of the event relative to the game widget position/size + late final Vector2 widget = _localPosition.toVector2(); + + /// Coordinates of the event relative to the whole screen + late final Vector2 global = _globalPosition?.toVector2() ?? + _game.convertLocalToGlobalCoordinate(_localPosition.toVector2()); + + EventPosition(this._game, this._localPosition, this._globalPosition); +} + +/// BaseInfo is the base class for Flame's input events. +/// This base class just wraps Flutter's [raw] attribute. +abstract class BaseInfo { + final T raw; + + BaseInfo(this.raw); +} + +/// A more specialized wrapper of Flame's base class for input events. +/// It adds the [eventPosition] field and is used by all position based +/// events on Flame. +abstract class PositionInfo extends BaseInfo { + final Game _game; + final Offset _position; + final Offset? _globalPosition; + + late final eventPosition = EventPosition( + _game, + _position, + _globalPosition, + ); + + PositionInfo( + this._game, + this._position, + this._globalPosition, + T raw, + ) : super(raw); +} + +class TapDownInfo extends PositionInfo { + TapDownInfo.fromDetails( + Game game, + TapDownDetails raw, + ) : super(game, raw.localPosition, raw.globalPosition, raw); +} + +class TapUpInfo extends PositionInfo { + TapUpInfo.fromDetails( + Game game, + TapUpDetails raw, + ) : super(game, raw.localPosition, raw.globalPosition, raw); +} + +class LongPressStartInfo extends PositionInfo { + LongPressStartInfo.fromDetails( + Game game, + LongPressStartDetails raw, + ) : super(game, raw.localPosition, raw.globalPosition, raw); +} + +class LongPressEndInfo extends PositionInfo { + late final Vector2 velocity = _game.scaleOffset(raw.velocity.pixelsPerSecond); + + LongPressEndInfo.fromDetails( + Game game, + LongPressEndDetails raw, + ) : super(game, raw.localPosition, raw.globalPosition, raw); +} + +class LongPressMoveUpdateInfo extends PositionInfo { + LongPressMoveUpdateInfo.fromDetails( + Game game, + LongPressMoveUpdateDetails raw, + ) : super(game, raw.localPosition, raw.globalPosition, raw); +} + +class ForcePressInfo extends PositionInfo { + late final double pressure = raw.pressure; + + ForcePressInfo.fromDetails( + Game game, + ForcePressDetails raw, + ) : super(game, raw.localPosition, raw.globalPosition, raw); +} + +class PointerScrollInfo extends PositionInfo { + late final Vector2 scrollDelta = _game.scaleOffset(raw.scrollDelta); + + PointerScrollInfo.fromDetails( + Game game, + PointerScrollEvent raw, + ) : super(game, raw.localPosition, null, raw); +} + +class PointerHoverInfo extends PositionInfo { + PointerHoverInfo.fromDetails( + Game game, + PointerHoverEvent raw, + ) : super(game, raw.localPosition, null, raw); +} + +class DragDownInfo extends PositionInfo { + DragDownInfo.fromDetails( + Game game, + DragDownDetails raw, + ) : super(game, raw.localPosition, raw.globalPosition, raw); +} + +class DragStartInfo extends PositionInfo { + DragStartInfo.fromDetails( + Game game, + DragStartDetails raw, + ) : super(game, raw.localPosition, raw.globalPosition, raw); +} + +class DragUpdateInfo extends PositionInfo { + late final Vector2 delta = _game.scaleOffset(raw.delta); + + DragUpdateInfo.fromDetails( + Game game, + DragUpdateDetails raw, + ) : super(game, raw.localPosition, raw.globalPosition, raw); +} + +class DragEndInfo extends BaseInfo { + final Game _game; + late final Vector2 velocity = _game.scaleOffset(raw.velocity.pixelsPerSecond); + double? get primaryVelocity => raw.primaryVelocity; + + DragEndInfo.fromDetails( + this._game, + DragEndDetails raw, + ) : super(raw); +} + +class ScaleStartInfo extends PositionInfo { + int get pointerCount => raw.pointerCount; + + ScaleStartInfo.fromDetails( + Game game, + ScaleStartDetails raw, + ) : super(game, raw.localFocalPoint, raw.focalPoint, raw); +} + +class ScaleEndInfo extends BaseInfo { + final Game _game; + late final Vector2 velocity = _game.scaleOffset(raw.velocity.pixelsPerSecond); + int get pointerCount => raw.pointerCount; + + ScaleEndInfo.fromDetails( + this._game, + ScaleEndDetails raw, + ) : super(raw); +} + +class ScaleUpdateInfo extends PositionInfo { + int get pointerCount => raw.pointerCount; + double get rotation => raw.rotation; + late final Vector2 scale = + _game.scaleOffset(Offset(raw.horizontalScale, raw.verticalScale)); + + ScaleUpdateInfo.fromDetails( + Game game, + ScaleUpdateDetails raw, + ) : super(game, raw.localFocalPoint, raw.focalPoint, raw); +} diff --git a/packages/flame/test/components/composed_component_test.dart b/packages/flame/test/components/composed_component_test.dart index 3c9833a78..0934cac70 100644 --- a/packages/flame/test/components/composed_component_test.dart +++ b/packages/flame/test/components/composed_component_test.dart @@ -2,10 +2,10 @@ import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame/game.dart'; -import 'package:flutter/gestures.dart'; import 'package:test/test.dart'; import '../util/mock_canvas.dart'; +import '../util/mock_gesture_events.dart'; class MyGame extends BaseGame with HasTapableComponents {} @@ -36,7 +36,7 @@ class MyTap extends PositionComponent with Tapable { } @override - bool onTapDown(TapDownDetails details) { + bool onTapDown(_) { ++tapTimes; return true; } @@ -92,7 +92,7 @@ void main() { game.add(wrapper); wrapper.addChild(child); game.update(0.0); - game.onTapDown(1, TapDownDetails()); + game.onTapDown(1, createTapDownEvent(game)); expect(child.gameSize, size); expect(child.tapped, true); @@ -111,7 +111,13 @@ void main() { game.add(wrapper); wrapper.addChild(child); game.update(0.0); - game.onTapDown(1, TapDownDetails(globalPosition: const Offset(250, 250))); + game.onTapDown( + 1, + createTapDownEvent( + game, + position: const Offset(250, 250), + ), + ); expect(child.gameSize, size); expect(child.tapped, true); diff --git a/packages/flame/test/game/base_game_test.dart b/packages/flame/test/game/base_game_test.dart index a18001a20..f63fceb3e 100644 --- a/packages/flame/test/game/base_game_test.dart +++ b/packages/flame/test/game/base_game_test.dart @@ -9,6 +9,8 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart' as flutter; import 'package:test/test.dart'; +import '../util/mock_gesture_events.dart'; + class MyGame extends BaseGame with HasTapableComponents {} class MyComponent extends PositionComponent with Tapable, HasGameRef { @@ -19,7 +21,7 @@ class MyComponent extends PositionComponent with Tapable, HasGameRef { late Vector2 gameSize; @override - bool onTapDown(TapDownDetails details) { + bool onTapDown(_) { tapped = true; return true; } @@ -112,7 +114,7 @@ void main() { game.add(component); // The component is not added to the component list until an update has been performed game.update(0.0); - game.onTapDown(1, TapDownDetails()); + game.onTapDown(1, createTapDownEvent(game)); expect(component.tapped, true); }); diff --git a/packages/flame/test/util/mock_gesture_events.dart b/packages/flame/test/util/mock_gesture_events.dart new file mode 100644 index 000000000..aec07316d --- /dev/null +++ b/packages/flame/test/util/mock_gesture_events.dart @@ -0,0 +1,18 @@ +import 'package:flame/game.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flame/gestures.dart'; +import 'package:flame/extensions.dart'; + +TapDownInfo createTapDownEvent( + Game game, { + Offset? position, + Offset? globalPosition, +}) { + return TapDownInfo.fromDetails( + game, + TapDownDetails( + localPosition: position ?? Offset.zero, + globalPosition: globalPosition ?? Offset.zero, + ), + ); +} diff --git a/tutorials/2_sprite_animations_gestures/README.md b/tutorials/2_sprite_animations_gestures/README.md index ebd693e08..eb0ecc589 100644 --- a/tutorials/2_sprite_animations_gestures/README.md +++ b/tutorials/2_sprite_animations_gestures/README.md @@ -189,7 +189,7 @@ class MyGame extends Game with TapDetector { // Variables declaration, onLoad and render methods omited... @override - void onTapDown(TapDownDetails details) { + void onTapDown(TapDownInfo event) { // On tap down we need to check if the event ocurred on the // button area. There are several ways of doing it, for this // tutorial we do that by transforming ours position and size @@ -198,13 +198,13 @@ class MyGame extends Game with TapDetector { // if a point (Offset) is inside that rect final buttonArea = buttonPosition & buttonSize; - isPressed = buttonArea.contains(details.localPosition); + isPressed = buttonArea.contains(event.eventPosition.game.toOffset()); } // On both tap up and tap cancel we just set the isPressed // variable to false @override - void onTapUp(TapUpDetails details) { + void onTapUp(TapUpInfo event) { isPressed = false; } diff --git a/tutorials/2_sprite_animations_gestures/code/lib/main.dart b/tutorials/2_sprite_animations_gestures/code/lib/main.dart index 9426c3215..64b68023c 100644 --- a/tutorials/2_sprite_animations_gestures/code/lib/main.dart +++ b/tutorials/2_sprite_animations_gestures/code/lib/main.dart @@ -46,14 +46,14 @@ class MyGame extends Game with TapDetector { } @override - void onTapDown(TapDownDetails details) { + void onTapDown(TapDownInfo event) { final buttonArea = buttonPosition & buttonSize; - isPressed = buttonArea.contains(details.localPosition); + isPressed = buttonArea.contains(event.eventPosition.game.toOffset()); } @override - void onTapUp(TapUpDetails details) { + void onTapUp(TapUpInfo event) { isPressed = false; }