From 7694edb31a8a4b60e1144c5b5e660aefb52ff40f Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 6 Jan 2021 18:58:34 +0100 Subject: [PATCH 01/28] Move out collision detection methods --- lib/collision_detection.dart | 27 +++++++++++++++ lib/src/components/base_component.dart | 2 +- lib/src/components/mixins/draggable.dart | 2 +- lib/src/components/mixins/tapable.dart | 4 +-- lib/src/components/position_component.dart | 36 +++++++++++--------- test/components/position_component_test.dart | 14 ++++---- 6 files changed, 57 insertions(+), 28 deletions(-) create mode 100644 lib/collision_detection.dart diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart new file mode 100644 index 00000000000..a65bf6cea89 --- /dev/null +++ b/lib/collision_detection.dart @@ -0,0 +1,27 @@ +import 'dart:math' as math; + +import 'src/extensions/vector2.dart'; + +/// Checks whether the [polygon] represented by the list of [Vector2] contains +/// the point. Note that the polygon list is mutated. +bool containsPoint(Vector2 point, List polygon) { + polygon.add(polygon.first); + for (int i = 1; i < polygon.length; i++) { + final previousPoint = polygon[i - 1]; + final point = polygon[i]; + final isOutside = + (point.x - previousPoint.x) * (point.y - previousPoint.y) - + (point.x - previousPoint.x) * (point.y - previousPoint.y) > + 0; + if (isOutside) { + // Point is outside of convex polygon (only used for rectangles so far) + return false; + } + } + return true; +} + +List collisionPoints(List polygon1, List polygon2) { + return []; +} + diff --git a/lib/src/components/base_component.dart b/lib/src/components/base_component.dart index 3db21eaef03..51313579f1f 100644 --- a/lib/src/components/base_component.dart +++ b/lib/src/components/base_component.dart @@ -109,7 +109,7 @@ abstract class BaseComponent extends Component { /// Called to check whether the point is to be counted as within the component /// It needs to be overridden to have any effect, like it is in the /// [PositionComponent] - bool checkOverlap(Vector2 point) => false; + bool containsPoint(Vector2 point) => false; /// Add an effect to the component void addEffect(ComponentEffect effect) { diff --git a/lib/src/components/mixins/draggable.dart b/lib/src/components/mixins/draggable.dart index dda70947e45..880237ca4be 100644 --- a/lib/src/components/mixins/draggable.dart +++ b/lib/src/components/mixins/draggable.dart @@ -12,7 +12,7 @@ mixin Draggable on BaseComponent { } bool handleReceiveDrag(DragEvent event) { - if (checkOverlap(event.initialPosition.toVector2())) { + if (containsPoint(event.initialPosition.toVector2())) { return onReceiveDrag(event); } return true; diff --git a/lib/src/components/mixins/tapable.dart b/lib/src/components/mixins/tapable.dart index 833694f7275..c9567600cbe 100644 --- a/lib/src/components/mixins/tapable.dart +++ b/lib/src/components/mixins/tapable.dart @@ -24,7 +24,7 @@ mixin Tapable on BaseComponent { bool _checkPointerId(int pointerId) => _currentPointerId == pointerId; bool handleTapDown(int pointerId, TapDownDetails details) { - if (checkOverlap(details.localPosition.toVector2())) { + if (containsPoint(details.localPosition.toVector2())) { _currentPointerId = pointerId; return onTapDown(details); } @@ -33,7 +33,7 @@ mixin Tapable on BaseComponent { bool handleTapUp(int pointerId, TapUpDetails details) { if (_checkPointerId(pointerId) && - checkOverlap(details.localPosition.toVector2())) { + containsPoint(details.localPosition.toVector2())) { _currentPointerId = null; return onTapUp(details); } diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 7f84fc89d5e..2fd0e430e1c 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -2,6 +2,7 @@ import 'dart:ui' hide Offset; import 'dart:math' as math; import '../anchor.dart'; +import '../../collision_detection.dart' as collision_detection; import '../extensions/offset.dart'; import '../extensions/vector2.dart'; import '../../game.dart'; @@ -99,25 +100,18 @@ abstract class PositionComponent extends BaseComponent { } @override - bool checkOverlap(Vector2 absolutePoint) { - final point = absolutePoint - absoluteCanvasPosition; - final corners = _rotatedCorners(); - for (int i = 0; i < corners.length; i++) { - final previousCorner = corners[i]; - final corner = corners[(i + 1) % corners.length]; - final isOutside = - (corner.x - previousCorner.x) * (point.y - previousCorner.y) - - (point.x - previousCorner.x) * (corner.y - previousCorner.y) > - 0; - if (isOutside) { - // Point is outside of convex polygon (only used for rectangles so far) - return false; - } - } - return true; + bool containsPoint(Vector2 point) { + return collision_detection.containsPoint( + point - absoluteCanvasPosition, + boundingBox(), + ); } - List _rotatedCorners() { + /// Gives back the bounding box represented as a list of points which are the + /// corners of the box rotated with [angle], if overridden it can return + /// more than four "corners" for more accurate collision detection and overlap + /// detection, but the points has to form a convex polygon. + List boundingBox() { // Rotates the corner around [position] Vector2 rotateCorner(Vector2 corner) { return Vector2( @@ -139,6 +133,14 @@ abstract class PositionComponent extends BaseComponent { ]; } + + + List collisionPoints(PositionComponent other) { + + + return []; + } + double angleTo(PositionComponent c) => position.angleTo(c.position); double distance(PositionComponent c) => position.distanceTo(c.position); diff --git a/test/components/position_component_test.dart b/test/components/position_component_test.dart index 34ff5ce84ab..9e016feccf6 100644 --- a/test/components/position_component_test.dart +++ b/test/components/position_component_test.dart @@ -15,7 +15,7 @@ void main() { component.anchor = Anchor.center; final point = Vector2(2.0, 2.0); - expect(component.checkOverlap(point), true); + expect(component.containsPoint(point), true); }); test('overlap on edge', () { @@ -26,7 +26,7 @@ void main() { component.anchor = Anchor.center; final point = Vector2(1.0, 1.0); - expect(component.checkOverlap(point), true); + expect(component.containsPoint(point), true); }); test('not overlapping with x', () { @@ -37,7 +37,7 @@ void main() { component.anchor = Anchor.center; final point = Vector2(4.0, 1.0); - expect(component.checkOverlap(point), false); + expect(component.containsPoint(point), false); }); test('not overlapping with y', () { @@ -48,7 +48,7 @@ void main() { component.anchor = Anchor.center; final point = Vector2(1.0, 4.0); - expect(component.checkOverlap(point), false); + expect(component.containsPoint(point), false); }); test('overlapping with angle', () { @@ -59,7 +59,7 @@ void main() { component.anchor = Anchor.center; final point = Vector2(3.1, 2.0); - expect(component.checkOverlap(point), true); + expect(component.containsPoint(point), true); }); test('not overlapping with angle', () { @@ -70,7 +70,7 @@ void main() { component.anchor = Anchor.center; final point = Vector2(1.0, 0.1); - expect(component.checkOverlap(point), false); + expect(component.containsPoint(point), false); }); test('overlapping with angle and topLeft anchor', () { @@ -81,7 +81,7 @@ void main() { component.anchor = Anchor.topLeft; final point = Vector2(1.0, 3.1); - expect(component.checkOverlap(point), true); + expect(component.containsPoint(point), true); }); }); } From 8e76ed3d8a2c0b0d70fb144599817940fef861d6 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sat, 9 Jan 2021 16:36:41 +0100 Subject: [PATCH 02/28] Add possibility to define a hull for PositionComponents --- lib/src/components/position_component.dart | 37 +++++++++++++--------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 2fd0e430e1c..1ed0bf08806 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -25,6 +25,12 @@ abstract class PositionComponent extends BaseComponent { /// The position of this component on the screen (relative to the anchor). Vector2 position = Vector2.zero(); + /// The list of vertices used for collision detection and to define whether + /// a point is inside of the component or not, so that the tap detection etc + /// can be more accurately performed. + /// The hull is defined from the center of the component. + List hull; + /// X position of this component on the screen (relative to the anchor). double get x => position.x; set x(double x) => position.x = x; @@ -72,6 +78,11 @@ abstract class PositionComponent extends BaseComponent { this.position = position + (anchor.toVector2..multiply(size)); } + /// Get the position of the center of the component + Vector2 get center { + return anchor == Anchor.center ? position : topLeftPosition + (size / 2); + } + /// Angle (with respect to the x-axis) this component should be rendered with. /// It is rotated around its anchor. double angle = 0.0; @@ -124,21 +135,17 @@ abstract class PositionComponent extends BaseComponent { ); } - // Counter-clockwise direction - return [ - rotateCorner(topLeftPosition), // Top-left - rotateCorner(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left - rotateCorner(topLeftPosition + size), // Bottom-right - rotateCorner(topLeftPosition + Vector2(size.x, 0.0)), // Top-right - ]; - } - - - - List collisionPoints(PositionComponent other) { - - - return []; + // Uses a hull if defined, otherwise just the size rectangle + return hull + ?.map((point) => position + point) + ?.map(rotateCorner) + ?.toList(growable: false) ?? + [ + rotateCorner(topLeftPosition), // Top-left + rotateCorner(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left + rotateCorner(topLeftPosition + size), // Bottom-right + rotateCorner(topLeftPosition + Vector2(size.x, 0.0)), // Top-right + ]; } double angleTo(PositionComponent c) => position.angleTo(c.position); From 99ec70f896d99ef90daf8609a8cbd1784df016fe Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sat, 9 Jan 2021 17:12:51 +0100 Subject: [PATCH 03/28] Add example of how to use hull with tapable --- .../gestures/lib/main_tapables_hull.dart | 60 +++++++++++++++++++ lib/collision_detection.dart | 59 ++++++++++++++++-- lib/components/mixins/collidable.dart | 31 ++++++++++ lib/src/components/position_component.dart | 18 ++++-- 4 files changed, 158 insertions(+), 10 deletions(-) create mode 100644 doc/examples/gestures/lib/main_tapables_hull.dart create mode 100644 lib/components/mixins/collidable.dart diff --git a/doc/examples/gestures/lib/main_tapables_hull.dart b/doc/examples/gestures/lib/main_tapables_hull.dart new file mode 100644 index 00000000000..def2fb305a2 --- /dev/null +++ b/doc/examples/gestures/lib/main_tapables_hull.dart @@ -0,0 +1,60 @@ +import 'package:flame/anchor.dart'; +import 'package:flame/extensions/vector2.dart'; +import 'package:flutter/material.dart'; +import 'package:flame/game.dart'; +import 'package:flame/components/position_component.dart'; +import 'package:flame/components/mixins/tapable.dart'; + +void main() { + runApp( + Container( + padding: const EdgeInsets.all(50), + color: const Color(0xFFA9A9A9), + child: GameWidget( + game: MyGame(), + ), + ), + ); +} + +class TapablePolygon extends PositionComponent with Tapable { + + TapablePolygon({Vector2 position}) { + size = Vector2.all(100); + hull = [ + Vector2(-50, 0), + Vector2(-40, 30), + Vector2(0, 50), + Vector2(30, 45), + Vector2(50, 0), + Vector2(30, -40), + Vector2(0, -50), + Vector2(-40, -40), + ]; + this.position = position ?? Vector2.all(100); + } + + @override + bool onTapUp(TapUpDetails details) { + return true; + } + + @override + bool onTapDown(TapDownDetails details) { + angle += 1.0; + return true; + } + + @override + bool onTapCancel() { + return true; + } +} + +class MyGame extends BaseGame with HasTapableComponents { + MyGame() { + debugMode = true; + add(TapablePolygon()..anchor = Anchor.center); + add(TapablePolygon()..y = 350); + } +} diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart index a65bf6cea89..bb0c3237574 100644 --- a/lib/collision_detection.dart +++ b/lib/collision_detection.dart @@ -5,10 +5,9 @@ import 'src/extensions/vector2.dart'; /// Checks whether the [polygon] represented by the list of [Vector2] contains /// the point. Note that the polygon list is mutated. bool containsPoint(Vector2 point, List polygon) { - polygon.add(polygon.first); - for (int i = 1; i < polygon.length; i++) { - final previousPoint = polygon[i - 1]; - final point = polygon[i]; + for (int i = 0; i < polygon.length; i++) { + final previousPoint = polygon[i]; + final point = polygon[(i + 1) % polygon.length]; final isOutside = (point.x - previousPoint.x) * (point.y - previousPoint.y) - (point.x - previousPoint.x) * (point.y - previousPoint.y) > @@ -21,7 +20,57 @@ bool containsPoint(Vector2 point, List polygon) { return true; } -List collisionPoints(List polygon1, List polygon2) { +class LinearEquation { + final double a; + final double b; + final double c; + + const LinearEquation(this.a, this.b, this.c); + + static LinearEquation fromPoints(Vector2 a1, Vector2 a2) { + // ax + by = c + final a = a2.y - a1.y; + final b = a1.x - a2.x; + final c = a * a1.x + b * a1.y; + return LinearEquation(a, b, c); + } +} + +/// Returns an empty list if there is no intersection +List lineSegmentIntersection( + Vector2 a1, + Vector2 a2, + Vector2 b1, + Vector2 b2, +) { + final line1 = LinearEquation.fromPoints(a1, a2); + final line2 = LinearEquation.fromPoints(a1, a2); + final determinant = line1.a * line2.b - line2.a * line1.b; + if (determinant == 0) { + //The lines are parallel and have no intersection + return []; + } + final result = Vector2( + (line2.b * line1.c - line1.b * line2.c) / determinant, + (line1.a * line2.c - line2.a * line1.c) / determinant, + ); + if (math.min(a1.x, b1.x) <= result.x && + result.x <= math.max(a2.x, b2.x) && + math.min(a1.y, b1.y) <= result.y && + result.y <= math.max(a2.y, b2.y)) { + // The result is within the two segments + return [result]; + } return []; } +/// Determines where (or if) two polygons intersect, if they don't have any +/// intersection an empty list will be returned. +List collisionPoints(List polygon1, List polygon2) { + int x = 0; + int y = 0; + + while (x < polygon1.length || y < polygon2.length) {} + + return []; +} diff --git a/lib/components/mixins/collidable.dart b/lib/components/mixins/collidable.dart new file mode 100644 index 00000000000..617ea77c72c --- /dev/null +++ b/lib/components/mixins/collidable.dart @@ -0,0 +1,31 @@ +import '../position_component.dart'; +import '../../extensions/vector2.dart'; +import '../../game/base_game.dart'; + +mixin Collidable on PositionComponent { + void setHullFromSize() { + if (size == null || (size?.length ?? 0) == 0) { + hull = []; + } else { + hull = [ + size / 2, + (size / 2)..multiply(Vector2(1, -1)), + (size / 2)..multiply(Vector2(-1, -1)), + (size / 2)..multiply(Vector2(-1, 1)), + ]; + } + } + + /// Override this to define what will happen to your component when it + /// collides with another component + bool collisionCallback( + PositionComponent otherComponent, + List collisionPoints, + ); + + /// Override this to define what will happen to your component when it + /// collides with a wall + bool wallCollisionCallback(List collisionPoints); +} + +mixin HasCollidableComponents on BaseGame {} diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 1ed0bf08806..282df064a64 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -136,11 +136,11 @@ abstract class PositionComponent extends BaseComponent { } // Uses a hull if defined, otherwise just the size rectangle - return hull - ?.map((point) => position + point) - ?.map(rotateCorner) - ?.toList(growable: false) ?? - [ + //return hull + // ?.map((point) => position + point) + // ?.map(rotateCorner) + // ?.toList(growable: false) ?? + return [ rotateCorner(topLeftPosition), // Top-left rotateCorner(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left rotateCorner(topLeftPosition + size), // Bottom-right @@ -154,6 +154,14 @@ abstract class PositionComponent extends BaseComponent { @override void renderDebugMode(Canvas canvas) { + if (hull != null) { + final hullPath = Path() + ..addPolygon( + hull.map((point) => (point + size / 2).toOffset()).toList(), + true, + ); + canvas.drawPath(hullPath, debugPaint); + } canvas.drawRect(size.toRect(), debugPaint); debugTextConfig.render( canvas, From a6ee7ca9cfdb567deacba8b525edefea39abcfba Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sat, 9 Jan 2021 17:38:29 +0100 Subject: [PATCH 04/28] Update contains point comment --- lib/collision_detection.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart index bb0c3237574..fdf15ccea60 100644 --- a/lib/collision_detection.dart +++ b/lib/collision_detection.dart @@ -3,7 +3,7 @@ import 'dart:math' as math; import 'src/extensions/vector2.dart'; /// Checks whether the [polygon] represented by the list of [Vector2] contains -/// the point. Note that the polygon list is mutated. +/// the [point]. bool containsPoint(Vector2 point, List polygon) { for (int i = 0; i < polygon.length; i++) { final previousPoint = polygon[i]; @@ -13,7 +13,7 @@ bool containsPoint(Vector2 point, List polygon) { (point.x - previousPoint.x) * (point.y - previousPoint.y) > 0; if (isOutside) { - // Point is outside of convex polygon (only used for rectangles so far) + // Point is outside of convex polygon return false; } } From 040502bc8e0c6e9d5444a6acdfb0e6c479a84c61 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sat, 9 Jan 2021 17:51:28 +0100 Subject: [PATCH 05/28] Fix contains point --- lib/collision_detection.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart index fdf15ccea60..f3b7cf18fb3 100644 --- a/lib/collision_detection.dart +++ b/lib/collision_detection.dart @@ -6,11 +6,11 @@ import 'src/extensions/vector2.dart'; /// the [point]. bool containsPoint(Vector2 point, List polygon) { for (int i = 0; i < polygon.length; i++) { - final previousPoint = polygon[i]; - final point = polygon[(i + 1) % polygon.length]; + final previousNode = polygon[i]; + final node = polygon[(i + 1) % polygon.length]; final isOutside = - (point.x - previousPoint.x) * (point.y - previousPoint.y) - - (point.x - previousPoint.x) * (point.y - previousPoint.y) > + (node.x - previousNode.x) * (point.y - previousNode.y) - + (point.x - previousNode.x) * (node.y - previousNode.y) > 0; if (isOutside) { // Point is outside of convex polygon From 851105970e1fb8950eb71d67e14f93357bff73c3 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sat, 9 Jan 2021 17:55:14 +0100 Subject: [PATCH 06/28] Hull should be based on center position --- doc/examples/gestures/lib/main_tapables_hull.dart | 1 - lib/collision_detection.dart | 7 +++---- lib/src/components/position_component.dart | 10 +++++----- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/doc/examples/gestures/lib/main_tapables_hull.dart b/doc/examples/gestures/lib/main_tapables_hull.dart index def2fb305a2..d3f88d67da0 100644 --- a/doc/examples/gestures/lib/main_tapables_hull.dart +++ b/doc/examples/gestures/lib/main_tapables_hull.dart @@ -18,7 +18,6 @@ void main() { } class TapablePolygon extends PositionComponent with Tapable { - TapablePolygon({Vector2 position}) { size = Vector2.all(100); hull = [ diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart index f3b7cf18fb3..4d71c59c663 100644 --- a/lib/collision_detection.dart +++ b/lib/collision_detection.dart @@ -8,10 +8,9 @@ bool containsPoint(Vector2 point, List polygon) { for (int i = 0; i < polygon.length; i++) { final previousNode = polygon[i]; final node = polygon[(i + 1) % polygon.length]; - final isOutside = - (node.x - previousNode.x) * (point.y - previousNode.y) - - (point.x - previousNode.x) * (node.y - previousNode.y) > - 0; + final isOutside = (node.x - previousNode.x) * (point.y - previousNode.y) - + (point.x - previousNode.x) * (node.y - previousNode.y) > + 0; if (isOutside) { // Point is outside of convex polygon return false; diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 282df064a64..1a807733c9a 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -136,11 +136,11 @@ abstract class PositionComponent extends BaseComponent { } // Uses a hull if defined, otherwise just the size rectangle - //return hull - // ?.map((point) => position + point) - // ?.map(rotateCorner) - // ?.toList(growable: false) ?? - return [ + return hull + ?.map((point) => center + point) + ?.map(rotateCorner) + ?.toList(growable: false) ?? + [ rotateCorner(topLeftPosition), // Top-left rotateCorner(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left rotateCorner(topLeftPosition + size), // Bottom-right From a761c7fb56bfea88648775036caac070d1320547 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sat, 9 Jan 2021 18:02:22 +0100 Subject: [PATCH 07/28] Remove collision detection parts --- lib/collision_detection.dart | 55 ------------------- .../components/mixins/collidable.dart | 0 lib/src/components/position_component.dart | 11 ++-- 3 files changed, 5 insertions(+), 61 deletions(-) rename lib/{ => src}/components/mixins/collidable.dart (100%) diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart index 4d71c59c663..8436a2cb463 100644 --- a/lib/collision_detection.dart +++ b/lib/collision_detection.dart @@ -18,58 +18,3 @@ bool containsPoint(Vector2 point, List polygon) { } return true; } - -class LinearEquation { - final double a; - final double b; - final double c; - - const LinearEquation(this.a, this.b, this.c); - - static LinearEquation fromPoints(Vector2 a1, Vector2 a2) { - // ax + by = c - final a = a2.y - a1.y; - final b = a1.x - a2.x; - final c = a * a1.x + b * a1.y; - return LinearEquation(a, b, c); - } -} - -/// Returns an empty list if there is no intersection -List lineSegmentIntersection( - Vector2 a1, - Vector2 a2, - Vector2 b1, - Vector2 b2, -) { - final line1 = LinearEquation.fromPoints(a1, a2); - final line2 = LinearEquation.fromPoints(a1, a2); - final determinant = line1.a * line2.b - line2.a * line1.b; - if (determinant == 0) { - //The lines are parallel and have no intersection - return []; - } - final result = Vector2( - (line2.b * line1.c - line1.b * line2.c) / determinant, - (line1.a * line2.c - line2.a * line1.c) / determinant, - ); - if (math.min(a1.x, b1.x) <= result.x && - result.x <= math.max(a2.x, b2.x) && - math.min(a1.y, b1.y) <= result.y && - result.y <= math.max(a2.y, b2.y)) { - // The result is within the two segments - return [result]; - } - return []; -} - -/// Determines where (or if) two polygons intersect, if they don't have any -/// intersection an empty list will be returned. -List collisionPoints(List polygon1, List polygon2) { - int x = 0; - int y = 0; - - while (x < polygon1.length || y < polygon2.length) {} - - return []; -} diff --git a/lib/components/mixins/collidable.dart b/lib/src/components/mixins/collidable.dart similarity index 100% rename from lib/components/mixins/collidable.dart rename to lib/src/components/mixins/collidable.dart diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 1a807733c9a..6ac9a5b1b59 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -114,15 +114,14 @@ abstract class PositionComponent extends BaseComponent { bool containsPoint(Vector2 point) { return collision_detection.containsPoint( point - absoluteCanvasPosition, - boundingBox(), + boundingVertices(), ); } - /// Gives back the bounding box represented as a list of points which are the - /// corners of the box rotated with [angle], if overridden it can return - /// more than four "corners" for more accurate collision detection and overlap - /// detection, but the points has to form a convex polygon. - List boundingBox() { + /// Gives back the bounding vertices (bounding box if no hull is specified) + /// represented as a list of points which are the "corners" of the hull/box + /// rotated with [angle]. + List boundingVertices() { // Rotates the corner around [position] Vector2 rotateCorner(Vector2 corner) { return Vector2( From 7bf6c1c508eae237d81a56ade23b3e37ff07f9fd Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sat, 9 Jan 2021 18:31:10 +0100 Subject: [PATCH 08/28] Added tests --- CHANGELOG.md | 1 + test/components/position_component_test.dart | 34 ++++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a98f2f0345f..d9821399709 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Move files to comply with the dart package layout convention - Fix gesture detection bug of children of `PositionComponent` - The `game` argument on `GameWidget` is now required + - Add hull capabilities for PositionComponent to make more accurate gestures ## 1.0.0-rc5 - Option for overlays to be already visible on the GameWidget diff --git a/test/components/position_component_test.dart b/test/components/position_component_test.dart index 9e016feccf6..3a55dcbb652 100644 --- a/test/components/position_component_test.dart +++ b/test/components/position_component_test.dart @@ -83,5 +83,39 @@ void main() { final point = Vector2(1.0, 3.1); expect(component.containsPoint(point), true); }); + + test('component with hull contains point', () { + final size = Vector2(2.0, 2.0); + final PositionComponent component = MyComponent(); + component.position = Vector2(1.0, 1.0); + component.anchor = Anchor.topLeft; + component.size = size; + component.hull = [ + Vector2(size.x/2, 0), + Vector2(0, -size.y/2), + Vector2(-size.x/2, 0), + Vector2(0, size.y/2), + ]; + + final point = component.position + component.size / 4; + expect(component.containsPoint(point), true); + }); + + test('component with hull does not contains point', () { + final size = Vector2(2.0, 2.0); + final PositionComponent component = MyComponent(); + component.position = Vector2(1.0, 1.0); + component.anchor = Anchor.topLeft; + component.size = size; + component.hull = [ + Vector2(size.x/2, 0), + Vector2(0, -size.y/2), + Vector2(-size.x/2, 0), + Vector2(0, size.y/2), + ]; + + final point = Vector2(1.1, 1.1); + expect(component.containsPoint(point), false); + }); }); } From 415414ea857d1564c55b534049347abbf97ab886 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sat, 9 Jan 2021 19:11:35 +0100 Subject: [PATCH 09/28] Use percentage of size instead of absolute size --- .../gestures/lib/main_tapables_hull.dart | 16 ++++----- lib/src/components/position_component.dart | 35 +++++++++++++------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/doc/examples/gestures/lib/main_tapables_hull.dart b/doc/examples/gestures/lib/main_tapables_hull.dart index d3f88d67da0..2dc53ec793b 100644 --- a/doc/examples/gestures/lib/main_tapables_hull.dart +++ b/doc/examples/gestures/lib/main_tapables_hull.dart @@ -21,14 +21,14 @@ class TapablePolygon extends PositionComponent with Tapable { TapablePolygon({Vector2 position}) { size = Vector2.all(100); hull = [ - Vector2(-50, 0), - Vector2(-40, 30), - Vector2(0, 50), - Vector2(30, 45), - Vector2(50, 0), - Vector2(30, -40), - Vector2(0, -50), - Vector2(-40, -40), + Vector2(-0.5, 0), + Vector2(-0.4, 0.3), + Vector2(0, 0.5), + Vector2(0.3, 0.45), + Vector2(0.5, 0), + Vector2(0.3, -0.4), + Vector2(0, -0.5), + Vector2(-0.4, -0.4), ]; this.position = position ?? Vector2.all(100); } diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 6ac9a5b1b59..ca8128aa78d 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -25,12 +25,6 @@ abstract class PositionComponent extends BaseComponent { /// The position of this component on the screen (relative to the anchor). Vector2 position = Vector2.zero(); - /// The list of vertices used for collision detection and to define whether - /// a point is inside of the component or not, so that the tap detection etc - /// can be more accurately performed. - /// The hull is defined from the center of the component. - List hull; - /// X position of this component on the screen (relative to the anchor). double get x => position.x; set x(double x) => position.x = x; @@ -98,6 +92,28 @@ abstract class PositionComponent extends BaseComponent { /// Whether this component should be flipped ofn the Y axis before being rendered. bool renderFlipY = false; + /// The list of vertices used for collision detection and to define whether + /// a point is inside of the component or not, so that the tap detection etc + /// can be more accurately performed. + /// The hull is defined from the center of the component and with percentages + /// of the size of the component. + /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] + /// This will form a square with a 45 degree angle (pi/4 rad) within the + /// bounding size box. + List hull; + + Iterable _scaledHull; + Vector2 _lastScaledSize; + /// Gives back the hull vectors multiplied by the size of the component and + /// positioned from the current component center. + Iterable get scaledHull { + if(_lastScaledSize != size || _scaledHull == null) { + _lastScaledSize = size; + _scaledHull = hull?.map((p) => p.clone()..multiply(size)) ?? []; + } + return _scaledHull; + } + /// Returns the relative position/size of this component. /// Relative because it might be translated by their parents (which is not considered here). Rect toRect() => topLeftPosition.toPositionedRect(size); @@ -135,9 +151,8 @@ abstract class PositionComponent extends BaseComponent { } // Uses a hull if defined, otherwise just the size rectangle - return hull - ?.map((point) => center + point) - ?.map(rotateCorner) + return scaledHull + ?.map((point) => rotateCorner(center + point)) ?.toList(growable: false) ?? [ rotateCorner(topLeftPosition), // Top-left @@ -156,7 +171,7 @@ abstract class PositionComponent extends BaseComponent { if (hull != null) { final hullPath = Path() ..addPolygon( - hull.map((point) => (point + size / 2).toOffset()).toList(), + scaledHull.map((point) => (point + size / 2).toOffset()).toList(), true, ); canvas.drawPath(hullPath, debugPaint); From 62c33a37b47386d160e2d3973214664e8d165c35 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 10 Jan 2021 14:57:20 +0100 Subject: [PATCH 10/28] Separate hull from PositionComponent --- .../gestures/lib/main_tapables_hull.dart | 6 +- lib/collision_detection.dart | 95 +++++++++++++++++++ lib/src/components/position_component.dart | 70 +++----------- test/components/position_component_test.dart | 16 ++-- 4 files changed, 120 insertions(+), 67 deletions(-) diff --git a/doc/examples/gestures/lib/main_tapables_hull.dart b/doc/examples/gestures/lib/main_tapables_hull.dart index 2dc53ec793b..f3cc2480f59 100644 --- a/doc/examples/gestures/lib/main_tapables_hull.dart +++ b/doc/examples/gestures/lib/main_tapables_hull.dart @@ -1,9 +1,6 @@ -import 'package:flame/anchor.dart'; -import 'package:flame/extensions/vector2.dart'; +import 'package:flame/components.dart'; import 'package:flutter/material.dart'; import 'package:flame/game.dart'; -import 'package:flame/components/position_component.dart'; -import 'package:flame/components/mixins/tapable.dart'; void main() { runApp( @@ -20,6 +17,7 @@ void main() { class TapablePolygon extends PositionComponent with Tapable { TapablePolygon({Vector2 position}) { size = Vector2.all(100); + // The hull is defined as percentages of the full size of the component hull = [ Vector2(-0.5, 0), Vector2(-0.4, 0.3), diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart index 8436a2cb463..f245f3b9c80 100644 --- a/lib/collision_detection.dart +++ b/lib/collision_detection.dart @@ -1,6 +1,101 @@ import 'dart:math' as math; import 'src/extensions/vector2.dart'; +import 'src/components/position_component.dart'; + +class Hull { + /// The list of vertices used for collision detection and to define whether + /// a point is inside of the component or not, so that the tap detection etc + /// can be more accurately performed. + /// The hull is defined from the center of the component and with percentages + /// of the size of the component. + /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] + /// This will form a square with a 45 degree angle (pi/4 rad) within the + /// bounding size box. + List vertices; + + /// The [PositionComponent] that the hull belongs to + final PositionComponent component; + + Hull(this.component, {this.vertices}); + + Iterable _scaledHull; + Vector2 _lastScaledSize; + + /// Whether the hull has defined vertices or not + /// An empty list of vertices is also accepted as valid hull + bool hasVertices() => vertices != null; + + /// Gives back the hull vectors multiplied by the size of the component and + /// positioned from the component's current center position. + Iterable get scaledHull { + if (_lastScaledSize != component.size || _scaledHull == null) { + _lastScaledSize = component.size; + _scaledHull = vertices?.map((p) => p.clone()..multiply(component.size)); + } + return _scaledHull; + } + + + // These variables are used to see whether the bounding vertices cache is + // valid or not + Vector2 _lastBoundingVerticesPosition; + Vector2 _lastBoundingVerticesSize; + double _lastBoundingVerticesAngle; + bool _hadVertices = false; + List _cachedBoundingVertices; + + bool _isBoundingVerticesCacheValid(PositionComponent component) { + final position = component.position; + final angle = component.angle; + final size = component.size; + return _lastBoundingVerticesAngle == angle && + _lastBoundingVerticesSize == size && + _lastBoundingVerticesPosition == position && + _hadVertices == hasVertices(); +} + + /// Gives back the bounding vertices (bounding box if no hull is specified) + /// represented as a list of points which are the "corners" of the hull/box + /// rotated with [angle]. + List boundingVertices() { + final position = component.position; + final angle = component.angle; + final size = component.size; + final topLeftPosition = component.topLeftPosition; + // Rotates the [point] with [angle] around [position] + Vector2 rotatePoint(Vector2 point) { + return Vector2( + math.cos(angle) * (point.x - position.x) - + math.sin(angle) * (point.y - position.y) + + position.x, + math.sin(angle) * (point.x - position.x) + + math.cos(angle) * (point.y - position.y) + + position.y, + ); + } + + // Use cached bounding vertices if state of the component hasn't changed + if (!_isBoundingVerticesCacheValid(component)) { + // Uses a the vertices as a hull if defined, otherwise just using the size rectangle + _cachedBoundingVertices = scaledHull + ?.map((point) => rotatePoint(component.center + point)) + ?.toList(growable: false) ?? + [ + rotatePoint(topLeftPosition), // Top-left + rotatePoint(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left + rotatePoint(topLeftPosition + size), // Bottom-right + rotatePoint(topLeftPosition + Vector2(size.x, 0.0)), // Top-right + ]; + _lastBoundingVerticesPosition = position; + _lastBoundingVerticesSize = size; + _lastBoundingVerticesAngle = angle; + _hadVertices = hasVertices(); + } + + return _cachedBoundingVertices; + } +} /// Checks whether the [polygon] represented by the list of [Vector2] contains /// the [point]. diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index ca8128aa78d..5d26bdcbafb 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -1,8 +1,10 @@ import 'dart:ui' hide Offset; -import 'dart:math' as math; -import '../anchor.dart'; +import 'package:meta/meta.dart'; + import '../../collision_detection.dart' as collision_detection; +import '../../collision_detection.dart'; +import '../anchor.dart'; import '../extensions/offset.dart'; import '../extensions/vector2.dart'; import '../../game.dart'; @@ -92,31 +94,17 @@ abstract class PositionComponent extends BaseComponent { /// Whether this component should be flipped ofn the Y axis before being rendered. bool renderFlipY = false; - /// The list of vertices used for collision detection and to define whether - /// a point is inside of the component or not, so that the tap detection etc - /// can be more accurately performed. - /// The hull is defined from the center of the component and with percentages - /// of the size of the component. - /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] - /// This will form a square with a 45 degree angle (pi/4 rad) within the - /// bounding size box. - List hull; - - Iterable _scaledHull; - Vector2 _lastScaledSize; - /// Gives back the hull vectors multiplied by the size of the component and - /// positioned from the current component center. - Iterable get scaledHull { - if(_lastScaledSize != size || _scaledHull == null) { - _lastScaledSize = size; - _scaledHull = hull?.map((p) => p.clone()..multiply(size)) ?? []; - } - return _scaledHull; - } - /// Returns the relative position/size of this component. /// Relative because it might be translated by their parents (which is not considered here). Rect toRect() => topLeftPosition.toPositionedRect(size); + + Hull _hull; + set hull(List vertices) => _hull.vertices = vertices; + + @mustCallSuper + PositionComponent() { + _hull = Hull(this); + } /// Mutates position and size using the provided [rect] as basis. /// This is a relative rect, same definition that [toRect] use @@ -130,48 +118,20 @@ abstract class PositionComponent extends BaseComponent { bool containsPoint(Vector2 point) { return collision_detection.containsPoint( point - absoluteCanvasPosition, - boundingVertices(), + _hull.boundingVertices(), ); } - /// Gives back the bounding vertices (bounding box if no hull is specified) - /// represented as a list of points which are the "corners" of the hull/box - /// rotated with [angle]. - List boundingVertices() { - // Rotates the corner around [position] - Vector2 rotateCorner(Vector2 corner) { - return Vector2( - math.cos(angle) * (corner.x - position.x) - - math.sin(angle) * (corner.y - position.y) + - position.x, - math.sin(angle) * (corner.x - position.x) + - math.cos(angle) * (corner.y - position.y) + - position.y, - ); - } - - // Uses a hull if defined, otherwise just the size rectangle - return scaledHull - ?.map((point) => rotateCorner(center + point)) - ?.toList(growable: false) ?? - [ - rotateCorner(topLeftPosition), // Top-left - rotateCorner(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left - rotateCorner(topLeftPosition + size), // Bottom-right - rotateCorner(topLeftPosition + Vector2(size.x, 0.0)), // Top-right - ]; - } - double angleTo(PositionComponent c) => position.angleTo(c.position); double distance(PositionComponent c) => position.distanceTo(c.position); @override void renderDebugMode(Canvas canvas) { - if (hull != null) { + if (_hull != null) { final hullPath = Path() ..addPolygon( - scaledHull.map((point) => (point + size / 2).toOffset()).toList(), + _hull.scaledHull.map((point) => (point + size / 2).toOffset()).toList(), true, ); canvas.drawPath(hullPath, debugPaint); diff --git a/test/components/position_component_test.dart b/test/components/position_component_test.dart index 3a55dcbb652..bddf4471c76 100644 --- a/test/components/position_component_test.dart +++ b/test/components/position_component_test.dart @@ -91,10 +91,10 @@ void main() { component.anchor = Anchor.topLeft; component.size = size; component.hull = [ - Vector2(size.x/2, 0), - Vector2(0, -size.y/2), - Vector2(-size.x/2, 0), - Vector2(0, size.y/2), + Vector2(0.5, 0), + Vector2(0, -0.5), + Vector2(-0.5, 0), + Vector2(0, 0.5), ]; final point = component.position + component.size / 4; @@ -108,10 +108,10 @@ void main() { component.anchor = Anchor.topLeft; component.size = size; component.hull = [ - Vector2(size.x/2, 0), - Vector2(0, -size.y/2), - Vector2(-size.x/2, 0), - Vector2(0, size.y/2), + Vector2(0.5, 0), + Vector2(0, -0.5), + Vector2(-0.5, 0), + Vector2(0, 0.5), ]; final point = Vector2(1.1, 1.1); From 288bb8607181b73e7b68c0270288a6904d2383a6 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 10 Jan 2021 21:18:39 +0100 Subject: [PATCH 11/28] Clarify hull example --- .../gestures/lib/main_tapables_hull.dart | 3 +- lib/collision_detection.dart | 80 ++++++++++--------- lib/src/components/position_component.dart | 16 +++- lib/src/extensions/vector2.dart | 3 + 4 files changed, 60 insertions(+), 42 deletions(-) diff --git a/doc/examples/gestures/lib/main_tapables_hull.dart b/doc/examples/gestures/lib/main_tapables_hull.dart index f3cc2480f59..620d1cd1591 100644 --- a/doc/examples/gestures/lib/main_tapables_hull.dart +++ b/doc/examples/gestures/lib/main_tapables_hull.dart @@ -28,7 +28,7 @@ class TapablePolygon extends PositionComponent with Tapable { Vector2(0, -0.5), Vector2(-0.4, -0.4), ]; - this.position = position ?? Vector2.all(100); + this.position = position ?? Vector2.all(150); } @override @@ -39,6 +39,7 @@ class TapablePolygon extends PositionComponent with Tapable { @override bool onTapDown(TapDownDetails details) { angle += 1.0; + size.add(Vector2.all(10)); return true; } diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart index f245f3b9c80..4674fa269fc 100644 --- a/lib/collision_detection.dart +++ b/lib/collision_detection.dart @@ -12,48 +12,48 @@ class Hull { /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] /// This will form a square with a 45 degree angle (pi/4 rad) within the /// bounding size box. - List vertices; - + List vertexScales; + /// The [PositionComponent] that the hull belongs to final PositionComponent component; - - Hull(this.component, {this.vertices}); + + Hull(this.component, {this.vertexScales}); Iterable _scaledHull; Vector2 _lastScaledSize; /// Whether the hull has defined vertices or not /// An empty list of vertices is also accepted as valid hull - bool hasVertices() => vertices != null; + bool hasVertices() => vertexScales != null; /// Gives back the hull vectors multiplied by the size of the component and /// positioned from the component's current center position. Iterable get scaledHull { if (_lastScaledSize != component.size || _scaledHull == null) { _lastScaledSize = component.size; - _scaledHull = vertices?.map((p) => p.clone()..multiply(component.size)); + _scaledHull = + vertexScales?.map((p) => p.clone()..multiply(component.size)); } return _scaledHull; } - // These variables are used to see whether the bounding vertices cache is // valid or not - Vector2 _lastBoundingVerticesPosition; - Vector2 _lastBoundingVerticesSize; - double _lastBoundingVerticesAngle; + Vector2 _lastCachePosition; + Vector2 _lastCacheSize; + double _lastCacheAngle; bool _hadVertices = false; - List _cachedBoundingVertices; - + List _cachedVertices; + bool _isBoundingVerticesCacheValid(PositionComponent component) { final position = component.position; final angle = component.angle; final size = component.size; - return _lastBoundingVerticesAngle == angle && - _lastBoundingVerticesSize == size && - _lastBoundingVerticesPosition == position && + return _lastCacheAngle == angle && + _lastCacheSize == size && + _lastCachePosition == position && _hadVertices == hasVertices(); -} + } /// Gives back the bounding vertices (bounding box if no hull is specified) /// represented as a list of points which are the "corners" of the hull/box @@ -63,37 +63,29 @@ class Hull { final angle = component.angle; final size = component.size; final topLeftPosition = component.topLeftPosition; - // Rotates the [point] with [angle] around [position] - Vector2 rotatePoint(Vector2 point) { - return Vector2( - math.cos(angle) * (point.x - position.x) - - math.sin(angle) * (point.y - position.y) + - position.x, - math.sin(angle) * (point.x - position.x) + - math.cos(angle) * (point.y - position.y) + - position.y, - ); - } // Use cached bounding vertices if state of the component hasn't changed if (!_isBoundingVerticesCacheValid(component)) { + // Rotate [point] around component angle and position (anchor) + Vector2 rotate(Vector2 point) => rotatePoint(point, angle, position); + // Uses a the vertices as a hull if defined, otherwise just using the size rectangle - _cachedBoundingVertices = scaledHull - ?.map((point) => rotatePoint(component.center + point)) - ?.toList(growable: false) ?? + _cachedVertices = scaledHull + ?.map((point) => rotate(component.center + point)) + ?.toList(growable: false) ?? [ - rotatePoint(topLeftPosition), // Top-left - rotatePoint(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left - rotatePoint(topLeftPosition + size), // Bottom-right - rotatePoint(topLeftPosition + Vector2(size.x, 0.0)), // Top-right + rotate(topLeftPosition), // Top-left + rotate(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left + rotate(topLeftPosition + size), // Bottom-right + rotate(topLeftPosition + Vector2(size.x, 0.0)), // Top-right ]; - _lastBoundingVerticesPosition = position; - _lastBoundingVerticesSize = size; - _lastBoundingVerticesAngle = angle; + _lastCachePosition = position; + _lastCacheSize = size; + _lastCacheAngle = angle; _hadVertices = hasVertices(); } - return _cachedBoundingVertices; + return _cachedVertices; } } @@ -113,3 +105,15 @@ bool containsPoint(Vector2 point, List polygon) { } return true; } + +// Rotates the [point] with [angle] around [position] +Vector2 rotatePoint(Vector2 point, double angle, Vector2 position) { + return Vector2( + math.cos(angle) * (point.x - position.x) - + math.sin(angle) * (point.y - position.y) + + position.x, + math.sin(angle) * (point.x - position.x) + + math.cos(angle) * (point.y - position.y) + + position.y, + ); +} diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 5d26bdcbafb..ae6353718d2 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -97,9 +97,17 @@ abstract class PositionComponent extends BaseComponent { /// Returns the relative position/size of this component. /// Relative because it might be translated by their parents (which is not considered here). Rect toRect() => topLeftPosition.toPositionedRect(size); - + Hull _hull; - set hull(List vertices) => _hull.vertices = vertices; + /// The list of vertices used for collision detection and to define whether + /// a point is inside of the component or not, so that the tap detection etc + /// can be more accurately performed. + /// The hull is defined from the center of the component and with percentages + /// of the size of the component. + /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] + /// This will form a square with a 45 degree angle (pi/4 rad) within the + /// bounding size box. + set hull(List vertices) => _hull.vertexScales = vertices; @mustCallSuper PositionComponent() { @@ -131,7 +139,9 @@ abstract class PositionComponent extends BaseComponent { if (_hull != null) { final hullPath = Path() ..addPolygon( - _hull.scaledHull.map((point) => (point + size / 2).toOffset()).toList(), + _hull.scaledHull + .map((point) => (point + size / 2).toOffset()) + .toList(), true, ); canvas.drawPath(hullPath, debugPaint); diff --git a/lib/src/extensions/vector2.dart b/lib/src/extensions/vector2.dart index 2e5f1da1378..b1685cd55f7 100644 --- a/lib/src/extensions/vector2.dart +++ b/lib/src/extensions/vector2.dart @@ -45,6 +45,9 @@ extension Vector2Extension on Vector2 { } } + /// Modulo/Remainder + Vector2 operator %(Vector2 mod) => Vector2(x % mod.x, y % mod.y); + /// Create a Vector2 with ints as input static Vector2 fromInts(int x, int y) => Vector2(x.toDouble(), y.toDouble()); } From cbe31a566d4d71bbf29aedaf2e635c10a9adfbc9 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 10 Jan 2021 21:32:03 +0100 Subject: [PATCH 12/28] Fix formatting --- lib/src/components/position_component.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index ae6353718d2..841ca8ed654 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -99,6 +99,7 @@ abstract class PositionComponent extends BaseComponent { Rect toRect() => topLeftPosition.toPositionedRect(size); Hull _hull; + /// The list of vertices used for collision detection and to define whether /// a point is inside of the component or not, so that the tap detection etc /// can be more accurately performed. From a81fec4785f439b48fe97bf8ea15550601514bc4 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 10 Jan 2021 21:50:27 +0100 Subject: [PATCH 13/28] Override correct method --- test/base_game_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/base_game_test.dart b/test/base_game_test.dart index e5260af609d..ccf936f1048 100644 --- a/test/base_game_test.dart +++ b/test/base_game_test.dart @@ -43,7 +43,7 @@ class MyComponent extends PositionComponent with Tapable, HasGameRef { } @override - bool checkOverlap(Vector2 v) => true; + bool containsPoint(Vector2 v) => true; @override void onRemove() { From 9ed9049fc947f104ce7a95b8e5b46257f2db0674 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 11 Jan 2021 20:05:50 +0100 Subject: [PATCH 14/28] Use mixin for hitbox --- ...es_hull.dart => main_tapables_hitbox.dart} | 4 +- lib/components.dart | 1 + lib/src/components/mixins/has_hitbox.dart | 76 +++++++++++++++++++ lib/src/components/position_component.dart | 12 ++- test/components/position_component_test.dart | 11 ++- 5 files changed, 95 insertions(+), 9 deletions(-) rename doc/examples/gestures/lib/{main_tapables_hull.dart => main_tapables_hitbox.dart} (92%) create mode 100644 lib/src/components/mixins/has_hitbox.dart diff --git a/doc/examples/gestures/lib/main_tapables_hull.dart b/doc/examples/gestures/lib/main_tapables_hitbox.dart similarity index 92% rename from doc/examples/gestures/lib/main_tapables_hull.dart rename to doc/examples/gestures/lib/main_tapables_hitbox.dart index 620d1cd1591..72b1a87f997 100644 --- a/doc/examples/gestures/lib/main_tapables_hull.dart +++ b/doc/examples/gestures/lib/main_tapables_hitbox.dart @@ -14,11 +14,11 @@ void main() { ); } -class TapablePolygon extends PositionComponent with Tapable { +class TapablePolygon extends PositionComponent with Tapable, HasHitbox { TapablePolygon({Vector2 position}) { size = Vector2.all(100); // The hull is defined as percentages of the full size of the component - hull = [ + hitbox = [ Vector2(-0.5, 0), Vector2(-0.4, 0.3), Vector2(0, 0.5), diff --git a/lib/components.dart b/lib/components.dart index 2f7046d6ccf..cd8f7b5d6bc 100644 --- a/lib/components.dart +++ b/lib/components.dart @@ -16,6 +16,7 @@ export 'joystick.dart'; export 'src/components/mixins/draggable.dart'; export 'src/components/mixins/has_game_ref.dart'; +export 'src/components/mixins/has_hitbox.dart'; export 'src/components/mixins/single_child_particle.dart'; export 'src/components/mixins/tapable.dart'; diff --git a/lib/src/components/mixins/has_hitbox.dart b/lib/src/components/mixins/has_hitbox.dart new file mode 100644 index 00000000000..e2742412721 --- /dev/null +++ b/lib/src/components/mixins/has_hitbox.dart @@ -0,0 +1,76 @@ +import '../../../collision_detection.dart' as collision_detection; +import '../../../components.dart'; + +mixin HasHitbox on PositionComponent { + List vertexScales; + + /// The list of vertices used for collision detection and to define whether + /// a point is inside of the component or not, so that the tap detection etc + /// can be more accurately performed. + /// The hull is defined from the center of the component and with percentages + /// of the size of the component. + /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] + /// This will form a square with a 45 degree angle (pi/4 rad) within the + /// bounding size box. + set hitbox(List vertices) => vertexScales = vertices; + + Iterable _scaledHitbox; + Vector2 _lastScaledSize; + + /// Whether the hull has defined vertices or not + /// An empty list of vertices is also accepted as valid hull + bool hasVertices() => vertexScales != null; + + /// Gives back the hull vectors multiplied by the size of the component and + /// positioned from the component's current center position. + Iterable get scaledHitbox { + if (_lastScaledSize != size || _scaledHitbox == null) { + _lastScaledSize = size; + _scaledHitbox = vertexScales?.map( + (p) => p.clone()..multiply(size), + ); + } + return _scaledHitbox; + } + + // These variables are used to see whether the bounding vertices cache is + // valid or not + Vector2 _lastCachePosition; + Vector2 _lastCacheSize; + double _lastCacheAngle; + bool _hadVertices = false; + List _cachedVertices; + + bool _isBoundingVerticesCacheValid() { + return _lastCacheAngle == angle && + _lastCacheSize == size && + _lastCachePosition == position && + _hadVertices == hasVertices(); + } + + /// Gives back the bounding vertices (bounding box if no hull is specified) + /// represented as a list of points which are the "corners" of the hull/box + /// rotated with [angle]. + List boundingVertices() { + // Use cached bounding vertices if state of the component hasn't changed + if (!_isBoundingVerticesCacheValid()) { + // Uses a the vertices as a hull if defined, otherwise just using the size rectangle + _cachedVertices = scaledHitbox + .map((point) => rotatePoint(center + point)) + .toList(growable: false) ?? + []; + _lastCachePosition = position.clone(); + _lastCacheSize = size.clone(); + _lastCacheAngle = angle; + _hadVertices = hasVertices(); + } + return _cachedVertices; + } + + /// Checks whether the [polygon] represented by the list of [Vector2] contains + /// the [point]. + @override + bool containsPoint(Vector2 point) { + return collision_detection.containsPoint(point, boundingVertices()); + } +} diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 841ca8ed654..e3e5004b737 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -7,9 +7,9 @@ import '../../collision_detection.dart'; import '../anchor.dart'; import '../extensions/offset.dart'; import '../extensions/vector2.dart'; -import '../../game.dart'; import 'base_component.dart'; import 'component.dart'; +import 'mixins/has_hitbox.dart'; /// A [Component] implementation that represents a component that has a /// specific, possibly dynamic position on the screen. @@ -123,6 +123,11 @@ abstract class PositionComponent extends BaseComponent { topLeftPosition = rect.topLeft.toVector2(); } + /// Rotate [point] around component's angle and position (anchor) + Vector2 rotatePoint(Vector2 point) { + return collision_detection.rotatePoint(point, angle, position); + } + @override bool containsPoint(Vector2 point) { return collision_detection.containsPoint( @@ -137,10 +142,11 @@ abstract class PositionComponent extends BaseComponent { @override void renderDebugMode(Canvas canvas) { - if (_hull != null) { + if (this is HasHitbox) { final hullPath = Path() ..addPolygon( - _hull.scaledHull + (this as HasHitbox) + .scaledHitbox .map((point) => (point + size / 2).toOffset()) .toList(), true, diff --git a/test/components/position_component_test.dart b/test/components/position_component_test.dart index bddf4471c76..1418717a940 100644 --- a/test/components/position_component_test.dart +++ b/test/components/position_component_test.dart @@ -1,10 +1,13 @@ import 'dart:math' as math; import 'package:flame/components.dart'; +import 'package:flame/src/components/mixins/has_hitbox.dart'; import 'package:test/test.dart'; class MyComponent extends PositionComponent {} +class MyHitboxComponent extends PositionComponent with HasHitbox {} + void main() { group('PositionComponent overlap test', () { test('overlap', () { @@ -86,11 +89,11 @@ void main() { test('component with hull contains point', () { final size = Vector2(2.0, 2.0); - final PositionComponent component = MyComponent(); + final HasHitbox component = MyHitboxComponent(); component.position = Vector2(1.0, 1.0); component.anchor = Anchor.topLeft; component.size = size; - component.hull = [ + component.hitbox = [ Vector2(0.5, 0), Vector2(0, -0.5), Vector2(-0.5, 0), @@ -103,11 +106,11 @@ void main() { test('component with hull does not contains point', () { final size = Vector2(2.0, 2.0); - final PositionComponent component = MyComponent(); + final HasHitbox component = MyHitboxComponent(); component.position = Vector2(1.0, 1.0); component.anchor = Anchor.topLeft; component.size = size; - component.hull = [ + component.hitbox = [ Vector2(0.5, 0), Vector2(0, -0.5), Vector2(-0.5, 0), From 59111198c36e99666eecd638e98463bb2476d228 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 11 Jan 2021 20:09:27 +0100 Subject: [PATCH 15/28] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d9821399709..b7f9c14d971 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Fix gesture detection bug of children of `PositionComponent` - The `game` argument on `GameWidget` is now required - Add hull capabilities for PositionComponent to make more accurate gestures + - Add hitbox mixin for PositionComponent to make more accurate gestures ## 1.0.0-rc5 - Option for overlays to be already visible on the GameWidget From 10d342e99d957dcb1e6e34d675718307b0ed30c6 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 11 Jan 2021 20:21:57 +0100 Subject: [PATCH 16/28] Rename HasHitbox to Hitbox --- .../gestures/lib/main_tapables_hitbox.dart | 4 +- lib/collision_detection.dart | 87 ------------------- lib/components.dart | 2 +- .../mixins/{has_hitbox.dart => hitbox.dart} | 34 ++++---- lib/src/components/position_component.dart | 10 +-- test/components/position_component_test.dart | 11 ++- 6 files changed, 30 insertions(+), 118 deletions(-) rename lib/src/components/mixins/{has_hitbox.dart => hitbox.dart} (68%) diff --git a/doc/examples/gestures/lib/main_tapables_hitbox.dart b/doc/examples/gestures/lib/main_tapables_hitbox.dart index 72b1a87f997..e7f1e719249 100644 --- a/doc/examples/gestures/lib/main_tapables_hitbox.dart +++ b/doc/examples/gestures/lib/main_tapables_hitbox.dart @@ -14,10 +14,10 @@ void main() { ); } -class TapablePolygon extends PositionComponent with Tapable, HasHitbox { +class TapablePolygon extends PositionComponent with Tapable, Hitbox { TapablePolygon({Vector2 position}) { size = Vector2.all(100); - // The hull is defined as percentages of the full size of the component + // The hitbox is defined as percentages of the full size of the component hitbox = [ Vector2(-0.5, 0), Vector2(-0.4, 0.3), diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart index 4674fa269fc..fee3c0c6846 100644 --- a/lib/collision_detection.dart +++ b/lib/collision_detection.dart @@ -1,93 +1,6 @@ import 'dart:math' as math; import 'src/extensions/vector2.dart'; -import 'src/components/position_component.dart'; - -class Hull { - /// The list of vertices used for collision detection and to define whether - /// a point is inside of the component or not, so that the tap detection etc - /// can be more accurately performed. - /// The hull is defined from the center of the component and with percentages - /// of the size of the component. - /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] - /// This will form a square with a 45 degree angle (pi/4 rad) within the - /// bounding size box. - List vertexScales; - - /// The [PositionComponent] that the hull belongs to - final PositionComponent component; - - Hull(this.component, {this.vertexScales}); - - Iterable _scaledHull; - Vector2 _lastScaledSize; - - /// Whether the hull has defined vertices or not - /// An empty list of vertices is also accepted as valid hull - bool hasVertices() => vertexScales != null; - - /// Gives back the hull vectors multiplied by the size of the component and - /// positioned from the component's current center position. - Iterable get scaledHull { - if (_lastScaledSize != component.size || _scaledHull == null) { - _lastScaledSize = component.size; - _scaledHull = - vertexScales?.map((p) => p.clone()..multiply(component.size)); - } - return _scaledHull; - } - - // These variables are used to see whether the bounding vertices cache is - // valid or not - Vector2 _lastCachePosition; - Vector2 _lastCacheSize; - double _lastCacheAngle; - bool _hadVertices = false; - List _cachedVertices; - - bool _isBoundingVerticesCacheValid(PositionComponent component) { - final position = component.position; - final angle = component.angle; - final size = component.size; - return _lastCacheAngle == angle && - _lastCacheSize == size && - _lastCachePosition == position && - _hadVertices == hasVertices(); - } - - /// Gives back the bounding vertices (bounding box if no hull is specified) - /// represented as a list of points which are the "corners" of the hull/box - /// rotated with [angle]. - List boundingVertices() { - final position = component.position; - final angle = component.angle; - final size = component.size; - final topLeftPosition = component.topLeftPosition; - - // Use cached bounding vertices if state of the component hasn't changed - if (!_isBoundingVerticesCacheValid(component)) { - // Rotate [point] around component angle and position (anchor) - Vector2 rotate(Vector2 point) => rotatePoint(point, angle, position); - - // Uses a the vertices as a hull if defined, otherwise just using the size rectangle - _cachedVertices = scaledHull - ?.map((point) => rotate(component.center + point)) - ?.toList(growable: false) ?? - [ - rotate(topLeftPosition), // Top-left - rotate(topLeftPosition + Vector2(0.0, size.y)), // Bottom-left - rotate(topLeftPosition + size), // Bottom-right - rotate(topLeftPosition + Vector2(size.x, 0.0)), // Top-right - ]; - _lastCachePosition = position; - _lastCacheSize = size; - _lastCacheAngle = angle; - _hadVertices = hasVertices(); - } - - return _cachedVertices; - } -} /// Checks whether the [polygon] represented by the list of [Vector2] contains /// the [point]. diff --git a/lib/components.dart b/lib/components.dart index cd8f7b5d6bc..b6a4a48b6c0 100644 --- a/lib/components.dart +++ b/lib/components.dart @@ -16,7 +16,7 @@ export 'joystick.dart'; export 'src/components/mixins/draggable.dart'; export 'src/components/mixins/has_game_ref.dart'; -export 'src/components/mixins/has_hitbox.dart'; +export 'src/components/mixins/hitbox.dart'; export 'src/components/mixins/single_child_particle.dart'; export 'src/components/mixins/tapable.dart'; diff --git a/lib/src/components/mixins/has_hitbox.dart b/lib/src/components/mixins/hitbox.dart similarity index 68% rename from lib/src/components/mixins/has_hitbox.dart rename to lib/src/components/mixins/hitbox.dart index e2742412721..86c1195977a 100644 --- a/lib/src/components/mixins/has_hitbox.dart +++ b/lib/src/components/mixins/hitbox.dart @@ -1,32 +1,34 @@ -import '../../../collision_detection.dart' as collision_detection; + import '../../../components.dart'; +import '../../../collision_detection.dart' as collision_detection; -mixin HasHitbox on PositionComponent { - List vertexScales; +mixin Hitbox on PositionComponent { + List _hitbox; /// The list of vertices used for collision detection and to define whether /// a point is inside of the component or not, so that the tap detection etc /// can be more accurately performed. - /// The hull is defined from the center of the component and with percentages - /// of the size of the component. + /// The hitbox is defined from the center of the component and with + /// percentages of the size of the component. /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] /// This will form a square with a 45 degree angle (pi/4 rad) within the /// bounding size box. - set hitbox(List vertices) => vertexScales = vertices; + set hitbox(List vertices) => _hitbox = vertices; + List get hitbox => _hitbox ?? []; + + /// Whether the hitbox has defined vertices or not + /// An empty list of vertices is also accepted as valid hitbox + bool hasVertices() => _hitbox?.isNotEmpty ?? false; Iterable _scaledHitbox; Vector2 _lastScaledSize; - /// Whether the hull has defined vertices or not - /// An empty list of vertices is also accepted as valid hull - bool hasVertices() => vertexScales != null; - - /// Gives back the hull vectors multiplied by the size of the component and + /// Gives back the hitbox vectors multiplied by the size of the component and /// positioned from the component's current center position. Iterable get scaledHitbox { if (_lastScaledSize != size || _scaledHitbox == null) { _lastScaledSize = size; - _scaledHitbox = vertexScales?.map( + _scaledHitbox = _hitbox?.map( (p) => p.clone()..multiply(size), ); } @@ -48,13 +50,11 @@ mixin HasHitbox on PositionComponent { _hadVertices == hasVertices(); } - /// Gives back the bounding vertices (bounding box if no hull is specified) - /// represented as a list of points which are the "corners" of the hull/box - /// rotated with [angle]. + /// Gives back the bounding vertices represented as a list of points which + /// are the "corners" of the hitbox rotated with [angle]. List boundingVertices() { // Use cached bounding vertices if state of the component hasn't changed if (!_isBoundingVerticesCacheValid()) { - // Uses a the vertices as a hull if defined, otherwise just using the size rectangle _cachedVertices = scaledHitbox .map((point) => rotatePoint(center + point)) .toList(growable: false) ?? @@ -67,7 +67,7 @@ mixin HasHitbox on PositionComponent { return _cachedVertices; } - /// Checks whether the [polygon] represented by the list of [Vector2] contains + /// Checks whether the hitbox represented by the list of [Vector2] contains /// the [point]. @override bool containsPoint(Vector2 point) { diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index e3e5004b737..382bbcabc51 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -9,7 +9,7 @@ import '../extensions/offset.dart'; import '../extensions/vector2.dart'; import 'base_component.dart'; import 'component.dart'; -import 'mixins/has_hitbox.dart'; +import 'mixins/hitbox.dart'; /// A [Component] implementation that represents a component that has a /// specific, possibly dynamic position on the screen. @@ -142,16 +142,16 @@ abstract class PositionComponent extends BaseComponent { @override void renderDebugMode(Canvas canvas) { - if (this is HasHitbox) { - final hullPath = Path() + if (this is Hitbox) { + final hitboxPath = Path() ..addPolygon( - (this as HasHitbox) + (this as Hitbox) .scaledHitbox .map((point) => (point + size / 2).toOffset()) .toList(), true, ); - canvas.drawPath(hullPath, debugPaint); + canvas.drawPath(hitboxPath, debugPaint); } canvas.drawRect(size.toRect(), debugPaint); debugTextConfig.render( diff --git a/test/components/position_component_test.dart b/test/components/position_component_test.dart index 1418717a940..2bf1095663c 100644 --- a/test/components/position_component_test.dart +++ b/test/components/position_component_test.dart @@ -1,12 +1,11 @@ import 'dart:math' as math; import 'package:flame/components.dart'; -import 'package:flame/src/components/mixins/has_hitbox.dart'; import 'package:test/test.dart'; class MyComponent extends PositionComponent {} -class MyHitboxComponent extends PositionComponent with HasHitbox {} +class MyHitboxComponent extends PositionComponent with Hitbox {} void main() { group('PositionComponent overlap test', () { @@ -87,9 +86,9 @@ void main() { expect(component.containsPoint(point), true); }); - test('component with hull contains point', () { + test('component with hitbox contains point', () { final size = Vector2(2.0, 2.0); - final HasHitbox component = MyHitboxComponent(); + final Hitbox component = MyHitboxComponent(); component.position = Vector2(1.0, 1.0); component.anchor = Anchor.topLeft; component.size = size; @@ -104,9 +103,9 @@ void main() { expect(component.containsPoint(point), true); }); - test('component with hull does not contains point', () { + test('component with hitbox does not contains point', () { final size = Vector2(2.0, 2.0); - final HasHitbox component = MyHitboxComponent(); + final Hitbox component = MyHitboxComponent(); component.position = Vector2(1.0, 1.0); component.anchor = Anchor.topLeft; component.size = size; From 4148f08eef1eec2c2b064704c57b4d7a6a364ee8 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 11 Jan 2021 20:49:25 +0100 Subject: [PATCH 17/28] Clarified names --- .../gestures/lib/main_tapables_hitbox.dart | 2 +- lib/src/components/mixins/hitbox.dart | 44 +++++++++---------- lib/src/components/position_component.dart | 2 +- test/components/position_component_test.dart | 4 +- 4 files changed, 25 insertions(+), 27 deletions(-) diff --git a/doc/examples/gestures/lib/main_tapables_hitbox.dart b/doc/examples/gestures/lib/main_tapables_hitbox.dart index e7f1e719249..d29759acd67 100644 --- a/doc/examples/gestures/lib/main_tapables_hitbox.dart +++ b/doc/examples/gestures/lib/main_tapables_hitbox.dart @@ -18,7 +18,7 @@ class TapablePolygon extends PositionComponent with Tapable, Hitbox { TapablePolygon({Vector2 position}) { size = Vector2.all(100); // The hitbox is defined as percentages of the full size of the component - hitbox = [ + shape = [ Vector2(-0.5, 0), Vector2(-0.4, 0.3), Vector2(0, 0.5), diff --git a/lib/src/components/mixins/hitbox.dart b/lib/src/components/mixins/hitbox.dart index 86c1195977a..3d6da0c33bb 100644 --- a/lib/src/components/mixins/hitbox.dart +++ b/lib/src/components/mixins/hitbox.dart @@ -3,7 +3,7 @@ import '../../../components.dart'; import '../../../collision_detection.dart' as collision_detection; mixin Hitbox on PositionComponent { - List _hitbox; + List _shape; /// The list of vertices used for collision detection and to define whether /// a point is inside of the component or not, so that the tap detection etc @@ -13,26 +13,24 @@ mixin Hitbox on PositionComponent { /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] /// This will form a square with a 45 degree angle (pi/4 rad) within the /// bounding size box. - set hitbox(List vertices) => _hitbox = vertices; - List get hitbox => _hitbox ?? []; + set shape(List vertices) => _shape = vertices; + List get shape => _shape ?? []; - /// Whether the hitbox has defined vertices or not - /// An empty list of vertices is also accepted as valid hitbox - bool hasVertices() => _hitbox?.isNotEmpty ?? false; + /// Whether the hitbox shape has defined vertices and is not an empty list + bool hasShape() => _shape?.isNotEmpty ?? false; - Iterable _scaledHitbox; + Iterable _scaledShape; Vector2 _lastScaledSize; - /// Gives back the hitbox vectors multiplied by the size of the component and - /// positioned from the component's current center position. - Iterable get scaledHitbox { - if (_lastScaledSize != size || _scaledHitbox == null) { + /// Gives back the shape vectors multiplied by the size of the component + Iterable get scaledShape { + if (_lastScaledSize != size || _scaledShape == null) { _lastScaledSize = size; - _scaledHitbox = _hitbox?.map( + _scaledShape = _shape?.map( (p) => p.clone()..multiply(size), ); } - return _scaledHitbox; + return _scaledShape; } // These variables are used to see whether the bounding vertices cache is @@ -40,37 +38,37 @@ mixin Hitbox on PositionComponent { Vector2 _lastCachePosition; Vector2 _lastCacheSize; double _lastCacheAngle; - bool _hadVertices = false; - List _cachedVertices; + bool _hadShape = false; + List _cachedHitbox; - bool _isBoundingVerticesCacheValid() { + bool _isHitboxCacheValid() { return _lastCacheAngle == angle && _lastCacheSize == size && _lastCachePosition == position && - _hadVertices == hasVertices(); + _hadShape == hasShape(); } /// Gives back the bounding vertices represented as a list of points which /// are the "corners" of the hitbox rotated with [angle]. - List boundingVertices() { + List get hitbox { // Use cached bounding vertices if state of the component hasn't changed - if (!_isBoundingVerticesCacheValid()) { - _cachedVertices = scaledHitbox + if (!_isHitboxCacheValid()) { + _cachedHitbox = scaledShape .map((point) => rotatePoint(center + point)) .toList(growable: false) ?? []; _lastCachePosition = position.clone(); _lastCacheSize = size.clone(); _lastCacheAngle = angle; - _hadVertices = hasVertices(); + _hadShape = hasShape(); } - return _cachedVertices; + return _cachedHitbox; } /// Checks whether the hitbox represented by the list of [Vector2] contains /// the [point]. @override bool containsPoint(Vector2 point) { - return collision_detection.containsPoint(point, boundingVertices()); + return collision_detection.containsPoint(point, hitbox); } } diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 382bbcabc51..cba425ed005 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -146,7 +146,7 @@ abstract class PositionComponent extends BaseComponent { final hitboxPath = Path() ..addPolygon( (this as Hitbox) - .scaledHitbox + .scaledShape .map((point) => (point + size / 2).toOffset()) .toList(), true, diff --git a/test/components/position_component_test.dart b/test/components/position_component_test.dart index 2bf1095663c..a9ae1fc4c90 100644 --- a/test/components/position_component_test.dart +++ b/test/components/position_component_test.dart @@ -92,7 +92,7 @@ void main() { component.position = Vector2(1.0, 1.0); component.anchor = Anchor.topLeft; component.size = size; - component.hitbox = [ + component.shape = [ Vector2(0.5, 0), Vector2(0, -0.5), Vector2(-0.5, 0), @@ -109,7 +109,7 @@ void main() { component.position = Vector2(1.0, 1.0); component.anchor = Anchor.topLeft; component.size = size; - component.hitbox = [ + component.shape = [ Vector2(0.5, 0), Vector2(0, -0.5), Vector2(-0.5, 0), From b7a12178d044e7ca34af9ab62c7e3346bf40de83 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 11 Jan 2021 20:53:38 +0100 Subject: [PATCH 18/28] Center to edge is considered as 1.0 --- .../gestures/lib/main_tapables_hitbox.dart | 16 ++++++++-------- lib/src/components/mixins/hitbox.dart | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/doc/examples/gestures/lib/main_tapables_hitbox.dart b/doc/examples/gestures/lib/main_tapables_hitbox.dart index d29759acd67..0dc85334765 100644 --- a/doc/examples/gestures/lib/main_tapables_hitbox.dart +++ b/doc/examples/gestures/lib/main_tapables_hitbox.dart @@ -19,14 +19,14 @@ class TapablePolygon extends PositionComponent with Tapable, Hitbox { size = Vector2.all(100); // The hitbox is defined as percentages of the full size of the component shape = [ - Vector2(-0.5, 0), - Vector2(-0.4, 0.3), - Vector2(0, 0.5), - Vector2(0.3, 0.45), - Vector2(0.5, 0), - Vector2(0.3, -0.4), - Vector2(0, -0.5), - Vector2(-0.4, -0.4), + Vector2(-1.0, 0.0), + Vector2(-0.8, 0.6), + Vector2(0.0, 1.0), + Vector2(0.6, 0.9), + Vector2(1.0, 0.0), + Vector2(0.6, -0.8), + Vector2(0, -1.0), + Vector2(-0.8, -0.8), ]; this.position = position ?? Vector2.all(150); } diff --git a/lib/src/components/mixins/hitbox.dart b/lib/src/components/mixins/hitbox.dart index 3d6da0c33bb..6aa4c8a6b43 100644 --- a/lib/src/components/mixins/hitbox.dart +++ b/lib/src/components/mixins/hitbox.dart @@ -10,7 +10,7 @@ mixin Hitbox on PositionComponent { /// can be more accurately performed. /// The hitbox is defined from the center of the component and with /// percentages of the size of the component. - /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] + /// Example: [[1.0, 0.0], [0.0, 1.0], [-1.0, 0.0], [0.0, -1.0]] /// This will form a square with a 45 degree angle (pi/4 rad) within the /// bounding size box. set shape(List vertices) => _shape = vertices; @@ -27,7 +27,7 @@ mixin Hitbox on PositionComponent { if (_lastScaledSize != size || _scaledShape == null) { _lastScaledSize = size; _scaledShape = _shape?.map( - (p) => p.clone()..multiply(size), + (p) => p.clone()..multiply(size / 2), ); } return _scaledShape; From 63964da8e17ad47f2e9edd7dd01c8391896f50aa Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 11 Jan 2021 21:00:04 +0100 Subject: [PATCH 19/28] Fix test --- test/components/position_component_test.dart | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/components/position_component_test.dart b/test/components/position_component_test.dart index a9ae1fc4c90..37f1de03bc5 100644 --- a/test/components/position_component_test.dart +++ b/test/components/position_component_test.dart @@ -93,10 +93,10 @@ void main() { component.anchor = Anchor.topLeft; component.size = size; component.shape = [ - Vector2(0.5, 0), - Vector2(0, -0.5), - Vector2(-0.5, 0), - Vector2(0, 0.5), + Vector2(1, 0), + Vector2(0, -1), + Vector2(-1, 0), + Vector2(0, 1), ]; final point = component.position + component.size / 4; @@ -110,10 +110,10 @@ void main() { component.anchor = Anchor.topLeft; component.size = size; component.shape = [ - Vector2(0.5, 0), - Vector2(0, -0.5), - Vector2(-0.5, 0), - Vector2(0, 0.5), + Vector2(1, 0), + Vector2(0, -1), + Vector2(-1, 0), + Vector2(0, 1), ]; final point = Vector2(1.1, 1.1); From 81f3402b98638b20f879908df72a499d337e40e1 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 12 Jan 2021 21:48:59 +0100 Subject: [PATCH 20/28] Add spaces within braces --- doc/examples/gestures/lib/main_tapables_hitbox.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/gestures/lib/main_tapables_hitbox.dart b/doc/examples/gestures/lib/main_tapables_hitbox.dart index 0dc85334765..3ee143fdcea 100644 --- a/doc/examples/gestures/lib/main_tapables_hitbox.dart +++ b/doc/examples/gestures/lib/main_tapables_hitbox.dart @@ -15,7 +15,7 @@ void main() { } class TapablePolygon extends PositionComponent with Tapable, Hitbox { - TapablePolygon({Vector2 position}) { + TapablePolygon({ Vector2 position }) { size = Vector2.all(100); // The hitbox is defined as percentages of the full size of the component shape = [ From 05c9d53982b0914fb8744786a08d1d2efb3f9360 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Tue, 12 Jan 2021 22:17:27 +0100 Subject: [PATCH 21/28] Removed extra spaces in the braces --- doc/examples/gestures/lib/main_tapables_hitbox.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/examples/gestures/lib/main_tapables_hitbox.dart b/doc/examples/gestures/lib/main_tapables_hitbox.dart index 3ee143fdcea..0dc85334765 100644 --- a/doc/examples/gestures/lib/main_tapables_hitbox.dart +++ b/doc/examples/gestures/lib/main_tapables_hitbox.dart @@ -15,7 +15,7 @@ void main() { } class TapablePolygon extends PositionComponent with Tapable, Hitbox { - TapablePolygon({ Vector2 position }) { + TapablePolygon({Vector2 position}) { size = Vector2.all(100); // The hitbox is defined as percentages of the full size of the component shape = [ From fce479dc97bdeb2c79a35f44e901ec81ab5325ee Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 13 Jan 2021 20:51:09 +0100 Subject: [PATCH 22/28] Add hitbox docs --- doc/input.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/input.md b/doc/input.md index 40c11455909..06bd5b353a9 100644 --- a/doc/input.md +++ b/doc/input.md @@ -207,6 +207,16 @@ class MyGame extends BaseGame with HasDraggableComponents { Warning: `HasDraggableComponents` uses an advanced gesture detector under the hood and as explained further up on this page, shouldn't be used alongside basic detectors. +## Hitbox +The `Hitbox` mixin is used to make detection of gestures on top of your `PositionComponent`s more +accurate. Say that you have a fairly round rock as a `SpriteComponent` for example, then you don't +want to register input that is in the corner of the image where the rock is not displayed. Then you +can use the `Hitbox` mixin to define a more accurate polygon for which the input should be within +for the event to be counted on your component. + +An example of you to use it can be seen +[here](https://github.com/flame-engine/flame/tree/master/doc/examples/main_tapables_hitbox.dart). + ## Keyboard Flame provides a simple way to access Flutter's features regarding accessing Keyboard input events. From a35cd188f1e86d7aef56f681181a396921b63524 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 13 Jan 2021 20:53:32 +0100 Subject: [PATCH 23/28] Fix link --- doc/input.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/input.md b/doc/input.md index 06bd5b353a9..06caab68395 100644 --- a/doc/input.md +++ b/doc/input.md @@ -215,7 +215,7 @@ can use the `Hitbox` mixin to define a more accurate polygon for which the input for the event to be counted on your component. An example of you to use it can be seen -[here](https://github.com/flame-engine/flame/tree/master/doc/examples/main_tapables_hitbox.dart). +[here](https://github.com/flame-engine/flame/blob/master/doc/examples/gestures/lib/main_tapables_hitbox.dart). ## Keyboard From 826ab88726e0b729b3e93bea4b25ed0fb274e306 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 17 Jan 2021 18:17:13 +0100 Subject: [PATCH 24/28] Moved point rotation to Vector2 extension --- lib/collision_detection.dart | 12 ------------ lib/src/components/position_component.dart | 2 +- lib/src/extensions/vector2.dart | 18 +++++++++++++----- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/lib/collision_detection.dart b/lib/collision_detection.dart index fee3c0c6846..8436a2cb463 100644 --- a/lib/collision_detection.dart +++ b/lib/collision_detection.dart @@ -18,15 +18,3 @@ bool containsPoint(Vector2 point, List polygon) { } return true; } - -// Rotates the [point] with [angle] around [position] -Vector2 rotatePoint(Vector2 point, double angle, Vector2 position) { - return Vector2( - math.cos(angle) * (point.x - position.x) - - math.sin(angle) * (point.y - position.y) + - position.x, - math.sin(angle) * (point.x - position.x) + - math.cos(angle) * (point.y - position.y) + - position.y, - ); -} diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index cba425ed005..3b8d989534a 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -125,7 +125,7 @@ abstract class PositionComponent extends BaseComponent { /// Rotate [point] around component's angle and position (anchor) Vector2 rotatePoint(Vector2 point) { - return collision_detection.rotatePoint(point, angle, position); + return point.clone()..rotate(angle, center: position); } @override diff --git a/lib/src/extensions/vector2.dart b/lib/src/extensions/vector2.dart index b1685cd55f7..7c759a6c7ac 100644 --- a/lib/src/extensions/vector2.dart +++ b/lib/src/extensions/vector2.dart @@ -28,11 +28,19 @@ extension Vector2Extension on Vector2 { } /// Rotates the [Vector2] with [angle] in radians - void rotate(double angle) { - setValues( - x * cos(angle) - y * sin(angle), - x * sin(angle) + y * cos(angle), - ); + /// rotates around [center] if it is defined + void rotate(double angle, {Vector2 center}) { + if (center == null) { + setValues( + x * cos(angle) - y * sin(angle), + x * sin(angle) + y * cos(angle), + ); + } else { + setValues( + cos(angle) * (x - center.x) - sin(angle) * (y - center.y) + center.x, + sin(angle) * (x - center.x) + cos(angle) * (y - center.y) + center.y, + ); + } } /// Changes the [length] of the vector to the length provided, without changing direction. From 1a28824b0b3f3bc040e241d33fd4fa9cada674c3 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Sun, 17 Jan 2021 18:32:21 +0100 Subject: [PATCH 25/28] Render hitbox within extension --- lib/src/components/mixins/hitbox.dart | 10 ++++++++++ lib/src/components/position_component.dart | 10 +--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/lib/src/components/mixins/hitbox.dart b/lib/src/components/mixins/hitbox.dart index 6aa4c8a6b43..803264666fa 100644 --- a/lib/src/components/mixins/hitbox.dart +++ b/lib/src/components/mixins/hitbox.dart @@ -1,3 +1,4 @@ +import 'dart:ui'; import '../../../components.dart'; import '../../../collision_detection.dart' as collision_detection; @@ -33,6 +34,15 @@ mixin Hitbox on PositionComponent { return _scaledShape; } + void renderContour(Canvas canvas) { + final hitboxPath = Path() + ..addPolygon( + scaledShape.map((point) => (point + size / 2).toOffset()).toList(), + true, + ); + canvas.drawPath(hitboxPath, debugPaint); + } + // These variables are used to see whether the bounding vertices cache is // valid or not Vector2 _lastCachePosition; diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 3b8d989534a..9e60adcbdef 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -143,15 +143,7 @@ abstract class PositionComponent extends BaseComponent { @override void renderDebugMode(Canvas canvas) { if (this is Hitbox) { - final hitboxPath = Path() - ..addPolygon( - (this as Hitbox) - .scaledShape - .map((point) => (point + size / 2).toOffset()) - .toList(), - true, - ); - canvas.drawPath(hitboxPath, debugPaint); + (this as Hitbox).renderContour(canvas); } canvas.drawRect(size.toRect(), debugPaint); debugTextConfig.render( From d193c932859c932ba05493deb948f9e10e57f5aa Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 20 Jan 2021 23:09:06 +0100 Subject: [PATCH 26/28] Fix rebase --- lib/src/components/mixins/collidable.dart | 31 ---------------------- lib/src/components/position_component.dart | 29 ++++++-------------- 2 files changed, 8 insertions(+), 52 deletions(-) delete mode 100644 lib/src/components/mixins/collidable.dart diff --git a/lib/src/components/mixins/collidable.dart b/lib/src/components/mixins/collidable.dart deleted file mode 100644 index 617ea77c72c..00000000000 --- a/lib/src/components/mixins/collidable.dart +++ /dev/null @@ -1,31 +0,0 @@ -import '../position_component.dart'; -import '../../extensions/vector2.dart'; -import '../../game/base_game.dart'; - -mixin Collidable on PositionComponent { - void setHullFromSize() { - if (size == null || (size?.length ?? 0) == 0) { - hull = []; - } else { - hull = [ - size / 2, - (size / 2)..multiply(Vector2(1, -1)), - (size / 2)..multiply(Vector2(-1, -1)), - (size / 2)..multiply(Vector2(-1, 1)), - ]; - } - } - - /// Override this to define what will happen to your component when it - /// collides with another component - bool collisionCallback( - PositionComponent otherComponent, - List collisionPoints, - ); - - /// Override this to define what will happen to your component when it - /// collides with a wall - bool wallCollisionCallback(List collisionPoints); -} - -mixin HasCollidableComponents on BaseGame {} diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 9e60adcbdef..79789679625 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -98,23 +98,6 @@ abstract class PositionComponent extends BaseComponent { /// Relative because it might be translated by their parents (which is not considered here). Rect toRect() => topLeftPosition.toPositionedRect(size); - Hull _hull; - - /// The list of vertices used for collision detection and to define whether - /// a point is inside of the component or not, so that the tap detection etc - /// can be more accurately performed. - /// The hull is defined from the center of the component and with percentages - /// of the size of the component. - /// Example: [[0.5, 0.0], [0.0, 0.5], [-0.5, 0.0], [0.0, -0.5]] - /// This will form a square with a 45 degree angle (pi/4 rad) within the - /// bounding size box. - set hull(List vertices) => _hull.vertexScales = vertices; - - @mustCallSuper - PositionComponent() { - _hull = Hull(this); - } - /// Mutates position and size using the provided [rect] as basis. /// This is a relative rect, same definition that [toRect] use /// (therefore both methods are compatible, i.e. setByRect ∘ toRect = identity). @@ -130,10 +113,14 @@ abstract class PositionComponent extends BaseComponent { @override bool containsPoint(Vector2 point) { - return collision_detection.containsPoint( - point - absoluteCanvasPosition, - _hull.boundingVertices(), - ); + final corners = [ + rotatePoint(absoluteTopLeftPosition), // Top-left + rotatePoint(absoluteTopLeftPosition + Vector2(0.0, size.y)), // Bottom-left + rotatePoint(absoluteTopLeftPosition + size), // Bottom-right + rotatePoint(absoluteTopLeftPosition + Vector2(size.x, 0.0)), // Top-right + ]; + + return collision_detection.containsPoint(point, corners); } double angleTo(PositionComponent c) => position.angleTo(c.position); From c4749b7874f90a1b6591b220e6fb3e6bb1dc4972 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 20 Jan 2021 23:14:07 +0100 Subject: [PATCH 27/28] Fix rebase --- CHANGELOG.md | 1 - lib/{ => src}/collision_detection.dart | 4 +--- lib/src/components/mixins/hitbox.dart | 2 +- lib/src/components/position_component.dart | 5 +---- 4 files changed, 3 insertions(+), 9 deletions(-) rename lib/{ => src}/collision_detection.dart (89%) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7f9c14d971..c8a9776abc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,6 @@ - Move files to comply with the dart package layout convention - Fix gesture detection bug of children of `PositionComponent` - The `game` argument on `GameWidget` is now required - - Add hull capabilities for PositionComponent to make more accurate gestures - Add hitbox mixin for PositionComponent to make more accurate gestures ## 1.0.0-rc5 diff --git a/lib/collision_detection.dart b/lib/src/collision_detection.dart similarity index 89% rename from lib/collision_detection.dart rename to lib/src/collision_detection.dart index 8436a2cb463..92d43c40f1a 100644 --- a/lib/collision_detection.dart +++ b/lib/src/collision_detection.dart @@ -1,6 +1,4 @@ -import 'dart:math' as math; - -import 'src/extensions/vector2.dart'; +import '../extensions.dart'; /// Checks whether the [polygon] represented by the list of [Vector2] contains /// the [point]. diff --git a/lib/src/components/mixins/hitbox.dart b/lib/src/components/mixins/hitbox.dart index 803264666fa..7258171a7fb 100644 --- a/lib/src/components/mixins/hitbox.dart +++ b/lib/src/components/mixins/hitbox.dart @@ -1,7 +1,7 @@ import 'dart:ui'; import '../../../components.dart'; -import '../../../collision_detection.dart' as collision_detection; +import '../../collision_detection.dart' as collision_detection; mixin Hitbox on PositionComponent { List _shape; diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 79789679625..3312c727353 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -1,9 +1,6 @@ import 'dart:ui' hide Offset; -import 'package:meta/meta.dart'; - -import '../../collision_detection.dart' as collision_detection; -import '../../collision_detection.dart'; +import '../collision_detection.dart' as collision_detection; import '../anchor.dart'; import '../extensions/offset.dart'; import '../extensions/vector2.dart'; From 666d4c702fa5a5504203cd62ab195b840b5f302e Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Wed, 20 Jan 2021 23:14:37 +0100 Subject: [PATCH 28/28] Fix formatting --- lib/src/components/position_component.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/components/position_component.dart b/lib/src/components/position_component.dart index 3312c727353..63ac4846e34 100644 --- a/lib/src/components/position_component.dart +++ b/lib/src/components/position_component.dart @@ -112,7 +112,8 @@ abstract class PositionComponent extends BaseComponent { bool containsPoint(Vector2 point) { final corners = [ rotatePoint(absoluteTopLeftPosition), // Top-left - rotatePoint(absoluteTopLeftPosition + Vector2(0.0, size.y)), // Bottom-left + rotatePoint( + absoluteTopLeftPosition + Vector2(0.0, size.y)), // Bottom-left rotatePoint(absoluteTopLeftPosition + size), // Bottom-right rotatePoint(absoluteTopLeftPosition + Vector2(size.x, 0.0)), // Top-right ];