From 76271ceef04264ec8fa5c39a23f43d638d731694 Mon Sep 17 00:00:00 2001 From: Luan Nico Date: Sun, 16 Jul 2023 10:24:57 +0200 Subject: [PATCH 01/15] feat: Add Rectangle.fromLTWH and Rect.toFlameRectangle utility methods (#2604) # Description Add a `Rectangle.fromLTWH` and `Rect.toFlameRectangle` utility methods to more easily create and convert between different rectangle represenations. --- doc/flame/other/util.md | 1 + .../src/experimental/geometry/shapes/rectangle.dart | 3 +++ packages/flame/lib/src/extensions/rect.dart | 5 +++++ .../experimental/geometry/shapes/rectangle_test.dart | 10 ++++++++++ packages/flame/test/extensions/rect_test.dart | 12 ++++++++++++ 5 files changed, 31 insertions(+) diff --git a/doc/flame/other/util.md b/doc/flame/other/util.md index 909cf5d94bf..11a36ee821d 100644 --- a/doc/flame/other/util.md +++ b/doc/flame/other/util.md @@ -251,6 +251,7 @@ Methods: - `intersectsSegment`; Whether the segment formed by two `Vector2`s intersects this `Rect`. - `intersectsLineSegment`: Whether the `LineSegment` intersects the `Rect`. - `toVertices`: Turns the four corners of the `Rect` into a list of `Vector2`. +- `toFlameRectangle`: Converts this `Rect` into a Flame `Rectangle`. - `toMathRectangle`: Converts this `Rect` into a `math.Rectangle`. - `toGeometryRectangle`: Converts this `Rect` into a `Rectangle` from flame-geom. - `transform`: Transforms the `Rect` using a `Matrix4`. diff --git a/packages/flame/lib/src/experimental/geometry/shapes/rectangle.dart b/packages/flame/lib/src/experimental/geometry/shapes/rectangle.dart index 7c732cf7a78..41bb076dc77 100644 --- a/packages/flame/lib/src/experimental/geometry/shapes/rectangle.dart +++ b/packages/flame/lib/src/experimental/geometry/shapes/rectangle.dart @@ -35,6 +35,9 @@ class Rectangle extends Shape { } } + Rectangle.fromLTWH(double left, double top, double width, double height) + : this.fromLTRB(left, top, left + width, top + height); + /// Constructs a [Rectangle] from two opposite corners. The points can be in /// any disposition to each other. factory Rectangle.fromPoints(Vector2 a, Vector2 b) => diff --git a/packages/flame/lib/src/extensions/rect.dart b/packages/flame/lib/src/extensions/rect.dart index a5c01009b64..462a4090580 100644 --- a/packages/flame/lib/src/extensions/rect.dart +++ b/packages/flame/lib/src/extensions/rect.dart @@ -2,6 +2,7 @@ import 'dart:math' show min, max; import 'dart:math' as math; import 'dart:ui'; +import 'package:flame/experimental.dart' as flame show Rectangle; import 'package:flame/geometry.dart'; import 'package:flame/src/extensions/matrix4.dart'; import 'package:flame/src/extensions/offset.dart'; @@ -19,6 +20,10 @@ extension RectExtension on Rect { /// Converts this [Rect] into a [math.Rectangle]. math.Rectangle toMathRectangle() => math.Rectangle(left, top, width, height); + /// Converts this [Rect] into a [flame.Rectangle]. + flame.Rectangle toFlameRectangle() => + flame.Rectangle.fromLTWH(left, top, width, height); + /// Converts this [Rect] into a [RectangleComponent]. RectangleComponent toRectangleComponent() { return RectangleComponent.fromRect(this); diff --git a/packages/flame/test/experimental/geometry/shapes/rectangle_test.dart b/packages/flame/test/experimental/geometry/shapes/rectangle_test.dart index b644cfcbce3..3946b25e2bf 100644 --- a/packages/flame/test/experimental/geometry/shapes/rectangle_test.dart +++ b/packages/flame/test/experimental/geometry/shapes/rectangle_test.dart @@ -24,6 +24,16 @@ void main() { expect('$rectangle', 'Rectangle([4.0, 0.0], [9.0, 12.0])'); }); + test('simple rectangle from LTWH', () { + final rectangle = Rectangle.fromLTWH(2, 2, 5, 7); + expect(rectangle.left, 2); + expect(rectangle.top, 2); + expect(rectangle.right, 7); + expect(rectangle.bottom, 9); + expect(rectangle.width, 5); + expect(rectangle.height, 7); + }); + test('rectangle with inverted left-right edges', () { final rectangle = Rectangle.fromLTRB(3, 4, 0, 10); expect(rectangle.left, 0); diff --git a/packages/flame/test/extensions/rect_test.dart b/packages/flame/test/extensions/rect_test.dart index 40b94c82a48..1cc76539c35 100644 --- a/packages/flame/test/extensions/rect_test.dart +++ b/packages/flame/test/extensions/rect_test.dart @@ -22,6 +22,7 @@ void main() { expect(vector.x, rect.width); expect(vector.y, rect.height); }); + test('test from ui Rect to math Rectangle', () { const r1 = Rect.fromLTWH(0, 10, 20, 30); final r2 = r1.toMathRectangle(); @@ -33,6 +34,17 @@ void main() { expect(r2.height, r1.height); }); + test('test from ui Rect to Flame Rectangle', () { + const r1 = Rect.fromLTWH(0, 10, 20, 30); + final r2 = r1.toFlameRectangle(); + expect(r2.top, r1.top); + expect(r2.bottom, r1.bottom); + expect(r2.left, r1.left); + expect(r2.right, r1.right); + expect(r2.width, r1.width); + expect(r2.height, r1.height); + }); + test('test from math Rectangle to ui Rect', () { const r1 = math.Rectangle(0, 10, 20, 30); final r2 = r1.toRect(); From 1f9f35093b3b90113e32a36e1103b87246212fa4 Mon Sep 17 00:00:00 2001 From: Luan Nico Date: Sun, 16 Jul 2023 10:37:18 +0200 Subject: [PATCH 02/15] feat: Add a midpoint getter to LineSegment (#2605) # Description Add a `LineSegment.midpoint` utility getter. --- packages/flame/lib/src/geometry/line.dart | 4 +++- packages/flame/lib/src/geometry/line_segment.dart | 2 ++ .../flame/test/geometry/line_segment_test.dart | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 packages/flame/test/geometry/line_segment_test.dart diff --git a/packages/flame/lib/src/geometry/line.dart b/packages/flame/lib/src/geometry/line.dart index 69e1d316a77..bf48317d869 100644 --- a/packages/flame/lib/src/geometry/line.dart +++ b/packages/flame/lib/src/geometry/line.dart @@ -2,7 +2,9 @@ import 'dart:math'; import 'package:flame/extensions.dart'; -/// This represents a line on the ax + by = c form +/// An infinite line on the 2D Cartesian space, represented in the form +/// of ax + by = c. +/// /// If you just want to represent a part of a line, look into LineSegment. class Line { final double a; diff --git a/packages/flame/lib/src/geometry/line_segment.dart b/packages/flame/lib/src/geometry/line_segment.dart index f8ddd30a1c7..2fbaf1b83bd 100644 --- a/packages/flame/lib/src/geometry/line_segment.dart +++ b/packages/flame/lib/src/geometry/line_segment.dart @@ -11,6 +11,8 @@ class LineSegment { factory LineSegment.zero() => LineSegment(Vector2.zero(), Vector2.zero()); + Vector2 get midpoint => (from + to)..scale(0.5); + /// Returns an empty list if there are no intersections between the segments /// If the segments are concurrent, the intersecting point is returned as a /// list with a single point diff --git a/packages/flame/test/geometry/line_segment_test.dart b/packages/flame/test/geometry/line_segment_test.dart new file mode 100644 index 00000000000..1e0e0c88863 --- /dev/null +++ b/packages/flame/test/geometry/line_segment_test.dart @@ -0,0 +1,15 @@ +import 'package:flame/components.dart'; +import 'package:flame/geometry.dart'; +import 'package:test/test.dart'; + +void main() { + group('LineSegment', () { + test('midpoint', () { + final lineSegment1 = LineSegment(Vector2.zero(), Vector2.all(2)); + expect(lineSegment1.midpoint, Vector2.all(1)); + + final lineSegment2 = LineSegment(Vector2.all(0), Vector2(0, 2)); + expect(lineSegment2.midpoint, Vector2(0, 1)); + }); + }); +} From a83f2815bbdaf9c176a34a325485a96b5a323575 Mon Sep 17 00:00:00 2001 From: Luan Nico Date: Sun, 16 Jul 2023 10:49:52 +0200 Subject: [PATCH 03/15] feat: Add a Circle.fromPoints utility method (#2603) # Description Add a `Circle.fromPoints` utility method to (maybe) create Circles that intersect three given points. --- .../experimental/geometry/shapes/circle.dart | 21 +++++++++++++++++++ .../geometry/shapes/circle_test.dart | 14 +++++++++++++ 2 files changed, 35 insertions(+) diff --git a/packages/flame/lib/src/experimental/geometry/shapes/circle.dart b/packages/flame/lib/src/experimental/geometry/shapes/circle.dart index 00aad1eb8a4..1a3e9991331 100644 --- a/packages/flame/lib/src/experimental/geometry/shapes/circle.dart +++ b/packages/flame/lib/src/experimental/geometry/shapes/circle.dart @@ -1,3 +1,4 @@ +import 'dart:math'; import 'dart:ui'; import 'package:flame/geometry.dart'; @@ -98,4 +99,24 @@ class Circle extends Shape { @override String toString() => 'Circle([${_center.x}, ${_center.y}], $_radius)'; + + /// Tries to create a Circle that intersects the 3 points, if it exists. + /// + /// As long as the points are not co-linear, there is always exactly one + /// circle intersecting all 3 points. + static Circle? fromPoints(Vector2 p1, Vector2 p2, Vector2 p3) { + final offset = p2.length2; + final bc = (p1.length2 - offset) / 2.0; + final cd = (offset - p3.length2) / 2.0; + final det = (p1.x - p2.x) * (p2.y - p3.y) - (p2.x - p3.x) * (p1.y - p2.y); + if (det == 0) { + return null; + } + + final centerX = (bc * (p2.y - p3.y) - cd * (p1.y - p2.y)) / det; + final centerY = (cd * (p1.x - p2.x) - bc * (p2.x - p3.x)) / det; + final radius = sqrt(pow(p2.x - centerX, 2) + pow(p2.y - centerY, 2)); + + return Circle(Vector2(centerX, centerY), radius); + } } diff --git a/packages/flame/test/experimental/geometry/shapes/circle_test.dart b/packages/flame/test/experimental/geometry/shapes/circle_test.dart index 20a463f5b6c..1b37aa75840 100644 --- a/packages/flame/test/experimental/geometry/shapes/circle_test.dart +++ b/packages/flame/test/experimental/geometry/shapes/circle_test.dart @@ -178,5 +178,19 @@ void main() { expect(result1, isNotNull); expect(result2, Vector2(0, 0)); }); + + test('fromPoints', () { + final p1 = Vector2.zero(); + final p2 = Vector2(0, 1); + final p3 = Vector2(1, 0); + + final circle = Circle.fromPoints(p1, p2, p3)!; + expect(circle.center, Vector2.all(0.5)); + expectDouble(circle.radius, 1 / sqrt(2)); + + expect(circle.containsPoint(p1), true); + expect(circle.containsPoint(p2), true); + expect(circle.containsPoint(p3), true); + }); }); } From 1567b3891057e4ce168d76c920bd40403febd82a Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 17 Jul 2023 00:52:15 +0200 Subject: [PATCH 04/15] fix: TextBoxConfig dismissDelay to not be ignored (#2607) `TextBoxConfig.dismissDelay` was being ignored, now it removed the component once it is finished and the delay has passed, like intended. --- .../src/components/text_box_component.dart | 19 +++++++++++----- .../components/text_box_component_test.dart | 22 +++++++++++++++++++ 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/packages/flame/lib/src/components/text_box_component.dart b/packages/flame/lib/src/components/text_box_component.dart index 5da8f53e39e..a6dc19a91d5 100644 --- a/packages/flame/lib/src/components/text_box_component.dart +++ b/packages/flame/lib/src/components/text_box_component.dart @@ -26,9 +26,9 @@ class TextBoxConfig { /// between each character. final double timePerChar; - /// Defaults to 0. If not zero, this component will disappear after this many - /// seconds after being fully typed out. - final double dismissDelay; + /// Defaults to null. If not null, this component will disappear after this + /// many seconds after being fully typed out. + final double? dismissDelay; /// Only relevant if [timePerChar] is set. If true, the box will start with /// the size to fit the first character and grow as more lines are typed. @@ -40,7 +40,7 @@ class TextBoxConfig { this.maxWidth = 200.0, this.margins = const EdgeInsets.all(8.0), this.timePerChar = 0.0, - this.dismissDelay = 0.0, + this.dismissDelay, this.growingBox = false, }); } @@ -173,7 +173,8 @@ class TextBoxComponent extends TextComponent { double get totalCharTime => text.length * _boxConfig.timePerChar; - bool get finished => _lifeTime > totalCharTime + _boxConfig.dismissDelay; + bool get finished => + _lifeTime >= totalCharTime + (_boxConfig.dismissDelay ?? 0); int get _actualTextLength { return lines.map((e) => e.length).sum; @@ -295,7 +296,9 @@ class TextBoxComponent extends TextComponent { // See issue #1618 for details. Future.delayed(const Duration(milliseconds: 100), () { cachedToRemove.remove(cachedImage); - cachedImage.dispose(); + if (isMounted) { + cachedImage.dispose(); + } }); } cache = await _fullRenderAsImage(newSize); @@ -309,6 +312,10 @@ class TextBoxComponent extends TextComponent { redraw(); } _previousChar = currentChar; + + if (_boxConfig.dismissDelay != null && finished) { + removeFromParent(); + } } @override diff --git a/packages/flame/test/components/text_box_component_test.dart b/packages/flame/test/components/text_box_component_test.dart index df3283fbaf7..912fc978feb 100644 --- a/packages/flame/test/components/text_box_component_test.dart +++ b/packages/flame/test/components/text_box_component_test.dart @@ -47,6 +47,28 @@ void main() { ); }); + testWithFlameGame( + 'setting dismissDelay removes component when finished', + (game) async { + final component = TextBoxComponent( + text: 'foo bar', + boxConfig: TextBoxConfig( + dismissDelay: 10.0, + timePerChar: 1.0, + ), + ); + + await game.ensureAdd(component); + game.update(8); + expect(component.isMounted, isTrue); + game.update(9); + expect(component.finished, isTrue); + expect(component.isRemoving, isTrue); + game.update(0); + expect(component.isMounted, isFalse); + }, + ); + testWithFlameGame('onLoad waits for cache to be done', (game) async { final c = TextBoxComponent(text: 'foo bar'); From 8a9f87e131c4bb63948b017d5ad8904c1aa811ea Mon Sep 17 00:00:00 2001 From: Brian Wo <45139213+brainwo@users.noreply.github.com> Date: Mon, 17 Jul 2023 15:42:34 +0800 Subject: [PATCH 05/15] docs: Adds more clarity to `pub get` (#2610) Apparently someone thought they should run pub get in their terminal instead of flutter pub get. Though it's called "Pub: Get Packages" in the VS Code command pallete. This changed pub get to flutter pub get in the installation guide (https://docs.flame-engine.org/latest/). --- doc/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/README.md b/doc/README.md index 1010a93827c..e91db33e8b3 100644 --- a/doc/README.md +++ b/doc/README.md @@ -42,7 +42,7 @@ dependencies: The latest version can be found on [pub.dev](https://pub.dev/packages/flame/install). -then run `pub get` and you are ready to start using it! +then run `flutter pub get` and you are ready to start using it! ## Getting started From 346bb6c85825c7a59dc7151cc1685de44d46f7cf Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 17 Jul 2023 16:23:55 +0200 Subject: [PATCH 06/15] docs: Update Nakama link + add Supabase (#2608) Update Nakama link + add Supabase to the docs. --- .github/.cspell/flame_dictionary.txt | 1 + doc/README.md | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/.cspell/flame_dictionary.txt b/.github/.cspell/flame_dictionary.txt index d32d6cf001b..d47e87d15df 100644 --- a/.github/.cspell/flame_dictionary.txt +++ b/.github/.cspell/flame_dictionary.txt @@ -26,6 +26,7 @@ spineboy spineboys spydon stpasha +Supabase tavian trex Videon diff --git a/doc/README.md b/doc/README.md index e91db33e8b3..4e4d6c2db50 100644 --- a/doc/README.md +++ b/doc/README.md @@ -72,10 +72,11 @@ Flame doesn't bundle any network feature, which may be needed to write online mu If you are building a multiplayer game, here are some recommendations of packages/services: -- [Nakama](https://github.com/Allan-Nava/nakama-flutter): Nakama is an open-source server designed +- [Nakama](https://github.com/obrunsmann/flutter_nakama/): Nakama is an open-source server designed to power modern games and apps. - [Firebase](https://firebase.google.com/): Provides dozens of services that can be used to write simpler multiplayer experiences. +- [Supabase](https://supabase.com/): A cheaper alternative to Firebase, based on Postgres. ### External assets From 832c051085e0fade8a7e4b262bf9941d279baef4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6hnn?= Date: Tue, 18 Jul 2023 19:33:41 +0200 Subject: [PATCH 07/15] fix: Only use pre-set ReadonlySizeProvider for sizing in HudMarginComponent (#2611) In a project where we wrapped the `JoystickComponent` into it's own `Component` to separate concerns resizing the window (or hot reloading) would throw an exception leading to a red screen. The `HudMarginComponent` currently expects the parent to be either a `FlameGame` or `ReadOnlySizeProvider`, which throws an exception when our wrapped `Joystick` class is neither. With the help of @spydon, we came to the conclusion that this fix should work and not break other cases. --- .../flame/lib/src/components/input/hud_margin_component.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/flame/lib/src/components/input/hud_margin_component.dart b/packages/flame/lib/src/components/input/hud_margin_component.dart index bab1ffc9fd7..50529e576cd 100644 --- a/packages/flame/lib/src/components/input/hud_margin_component.dart +++ b/packages/flame/lib/src/components/input/hud_margin_component.dart @@ -81,9 +81,7 @@ class HudMarginComponent extends PositionComponent { @override void onGameResize(Vector2 size) { super.onGameResize(size); - if (isMounted && - (parent is FlameGame || - (parent! as ReadonlySizeProvider).size is NotifyingVector2)) { + if (isMounted && _sizeProvider != null) { _updateMargins(); } } From 14f51635421b8b30049ea287b7c472e54a269250 Mon Sep 17 00:00:00 2001 From: DevKage <33748002+ufrshubham@users.noreply.github.com> Date: Thu, 20 Jul 2023 01:08:02 +0530 Subject: [PATCH 08/15] feat!: Make world nullable in `CameraComponent` (#2615) `CameraComponent` can now stare at nothingness because its world reference can be null now. ### Migration instructions `CameraComponent.world` is now nullable. While accessing it, make sure to perform null checks if it can be null in your case. Otherwise, if you are sure that the world is non-null perform unconditional using `CameraComponent.world!`. --- .../flame/lib/src/camera/camera_component.dart | 16 +++++++++------- .../viewports/fixed_size_viewport_test.dart | 4 ++-- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/flame/lib/src/camera/camera_component.dart b/packages/flame/lib/src/camera/camera_component.dart index 906cf7477a5..c07219ea36f 100644 --- a/packages/flame/lib/src/camera/camera_component.dart +++ b/packages/flame/lib/src/camera/camera_component.dart @@ -41,7 +41,7 @@ import 'package:vector_math/vector_math_64.dart'; /// That is, they will be affected both by the viewport and the viewfinder. class CameraComponent extends Component { CameraComponent({ - required this.world, + this.world, Viewport? viewport, Viewfinder? viewfinder, List? hudComponents, @@ -58,9 +58,9 @@ class CameraComponent extends Component { /// initially set up to show world coordinates (0, 0) at the center of the /// viewport. factory CameraComponent.withFixedResolution({ - required World world, required double width, required double height, + World? world, List? hudComponents, }) { return CameraComponent( @@ -97,7 +97,7 @@ class CameraComponent extends Component { /// /// The [world] component is generally mounted externally to the camera, and /// this variable is a mere reference to it. - World world; + World? world; /// The axis-aligned bounding rectangle of a [world] region which is currently /// visible through the viewport. @@ -140,13 +140,14 @@ class CameraComponent extends Component { viewport.position.y - viewport.anchor.y * viewport.size.y, ); // Render the world through the viewport - if (world.isMounted && currentCameras.length < maxCamerasDepth) { + if ((world?.isMounted ?? false) && + currentCameras.length < maxCamerasDepth) { canvas.save(); viewport.clip(canvas); try { currentCameras.add(this); canvas.transform(viewfinder.transform.transformMatrix.storage); - world.renderFromCamera(canvas); + world!.renderFromCamera(canvas); viewfinder.renderTree(canvas); } finally { currentCameras.removeLast(); @@ -167,11 +168,12 @@ class CameraComponent extends Component { point.x - viewport.position.x + viewport.anchor.x * viewport.size.x, point.y - viewport.position.y + viewport.anchor.y * viewport.size.y, ); - if (world.isMounted && currentCameras.length < maxCamerasDepth) { + if ((world?.isMounted ?? false) && + currentCameras.length < maxCamerasDepth) { if (viewport.containsLocalPoint(viewportPoint)) { currentCameras.add(this); final worldPoint = viewfinder.transform.globalToLocal(viewportPoint); - yield* world.componentsAtPoint(worldPoint, nestedPoints); + yield* world!.componentsAtPoint(worldPoint, nestedPoints); yield* viewfinder.componentsAtPoint(worldPoint, nestedPoints); currentCameras.removeLast(); } diff --git a/packages/flame/test/camera/viewports/fixed_size_viewport_test.dart b/packages/flame/test/camera/viewports/fixed_size_viewport_test.dart index ccf3a31a438..f3de7dba165 100644 --- a/packages/flame/test/camera/viewports/fixed_size_viewport_test.dart +++ b/packages/flame/test/camera/viewports/fixed_size_viewport_test.dart @@ -12,7 +12,7 @@ void main() { world: World(), viewport: FixedSizeViewport(300, 100), ); - game.addAll([camera.world, camera]); + game.addAll([camera.world!, camera]); await game.ready(); expect(camera.viewport, isA()); @@ -29,7 +29,7 @@ void main() { world: World(), viewport: FixedSizeViewport(400, 100), ); - game.addAll([camera.world, camera]); + game.addAll([camera.world!, camera]); await game.ready(); final viewport = camera.viewport; From 8e0a7879d7669e09efcbcee28d9f2038fe9014c0 Mon Sep 17 00:00:00 2001 From: Filip Hracek Date: Fri, 21 Jul 2023 15:55:09 +0200 Subject: [PATCH 09/15] perf: Improve performance of raycasts (#2617) Creates an axis-aligned bounding box (AABB) around the ray to short-circuit checking. In the general case, we skip ~75% of the checks because the ray originates at some point in the 2D cartesian space and goes towards infinity. The AABB in that case is a quadrant of space, and so only entities intersecting with this quadrant of space need to be checked. In the case where the raycast is called with a maxDistance, the savings are much higher. A relatively short ray will generate a small AABB around its origin and end point. In a typical game world, there will be many entities that do not intersect with this AABB, and can be skipped. --- .../standard_collision_detection.dart | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/packages/flame/lib/src/collisions/standard_collision_detection.dart b/packages/flame/lib/src/collisions/standard_collision_detection.dart index 635b0569038..0ae2f978668 100644 --- a/packages/flame/lib/src/collisions/standard_collision_detection.dart +++ b/packages/flame/lib/src/collisions/standard_collision_detection.dart @@ -1,3 +1,5 @@ +import 'dart:math' as math; + import 'package:flame/collisions.dart'; import 'package:flame/components.dart'; import 'package:flame/geometry.dart'; @@ -64,6 +66,8 @@ class StandardCollisionDetection> static final _temporaryRaycastResult = RaycastResult(); + static final _temporaryRayAabb = Aabb2(); + @override RaycastResult? raycast( Ray2 ray, { @@ -72,17 +76,21 @@ class StandardCollisionDetection> RaycastResult? out, }) { var finalResult = out?..reset(); + _updateRayAabb(ray, maxDistance); for (final item in items) { if (ignoreHitboxes?.contains(item) ?? false) { continue; } + if (!item.aabb.intersectsWithAabb2(_temporaryRayAabb)) { + continue; + } final currentResult = item.rayIntersection(ray, out: _temporaryRaycastResult); final possiblyFirstResult = !(finalResult?.isActive ?? false); if (currentResult != null && (possiblyFirstResult || currentResult.distance! < finalResult!.distance!) && - (currentResult.distance! <= (maxDistance ?? double.infinity))) { + currentResult.distance! <= (maxDistance ?? double.infinity)) { if (finalResult == null) { finalResult = currentResult.clone(); } else { @@ -172,4 +180,29 @@ class StandardCollisionDetection> } } } + + /// Computes an axis-aligned bounding box for a [ray]. + /// + /// When [maxDistance] is provided, this will be the bounding box around + /// the origin of the ray and its ending point. When [maxDistance] + /// is `null`, the bounding box will encompass the whole quadrant + /// of space, from the ray's origin to infinity. + void _updateRayAabb(Ray2 ray, double? maxDistance) { + final x1 = ray.origin.x; + final y1 = ray.origin.y; + double x2; + double y2; + + if (maxDistance != null) { + x2 = ray.origin.x + ray.direction.x * maxDistance; + y2 = ray.origin.y + ray.direction.y * maxDistance; + } else { + x2 = ray.direction.x > 0 ? double.infinity : double.negativeInfinity; + y2 = ray.direction.y > 0 ? double.infinity : double.negativeInfinity; + } + + _temporaryRayAabb + ..min.setValues(math.min(x1, x2), math.min(y1, y2)) + ..max.setValues(math.max(x1, x2), math.max(y1, y2)); + } } From 1cad0b23e18db8f352da5790c8ea5ec6053936da Mon Sep 17 00:00:00 2001 From: DevKage <33748002+ufrshubham@users.noreply.github.com> Date: Sat, 22 Jul 2023 02:42:26 +0530 Subject: [PATCH 10/15] feat: Add optional world input to `CameraComponent.canSee` (#2616) CameraComponent.canSee wasn't performing any kind of sanity checks on the given components world or mounted-ness. This PR adds these checks to correctly return false if the component is not mounted or if the optional world is not the same as camera's current target world. --- .../lib/src/camera/camera_component.dart | 20 ++++- .../test/camera/camera_component_test.dart | 81 ++++++++++++++++--- 2 files changed, 88 insertions(+), 13 deletions(-) diff --git a/packages/flame/lib/src/camera/camera_component.dart b/packages/flame/lib/src/camera/camera_component.dart index c07219ea36f..7b32dffd6f0 100644 --- a/packages/flame/lib/src/camera/camera_component.dart +++ b/packages/flame/lib/src/camera/camera_component.dart @@ -282,7 +282,25 @@ class CameraComponent extends Component { } /// Returns true if this camera is able to see the [component]. - bool canSee(PositionComponent component) { + /// Will always return false if + /// - [world] is null or + /// - [world] is not mounted or + /// - [component] is not mounted or + /// - [componentWorld] is non-null and does not match with [world] + /// + /// If [componentWorld] is null, this method does not take into consideration + /// the world to which the given [component] belongs (if any). This means, in + /// such cases, any component overlapping the [visibleWorldRect] will be + /// reported as visible, even if it is not part of the [world] this camera is + /// currently looking at. This can be changed by passing the the component's + /// world as [componentWorld]. + bool canSee(PositionComponent component, {World? componentWorld}) { + if (!(world?.isMounted ?? false) || + !component.isMounted || + (componentWorld != null && componentWorld != world)) { + return false; + } + return visibleWorldRect.overlaps(component.toAbsoluteRect()); } } diff --git a/packages/flame/test/camera/camera_component_test.dart b/packages/flame/test/camera/camera_component_test.dart index 230384ff216..99834e391c9 100644 --- a/packages/flame/test/camera/camera_component_test.dart +++ b/packages/flame/test/camera/camera_component_test.dart @@ -280,7 +280,11 @@ void main() { }); testWithFlameGame('component is in view for the camera', (game) async { - final world = World(); + final component = PositionComponent( + size: Vector2(10, 10), + position: Vector2(0, 0), + ); + final world = World(children: [component]); final camera = CameraComponent( world: world, viewport: FixedSizeViewport(60, 40), @@ -288,16 +292,15 @@ void main() { game.addAll([world, camera]); await game.ready(); - final component = PositionComponent( - size: Vector2(10, 10), - position: Vector2(0, 0), - ); - expect(camera.canSee(component), isTrue); }); testWithFlameGame('component is out of view for the camera', (game) async { - final world = World(); + final component = PositionComponent( + size: Vector2(10, 10), + position: Vector2(100, 100), + ); + final world = World(children: [component]); final camera = CameraComponent( world: world, viewport: FixedSizeViewport(60, 40), @@ -305,14 +308,68 @@ void main() { game.addAll([world, camera]); await game.ready(); - final component = PositionComponent( - size: Vector2(10, 10), - position: Vector2(100, 100), - ); - expect(camera.canSee(component), isFalse); }); }); + + group('CameraComponent.canSee', () { + testWithFlameGame('null world', (game) async { + final player = PositionComponent(); + final world = World(children: [player]); + final camera = CameraComponent(); + + await game.addAll([camera, world]); + await game.ready(); + expect(camera.canSee(player), false); + + camera.world = world; + expect(camera.canSee(player), true); + }); + + testWithFlameGame('unmounted world', (game) async { + final player = PositionComponent(); + final world = World(children: [player]); + final camera = CameraComponent(world: world); + + await game.addAll([camera]); + await game.ready(); + expect(camera.canSee(player), false); + + await game.add(world); + await game.ready(); + expect(camera.canSee(player), true); + }); + + testWithFlameGame('unmounted component', (game) async { + final player = PositionComponent(); + final world = World(); + final camera = CameraComponent(world: world); + + await game.addAll([camera, world]); + await game.ready(); + expect(camera.canSee(player), false); + + await world.add(player); + await game.ready(); + expect(camera.canSee(player), true); + }); + + testWithFlameGame('component from another world', (game) async { + final player = PositionComponent(); + final world1 = World(children: [player]); + final world2 = World(); + final camera = CameraComponent(world: world2); + + await game.addAll([camera, world1, world2]); + await game.ready(); + + // can see when player world is not known. + expect(camera.canSee(player), true); + + // can't see when the player world is known. + expect(camera.canSee(player, componentWorld: world1), false); + }); + }); } class _SolidBackground extends Component with HasPaint { From 36e2b509b30c68786d414210c34f4a1b5c0c7d9e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 24 Jul 2023 12:40:00 +0000 Subject: [PATCH 11/15] chore(deps): Bump pygments from 2.14.0 to 2.15.0 in /doc/_sphinx (#2620) Bumps [pygments](https://github.com/pygments/pygments) from 2.14.0 to 2.15.0. --- doc/_sphinx/requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/_sphinx/requirements.txt b/doc/_sphinx/requirements.txt index 989baaaad9c..26a0a35a768 100644 --- a/doc/_sphinx/requirements.txt +++ b/doc/_sphinx/requirements.txt @@ -1,6 +1,6 @@ linkify-it-py==2.0.0 myst-parser==1.0.0 -Pygments==2.14.0 +Pygments==2.15.0 Sphinx==6.1.3 sphinxcontrib-mermaid==0.8.1 sphinxcontrib-jquery==4.0 From b4f6e271dfacfa175bdf18c9a2937ec22dce2db6 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Mon, 24 Jul 2023 15:05:27 +0200 Subject: [PATCH 12/15] chore: Bump audioplayers to 5.0.0 (#2621) Bumps AP to 5.0.0 --- packages/flame_audio/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/flame_audio/pubspec.yaml b/packages/flame_audio/pubspec.yaml index ffdf8fe14de..b53abb745ec 100644 --- a/packages/flame_audio/pubspec.yaml +++ b/packages/flame_audio/pubspec.yaml @@ -13,7 +13,7 @@ environment: flutter: ">=3.3.0" dependencies: - audioplayers: ^4.0.1 + audioplayers: ^5.0.0 flame: ^1.8.1 flutter: sdk: flutter From 781e898315a0162117a83bf62e2650ce7244503d Mon Sep 17 00:00:00 2001 From: Filip Hracek Date: Wed, 26 Jul 2023 14:42:57 +0200 Subject: [PATCH 13/15] docs: Add more guidance to collision detection algorithm choices (#2624) This is adding a bit more precise language and more context to API docs of `HasCollisionDetection`, `Broadphase` and `HasQuadTreeCollisionDetection`. --- doc/flame/collision_detection.md | 103 +++++++++++------- .../src/collisions/broadphase/broadphase.dart | 15 ++- .../has_quadtree_collision_detection.dart | 8 ++ .../collisions/has_collision_detection.dart | 4 + 4 files changed, 86 insertions(+), 44 deletions(-) diff --git a/doc/flame/collision_detection.md b/doc/flame/collision_detection.md index bd5d3e03d41..d465ea902d2 100644 --- a/doc/flame/collision_detection.md +++ b/doc/flame/collision_detection.md @@ -8,8 +8,9 @@ bounding boxes of your components. In Flame the hitboxes are areas of the compon to collisions (and make [gesture input](inputs/gesture_input.md#gesturehitboxes)) more accurate. The collision detection system supports three different types of shapes that you can build hitboxes -from, these shapes are Polygon, Rectangle and Circle. Multiple hitbox can be added to a component to -form the area which can be used to either detect collisions or whether it contains a point or not, +from, these shapes are Polygon, Rectangle and Circle. Multiple hitbox can be added +to a component to form the area which can be used to either detect collisions +or whether it contains a point or not, the latter is very useful for accurate gesture detection. The collision detection does not handle what should happen when two hitboxes collide, so it is up to the user to implement what will happen when for example two `PositionComponent`s have intersecting hitboxes. @@ -42,7 +43,8 @@ class MyGame extends FlameGame with HasCollisionDetection { Now when you add `ShapeHitbox`s to components that are then added to the game, they will automatically be checked for collisions. -You can also add `HasCollisionDetection` directly to another `Component` instead of the `FlameGame`, +You can also add `HasCollisionDetection` directly to another `Component` instead +of the `FlameGame`, for example to the `World` that is used for the `CameraComponent`. If that is done, hitboxes that are added in that component's tree will only be compared to other hitboxes in that subtree, which makes it possible to have several worlds with collision detection @@ -96,8 +98,8 @@ class MyCollidable extends PositionComponent with CollisionCallbacks { } ``` -In this example we use Dart's `is` keyword to check what kind of component we collided with. The set -of points is where the edges of the hitboxes intersect. +In this example we use Dart's `is` keyword to check what kind of component we collided with. +The set of points is where the edges of the hitboxes intersect. Note that the `onCollision` method will be called on both `PositionComponent`s if they have both implemented the `onCollision` method, and also on both hitboxes. The same goes for the @@ -133,7 +135,8 @@ class MyComponent extends PositionComponent { ``` If you don't add any arguments to the hitbox, like above, the hitbox will try to fill its parent as -much as possible. Except for having the hitboxes trying to fill their parents, there are two ways to +much as possible. Except for having the hitboxes trying to fill their parents, +there are two ways to initiate hitboxes and it is with the normal constructor where you define the hitbox by itself, with a size and a position etc. The other way is to use the `relative` constructor which defines the hitbox in relation to the size of its intended parent. @@ -201,15 +204,16 @@ The `CollisionType` enum contains the following values: - `inactive` will not collide with any other `Collidable`s So if you have hitboxes that you don't need to check collisions against each other you can mark -them as passive by setting `collisionType: CollisionType.passive` in the constructor, this could for -example be ground components or maybe your enemies don't need to check collisions between each -other, then they could be marked as `passive` too. +them as passive by setting `collisionType: CollisionType.passive` in the constructor, +this could for example be ground components or maybe your enemies don't need +to check collisions between each other, then they could be marked as `passive` too. Imagine a game where there are a lot of bullets, that can't collide with each other, flying towards the player, then the player would be set to `CollisionType.active` and the bullets would be set to `CollisionType.passive`. -Then we have the `inactive` type which simply doesn't get checked at all in the collision detection. +Then we have the `inactive` type which simply doesn't get checked at all +in the collision detection. This could be used for example if you have components outside of the screen that you don't care about at the moment but that might later come back in to view so they are not completely removed from the game. @@ -222,8 +226,9 @@ them so don't doubt to use them even if your use case isn't listed here. It should be noted that if you want to use collision detection or `containsPoint` on the `Polygon`, the polygon needs to be convex. So always use convex polygons or you will most likely run into -problems if you don't really know what you are doing. It should also be noted that you should always -define the vertices in your polygon in a counter-clockwise order. +problems if you don't really know what you are doing. +It should also be noted that you should always define the vertices in your polygon +in a counter-clockwise order. The other hitbox shapes don't have any mandatory constructor, that is because they can have a default calculated from the size of the collidable that they are attached to, but since a @@ -259,32 +264,36 @@ want the `ScreenHitbox` itself to be notified when something collides with it. S ## CompositeHitbox -In the `CompositeHitbox` you can add multiple hitboxes so that they emulate being one joined hitbox. +In the `CompositeHitbox` you can add multiple hitboxes so that +they emulate being one joined hitbox. -If you want to form a hat for example you might want to use two [](#rectanglehitbox)s to follow that +If you want to form a hat for example you might want +to use two [](#rectanglehitbox)s to follow that hat's edges properly, then you can add those hitboxes to an instance of this class and react to collisions to the whole hat, instead of for just each hitbox separately. ## Broad phase -If your game field is small and do not have a lot of collidable components - you don't have to +If your game field isn't huge and does not have a lot of collidable components - you don't have to worry about the broad phase system that is used, so if the standard implementation is performant enough for you, you probably don't have to read this section. A broad phase is the first step of collision detection where potential collisions are calculated. -To calculate these potential collisions are a lot cheaper to calculate than to check the exact -intersections directly and it removes the need to check all hitboxes against each other and -therefore avoiding O(n²). The broad phase produces a set of potential collisions (a set of -`CollisionProspect`s), this set is then used to check the exact intersections between hitboxes, this -is sometimes called narrow phase. +Calculating these potential collisions is faster than to checking the intersections exactly, +and it removes the need to check all hitboxes against each other and +therefore avoiding O(n²). -By default Flame's collision detection is using a sweep and prune broadphase step, if your game +The broad phase produces a set of potential collisions (a set of +`CollisionProspect`s). This set is then used to check the exact intersections between +hitboxes (sometimes called "narrow phase"). + +By default, Flame's collision detection is using a sweep and prune broadphase step. If your game requires another type of broadphase you can write your own broadphase by extending `Broadphase` and manually setting the collision detection system that should be used. -For example if you have implemented a broadphase built on a magic algorithm instead of the standard -sweep and prune, then you would do the following: +For example, if you have implemented a broadphase built on a magic algorithm +instead of the standard sweep and prune, then you would do the following: ```dart class MyGame extends FlameGame with HasCollisionDetection { @@ -301,6 +310,7 @@ class MyGame extends FlameGame with HasCollisionDetection { If your game field is large and the game contains a lot of collidable components (more than a hundred), standard sweep and prune can become inefficient. If it does, you can try to use the quad tree broad phase. + To do this, add the `HasQuadTreeCollisionDetection` mixin to your game instead of `HasCollisionDetection` and call the `initializeCollisionDetection` function on game load: @@ -323,11 +333,12 @@ more efficient: - `minimumDistance`: minimum distance between objects to consider them as possibly colliding. If `null` - the check is disabled, it is default behavior - `maxObjects`: maximum objects count in one quadrant. Default to 25. -- `maxDepth`: - maximum nesting levels inside quadrant. Default to 10 +- `maxDepth`: maximum nesting levels inside quadrant. Default to 10 If you use the quad tree system, you can make it even more efficient by implementing the -`onComponentTypeCheck` function of the `CollisionCallbacks` mixin in your components. It is useful if -you need to prevent collisions of items of different types. The result of the calculation is cached so +`onComponentTypeCheck` function of the `CollisionCallbacks` mixin in your components. +It is useful if you need to prevent collisions of items of different types. +The result of the calculation is cached so you should not check any dynamic parameters here, the function is intended to be used as a pure type checker: @@ -383,16 +394,26 @@ class QuadTreeExample extends FlameGame ``` +```{note} +Always experiment with different collision detection approaches +and check how they perform on your game. +It is not unheard of that `QuadTreeBroadphase` is significantly +_slower_ than the default. +Don't assume that the more sophisticated approach is always faster. +``` + ## Ray casting and Ray tracing Ray casting and ray tracing are methods for sending out rays from a point in your game and being able to see what these rays collide with and how they reflect after hitting something. -For all of the following methods, if there are any hitboxes that you wish to ignore, you can add the -`ignoreHitboxes` argument which is a list of the hitboxes that you wish to disregard for the call. -This can be quite useful for example if you are casting rays from within a hitbox, which could be on -your player or NPC; or if you don't want a ray to bounce off a `ScreenHitbox`. +For all of the following methods, if there are any hitboxes that you wish to ignore, +you can add the `ignoreHitboxes` argument which is a list of the hitboxes +that you wish to disregard for the call. +This can be quite useful for example if you are casting rays from within a hitbox, +which could be on your player or NPC; +or if you don't want a ray to bounce off a `ScreenHitbox`. ### Ray casting @@ -402,12 +423,14 @@ anything, in Flame's case, hitboxes. We provide two methods for doing so, `raycast` and `raycastAll`. The first one just casts out a single ray and gets back a result with information about what and where the ray hit, and some -extra information like the distance, the normal and the reflection ray. The second one, `raycastAll`, +extra information like the distance, the normal and the reflection ray. +The second one, `raycastAll`, works similarly but sends out multiple rays uniformly around the origin, or within an angle centered at the origin. -By default, `raycast` and `raycastAll` scan for the nearest hit irrespective of how far it lies from -the ray origin. But in some use cases, it might be interesting to find hits only within a certain +By default, `raycast` and `raycastAll` scan for the nearest hit irrespective of +how far it lies from the ray origin. +But in some use cases, it might be interesting to find hits only within a certain range. For such cases, an optional `maxDistance` can be provided. To use the ray casting functionality you have to have the `HasCollisionDetection` mixin on your @@ -527,8 +550,9 @@ class MyGame extends FlameGame with HasCollisionDetection { } ``` -In the example above we send out a ray from (0, 100) diagonally down to the right and we say that we -want it the bounce on at most 100 hitboxes, it doesn't necessarily have to get 100 results since at +In the example above we send out a ray from (0, 100) diagonally down to the right +and we say that we want it the bounce on at most 100 hitboxes, +it doesn't necessarily have to get 100 results since at some point one of the reflection rays might not hit a hitbox and then the method is done. The method is lazy, which means that it will only do the calculations that you ask for, so you have @@ -537,8 +561,8 @@ calculate all the results. In the for-loop it can be seen how this can be used, in that loop we check whether the current reflection rays intersection point (where the previous ray hit the hitbox) is further away than 300 -pixels from the origin of the starting ray, and if it is we don't care about the rest of the results -(and then they don't have to be calculated either). +pixels from the origin of the starting ray, and if it is we don't care about the rest +of the results (and then they don't have to be calculated either). If you are concerned about performance you can re-use the `RaycastResult` objects that are created by the function by sending them in as a list with the `out` argument. @@ -569,8 +593,9 @@ need some of the following things (since it is simpler to not involve Forge2D): ## Migration from the collision detection system in v1.0 -The collision detection system introduced in v1.1 is easier to use, and much more efficient than the -one that was in v1.0, but while making these improvements some breaking changes had to be made. +The collision detection system introduced in v1.1 is easier to use, +and much more efficient than the one that was in v1.0, +but while making these improvements some breaking changes had to be made. There is no longer a `Collidable` mixin, instead your game automatically knows when a hitbox has been added to one of your components when the `HasCollisionDetection` mixin is added to your game. diff --git a/packages/flame/lib/src/collisions/broadphase/broadphase.dart b/packages/flame/lib/src/collisions/broadphase/broadphase.dart index 1fa6f881f0d..97e562a2b40 100644 --- a/packages/flame/lib/src/collisions/broadphase/broadphase.dart +++ b/packages/flame/lib/src/collisions/broadphase/broadphase.dart @@ -6,11 +6,16 @@ import 'package:meta/meta.dart'; /// actual intersections are calculated. /// /// Currently there are two implementations of [Broadphase]: -/// - [Sweep] is the simplest but slowest system, yet nice for small amounts of -/// hitboxes. -/// - [QuadTree] usually works faster, but requires additional setup and works -/// only with fixed-size maps. See [HasQuadTreeCollisionDetection] for -/// details. +/// +/// - [Sweep] is the simplest system. It simply short-circuits potential +/// collisions based on the horizontal (x) position of the components +/// in question. It is the default implementation when you use +/// `HasCollisionDetection`. +/// - [QuadTree] works faster in some cases. It requires additional setup +/// and works only with fixed-size maps. See [HasQuadTreeCollisionDetection] +/// for details. +/// +/// Always experiment to see which approach works best for your game. abstract class Broadphase> { Broadphase(); diff --git a/packages/flame/lib/src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart b/packages/flame/lib/src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart index 571aaa5fd2f..4a7b9fe10a6 100644 --- a/packages/flame/lib/src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart +++ b/packages/flame/lib/src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart @@ -5,6 +5,14 @@ import 'package:flame/game.dart'; /// This should be applied to a [FlameGame] to bring QuadTree collision /// support. /// +/// Use [HasQuadTreeCollisionDetection] if you have lots of collidable entities +/// in your game, but most of them are static (such as platforms, walls, trees, +/// buildings). +/// +/// Always experiment before deciding which collision detection +/// method to use. It's not unheard of to see better performance with +/// the default [HasCollisionDetection] mixin. +/// /// [initializeCollisionDetection] should be called in the game's [onLoad] /// method. mixin HasQuadTreeCollisionDetection on FlameGame diff --git a/packages/flame/lib/src/collisions/has_collision_detection.dart b/packages/flame/lib/src/collisions/has_collision_detection.dart index c1ee5969707..eb04f26ede0 100644 --- a/packages/flame/lib/src/collisions/has_collision_detection.dart +++ b/packages/flame/lib/src/collisions/has_collision_detection.dart @@ -7,6 +7,10 @@ import 'package:flame/components.dart'; /// Hitboxes are only part of the collision detection performed by its closest /// parent with the [HasCollisionDetection] mixin, if there are multiple nested /// classes that has [HasCollisionDetection]. +/// +/// You can experiment with non-standard collision detection methods, such +/// as `HasQuadtreeCollisionDetection`. This can sometimes bring better +/// performance, but it's not guaranteed. mixin HasCollisionDetection> on Component { CollisionDetection _collisionDetection = StandardCollisionDetection(); From e430b6cdf2e6be52bf384efb3428bcb41ae13d30 Mon Sep 17 00:00:00 2001 From: Lukas Klingsbo Date: Fri, 28 Jul 2023 22:59:24 +0200 Subject: [PATCH 14/15] perf!: Pool `CollisionProspect`s and remove some list creations from the collision detection (#2625) This change introduces a very simple pool for `CollisionProspect`s so that those objects don't have to be re-created each tick. It means that the `CollisionProspect` needs to be mutable though, so the code becomes a little bit harder to read since sets can't be used anymore. --- examples/.metadata | 25 ++----- examples/test/main_test.dart | 6 -- packages/flame/lib/collisions.dart | 1 + .../src/collisions/broadphase/broadphase.dart | 50 +++++++++---- .../collisions/broadphase/prospect_pool.dart | 25 +++++++ .../has_quadtree_collision_detection.dart | 4 +- .../quadtree/quad_tree_broadphase.dart | 75 ++++++++++--------- .../quadtree_collision_detection.dart | 28 ++++--- .../collisions/broadphase/sweep/sweep.dart | 17 +++-- .../src/collisions/collision_detection.dart | 43 +++++++---- .../collisions/collision_callback_test.dart | 6 +- .../collisions/collision_test_helpers.dart | 10 ++- packages/flame_lint/lib/analysis_options.yaml | 1 - 13 files changed, 180 insertions(+), 111 deletions(-) delete mode 100644 examples/test/main_test.dart create mode 100644 packages/flame/lib/src/collisions/broadphase/prospect_pool.dart diff --git a/examples/.metadata b/examples/.metadata index fe11f50758d..732ba6d37cc 100644 --- a/examples/.metadata +++ b/examples/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled. version: - revision: 2ad6cd72c040113b47ee9055e722606a490ef0da + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 channel: stable project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 - platform: android - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - - platform: ios - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - - platform: linux - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - - platform: macos - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - - platform: web - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - - platform: windows - create_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da - base_revision: 2ad6cd72c040113b47ee9055e722606a490ef0da + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 # User provided section diff --git a/examples/test/main_test.dart b/examples/test/main_test.dart deleted file mode 100644 index e566c9dcc33..00000000000 --- a/examples/test/main_test.dart +++ /dev/null @@ -1,6 +0,0 @@ -import 'package:examples/main.dart' as examples; -import 'package:test/test.dart'; - -void main() { - test('main', examples.main); -} diff --git a/packages/flame/lib/collisions.dart b/packages/flame/lib/collisions.dart index 69b74964ed4..9a0dd436bdb 100644 --- a/packages/flame/lib/collisions.dart +++ b/packages/flame/lib/collisions.dart @@ -1,4 +1,5 @@ export 'src/collisions/broadphase/broadphase.dart'; +export 'src/collisions/broadphase/prospect_pool.dart'; export 'src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart'; export 'src/collisions/broadphase/quadtree/quad_tree_broadphase.dart'; export 'src/collisions/broadphase/quadtree/quadtree.dart'; diff --git a/packages/flame/lib/src/collisions/broadphase/broadphase.dart b/packages/flame/lib/src/collisions/broadphase/broadphase.dart index 97e562a2b40..d183e74dde6 100644 --- a/packages/flame/lib/src/collisions/broadphase/broadphase.dart +++ b/packages/flame/lib/src/collisions/broadphase/broadphase.dart @@ -1,5 +1,4 @@ import 'package:flame/collisions.dart'; -import 'package:meta/meta.dart'; /// The [Broadphase] class is used to make collision detection more efficient /// by doing a rough estimation of which hitboxes that can collide before their @@ -32,33 +31,56 @@ abstract class Broadphase> { /// detection system. void add(T item); - void addAll(Iterable items) => items.forEach(add); + void addAll(Iterable items) { + for (final item in items) { + add(item); + } + } /// Removes an item from the broadphase. Should be called in a /// [CollisionDetection] class while removing a hitbox from its collision /// detection system. void remove(T item); - void removeAll(Iterable items) => items.forEach(remove); + void removeAll(Iterable items) { + for (final item in items) { + remove(item); + } + } /// Returns the potential hitbox collisions - Set> query(); + Iterable> query(); } /// A [CollisionProspect] is a tuple that is used to contain two potentially /// colliding hitboxes. -@immutable class CollisionProspect { - final T a; - final T b; + T _a; + T _b; - const CollisionProspect(this.a, this.b); + T get a => _a; + T get b => _b; - @override - bool operator ==(Object other) => - other is CollisionProspect && - ((other.a == a && other.b == b) || (other.a == b && other.b == a)); + int get hash => _hash; + int _hash; - @override - int get hashCode => Object.hashAllUnordered([a, b]); + CollisionProspect(this._a, this._b) : _hash = _a.hashCode ^ _b.hashCode; + + /// Sets the prospect to contain [a] and [b] instead of what it previously + /// contained. + void set(T a, T b) { + _a = a; + _b = b; + _hash = a.hashCode ^ b.hashCode; + } + + /// Sets the prospect to contain the content of [other]. + void setFrom(CollisionProspect other) { + _a = other._a; + _b = other._b; + _hash = other._hash; + } + + /// Creates a new prospect object with the same content. + CollisionProspect clone() => CollisionProspect(_a, _b); } diff --git a/packages/flame/lib/src/collisions/broadphase/prospect_pool.dart b/packages/flame/lib/src/collisions/broadphase/prospect_pool.dart new file mode 100644 index 00000000000..9dcf9e303bd --- /dev/null +++ b/packages/flame/lib/src/collisions/broadphase/prospect_pool.dart @@ -0,0 +1,25 @@ +import 'package:flame/src/collisions/broadphase/broadphase.dart'; +import 'package:flame/src/collisions/hitboxes/hitbox.dart'; + +/// This pool is used to not create unnecessary [CollisionProspect] objects +/// during collision detection, but to re-use the ones that have already been +/// created. +class ProspectPool> { + ProspectPool({this.incrementSize = 1000}); + + /// How much the pool should increase in size every time it needs to be made + /// larger. + final int incrementSize; + final _storage = >[]; + int get length => _storage.length; + + /// The size of the pool will expand with [incrementSize] amount of + /// [CollisionProspect]s that are initially populated with two [dummyItem]s. + void expand(T dummyItem) { + for (var i = 0; i < incrementSize; i++) { + _storage.add(CollisionProspect(dummyItem, dummyItem)); + } + } + + CollisionProspect operator [](int index) => _storage[index]; +} diff --git a/packages/flame/lib/src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart b/packages/flame/lib/src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart index 4a7b9fe10a6..dfa4fcbff27 100644 --- a/packages/flame/lib/src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart +++ b/packages/flame/lib/src/collisions/broadphase/quadtree/has_quadtree_collision_detection.dart @@ -16,7 +16,7 @@ import 'package:flame/game.dart'; /// [initializeCollisionDetection] should be called in the game's [onLoad] /// method. mixin HasQuadTreeCollisionDetection on FlameGame - implements HasCollisionDetection> { + implements HasCollisionDetection { late QuadTreeCollisionDetection _collisionDetection; @override @@ -24,7 +24,7 @@ mixin HasQuadTreeCollisionDetection on FlameGame @override set collisionDetection( - CollisionDetection> cd, + CollisionDetection cd, ) { if (cd is! QuadTreeCollisionDetection) { throw 'Must be QuadTreeCollisionDetection!'; diff --git a/packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart b/packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart index a5849997cfd..3e5722af8d3 100644 --- a/packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart +++ b/packages/flame/lib/src/collisions/broadphase/quadtree/quad_tree_broadphase.dart @@ -17,44 +17,43 @@ typedef ExternalMinDistanceCheck = bool Function( /// /// See [HasQuadTreeCollisionDetection.initializeCollisionDetection] for a /// detailed description of its initialization parameters. -class QuadTreeBroadphase> extends Broadphase { +class QuadTreeBroadphase extends Broadphase { QuadTreeBroadphase({ required Rect mainBoxSize, required this.broadphaseCheck, required this.minimumDistanceCheck, int maxObjects = 25, int maxDepth = 10, - }) : tree = QuadTree( + }) : tree = QuadTree( mainBoxSize: mainBoxSize, maxObjects: maxObjects, maxDepth: maxDepth, ); - final QuadTree tree; + final QuadTree tree; - final activeCollisions = HashSet(); + final activeHitboxes = HashSet(); ExternalBroadphaseCheck broadphaseCheck; ExternalMinDistanceCheck minimumDistanceCheck; - final _broadphaseCheckCache = >{}; + final _broadphaseCheckCache = >{}; final _cachedCenters = {}; - final _potentials = HashSet>(); - final _potentialsTmp = >[]; + final _potentials = >{}; + final _potentialsTmp = []; + final _prospectPool = ProspectPool(); @override - List get items => tree.hitboxes; + List get items => tree.hitboxes; @override - HashSet> query() { + Iterable> query() { _potentials.clear(); _potentialsTmp.clear(); - for (final activeItem in activeCollisions) { - final asShapeItem = activeItem as ShapeHitbox; - - if (asShapeItem.isRemoving || asShapeItem.parent == null) { + for (final activeItem in activeHitboxes) { + if (activeItem.isRemoving || !activeItem.isMounted) { tree.remove(activeItem); continue; } @@ -70,63 +69,69 @@ class QuadTreeBroadphase> extends Broadphase { continue; } - final asShapePotential = potential as ShapeHitbox; - - if (asShapePotential.parent == asShapeItem.parent && - asShapeItem.parent != null) { + if (!potential.allowSiblingCollision && + potential.hitboxParent == activeItem.hitboxParent && + potential.isMounted) { continue; } final distanceCloseEnough = minimumDistanceCheck.call( itemCenter, - _cacheCenterOfHitbox(asShapePotential), + _cacheCenterOfHitbox(potential), ); if (distanceCloseEnough == false) { continue; } - _potentialsTmp.add([asShapeItem, asShapePotential]); + _potentialsTmp + ..add(activeItem) + ..add(potential); } } if (_potentialsTmp.isNotEmpty) { - for (var i = 0; i < _potentialsTmp.length; i++) { - final item0 = _potentialsTmp[i].first; - final item1 = _potentialsTmp[i].last; + for (var i = 0; i < _potentialsTmp.length; i += 2) { + final item0 = _potentialsTmp[i]; + final item1 = _potentialsTmp[i + 1]; if (broadphaseCheck(item0, item1)) { - _potentials.add(CollisionProspect(item0 as T, item1 as T)); + final CollisionProspect prospect; + if (_prospectPool.length <= i) { + _prospectPool.expand(item0); + } + prospect = _prospectPool[i]..set(item0, item1); + _potentials[prospect.hash] = prospect; } else { - if (_broadphaseCheckCache[item0 as T] == null) { - _broadphaseCheckCache[item0 as T] = {}; + if (_broadphaseCheckCache[item0] == null) { + _broadphaseCheckCache[item0] = {}; } - _broadphaseCheckCache[item0 as T]![item1 as T] = false; + _broadphaseCheckCache[item0]![item1] = false; } } } - return _potentials; + return _potentials.values; } - void updateTransform(T item) { + void updateTransform(ShapeHitbox item) { tree.remove(item, keepOldPosition: true); - _cacheCenterOfHitbox(item as ShapeHitbox); + _cacheCenterOfHitbox(item); tree.add(item); } @override - void add(T item) { + void add(ShapeHitbox item) { tree.add(item); if (item.collisionType == CollisionType.active) { - activeCollisions.add(item); + activeHitboxes.add(item); } - _cacheCenterOfHitbox(item as ShapeHitbox); + _cacheCenterOfHitbox(item); } @override - void remove(T item) { + void remove(ShapeHitbox item) { tree.remove(item); _cachedCenters.remove(item); if (item.collisionType == CollisionType.active) { - activeCollisions.remove(item); + activeHitboxes.remove(item); } final checkCache = _broadphaseCheckCache[item]; @@ -140,7 +145,7 @@ class QuadTreeBroadphase> extends Broadphase { void clear() { tree.clear(); - activeCollisions.clear(); + activeHitboxes.clear(); _broadphaseCheckCache.clear(); _cachedCenters.clear(); } diff --git a/packages/flame/lib/src/collisions/broadphase/quadtree/quadtree_collision_detection.dart b/packages/flame/lib/src/collisions/broadphase/quadtree/quadtree_collision_detection.dart index db37d7b5f28..aa9eed4be6d 100644 --- a/packages/flame/lib/src/collisions/broadphase/quadtree/quadtree_collision_detection.dart +++ b/packages/flame/lib/src/collisions/broadphase/quadtree/quadtree_collision_detection.dart @@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart'; /// Do not use standard [items] list for components. Instead adds all components /// into [QuadTreeBroadphase] class. class QuadTreeCollisionDetection - extends StandardCollisionDetection> { + extends StandardCollisionDetection { QuadTreeCollisionDetection({ required Rect mapDimensions, required ExternalBroadphaseCheck onComponentTypeCheck, @@ -14,7 +14,7 @@ class QuadTreeCollisionDetection int maxObjects = 25, int maxDepth = 10, }) : super( - broadphase: QuadTreeBroadphase( + broadphase: QuadTreeBroadphase( mainBoxSize: mapDimensions, maxObjects: maxObjects, maxDepth: maxDepth, @@ -29,16 +29,16 @@ class QuadTreeCollisionDetection @override void add(ShapeHitbox item) { item.onAabbChanged = () => _scheduledUpdate.add(item); - // ignore: prefer_function_declarations_over_variables - final listenerCollisionType = () { + void listenerCollisionType() { if (item.isMounted) { if (item.collisionType == CollisionType.active) { - broadphase.activeCollisions.add(item); + broadphase.activeHitboxes.add(item); } else { - broadphase.activeCollisions.remove(item); + broadphase.activeHitboxes.remove(item); } } - }; + } + item.collisionTypeNotifier.addListener(listenerCollisionType); _listenerCollisionType[item] = listenerCollisionType; @@ -47,7 +47,9 @@ class QuadTreeCollisionDetection @override void addAll(Iterable items) { - items.forEach(add); + for (final item in items) { + add(item); + } } @override @@ -65,14 +67,16 @@ class QuadTreeCollisionDetection @override void removeAll(Iterable items) { broadphase.clear(); - items.forEach(remove); + for (final item in items) { + remove(item); + } } @override void run() { - _scheduledUpdate.forEach( - broadphase.updateTransform, - ); + for (final hitbox in _scheduledUpdate) { + broadphase.updateTransform(hitbox); + } _scheduledUpdate.clear(); super.run(); } diff --git a/packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart b/packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart index 18155e2bba4..86b2e33aa68 100644 --- a/packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart +++ b/packages/flame/lib/src/collisions/broadphase/sweep/sweep.dart @@ -6,8 +6,9 @@ class Sweep> extends Broadphase { @override final List items; - late final List _active = []; - late final Set> _potentials = {}; + final _active = []; + final _potentials = >{}; + final _prospectPool = ProspectPool(); @override void add(T item) => items.add(item); @@ -21,9 +22,10 @@ class Sweep> extends Broadphase { } @override - Set> query() { + Iterable> query() { _active.clear(); _potentials.clear(); + for (final item in items) { if (item.collisionType == CollisionType.inactive) { continue; @@ -40,7 +42,12 @@ class Sweep> extends Broadphase { if (activeBox.max.x >= currentMin) { if (item.collisionType == CollisionType.active || activeItem.collisionType == CollisionType.active) { - _potentials.add(CollisionProspect(item, activeItem)); + if (_prospectPool.length <= _potentials.length) { + _prospectPool.expand(item); + } + final prospect = _prospectPool[_potentials.length] + ..set(item, activeItem); + _potentials[prospect.hash] = prospect; } } else { _active.remove(activeItem); @@ -48,6 +55,6 @@ class Sweep> extends Broadphase { } _active.add(item); } - return _potentials; + return _potentials.values; } } diff --git a/packages/flame/lib/src/collisions/collision_detection.dart b/packages/flame/lib/src/collisions/collision_detection.dart index 704708caba4..42cde1b2ec6 100644 --- a/packages/flame/lib/src/collisions/collision_detection.dart +++ b/packages/flame/lib/src/collisions/collision_detection.dart @@ -12,7 +12,7 @@ abstract class CollisionDetection, final B broadphase; List get items => broadphase.items; - final Set> _lastPotentials = {}; + final _lastPotentials = >[]; CollisionDetection({required this.broadphase}); @@ -32,9 +32,11 @@ abstract class CollisionDetection, void run() { broadphase.update(); final potentials = broadphase.query(); - potentials.forEach((tuple) { - final itemA = tuple.a; - final itemB = tuple.b; + final hashes = Set.unmodifiable(potentials.map((p) => p.hash)); + + for (final potential in potentials) { + final itemA = potential.a; + final itemB = potential.b; if (itemA.possiblyIntersects(itemB)) { final intersectionPoints = intersections(itemA, itemB); @@ -49,18 +51,33 @@ abstract class CollisionDetection, } else if (itemA.collidingWith(itemB)) { handleCollisionEnd(itemA, itemB); } - }); + } // Handles callbacks for an ended collision that the broadphase didn't - // reports as a potential collision anymore. - _lastPotentials.difference(potentials).forEach((tuple) { - if (tuple.a.collidingWith(tuple.b)) { - handleCollisionEnd(tuple.a, tuple.b); + // report as a potential collision anymore. + for (final prospect in _lastPotentials) { + if (!hashes.contains(prospect.hash) && + prospect.a.collidingWith(prospect.b)) { + handleCollisionEnd(prospect.a, prospect.b); + } + } + _updateLastPotentials(potentials); + } + + final _lastPotentialsPool = >[]; + void _updateLastPotentials(Iterable> potentials) { + _lastPotentials.clear(); + for (final potential in potentials) { + final CollisionProspect lastPotential; + if (_lastPotentialsPool.length > _lastPotentials.length) { + lastPotential = _lastPotentialsPool[_lastPotentials.length] + ..setFrom(potential); + } else { + lastPotential = potential.clone(); + _lastPotentialsPool.add(lastPotential); } - }); - _lastPotentials - ..clear() - ..addAll(potentials); + _lastPotentials.add(lastPotential); + } } /// Check what the intersection points of two items are, diff --git a/packages/flame/test/collisions/collision_callback_test.dart b/packages/flame/test/collisions/collision_callback_test.dart index 854842e14a7..ce8b5d9d626 100644 --- a/packages/flame/test/collisions/collision_callback_test.dart +++ b/packages/flame/test/collisions/collision_callback_test.dart @@ -405,14 +405,16 @@ void main() { }, 'component collision callbacks are not called with hitbox ' 'triggersParentCollision option': (game) async { - final utilityHitboxA = TestHitbox()..triggersParentCollision = false; + final utilityHitboxA = TestHitbox('hitboxA') + ..triggersParentCollision = false; final blockA = TestBlock( Vector2.all(10), Vector2.all(10), ); blockA.add(utilityHitboxA); - final utilityHitboxB = TestHitbox()..triggersParentCollision = false; + final utilityHitboxB = TestHitbox('hitboxB') + ..triggersParentCollision = false; final blockB = TestBlock( Vector2.all(15), Vector2.all(10), diff --git a/packages/flame/test/collisions/collision_test_helpers.dart b/packages/flame/test/collisions/collision_test_helpers.dart index 4de8d6bc70b..2d3827b389e 100644 --- a/packages/flame/test/collisions/collision_test_helpers.dart +++ b/packages/flame/test/collisions/collision_test_helpers.dart @@ -53,8 +53,9 @@ class TestHitbox extends RectangleHitbox { int startCounter = 0; int onCollisionCounter = 0; int endCounter = 0; + String? name; - TestHitbox() { + TestHitbox([this.name]) { onCollisionCallback = (_, __) { onCollisionCounter++; }; @@ -65,6 +66,13 @@ class TestHitbox extends RectangleHitbox { endCounter++; }; } + + @override + String toString() { + return name == null + ? '_TestHitbox[${identityHashCode(this)}]' + : '_TestHitbox[$name]'; + } } class CompositeTestHitbox extends CompositeHitbox { diff --git a/packages/flame_lint/lib/analysis_options.yaml b/packages/flame_lint/lib/analysis_options.yaml index ee8e77e5bfc..eff1696bbca 100644 --- a/packages/flame_lint/lib/analysis_options.yaml +++ b/packages/flame_lint/lib/analysis_options.yaml @@ -104,7 +104,6 @@ linter: - prefer_final_in_for_each - prefer_final_locals - prefer_for_elements_to_map_fromIterable - - prefer_foreach - prefer_function_declarations_over_variables - prefer_generic_function_type_aliases - prefer_if_elements_to_conditional_expressions From 970babe97ea571bd3b2c39630d2c908a763c7661 Mon Sep 17 00:00:00 2001 From: Giles Correia Morton Date: Sat, 29 Jul 2023 14:15:11 +0100 Subject: [PATCH 15/15] docs: Minor updates to steps 3 & 4 in Klondike tutorial (#2626) I have just finished steps 1-4 of the Klondike tutorial and noticed a couple of minor things that look to be outdated with the current dart / flame versions. ### Step 3 - `late` keyword isn't required When following this step the analyser hightlight this rule [unnecessary_late](https://dart.dev/tools/linter-rules/unnecessary_late) (introduced in Dart 2.16.0) which states: > Top-level and static variables with initializers are already evaluated lazily as if they are marked late. I updated the notes around why the laziness is important so the reader can still understand the reasoning. --- doc/tutorials/klondike/step3.md | 27 ++++++++++++++------------- doc/tutorials/klondike/step4.md | 30 ++++++++++++++++-------------- 2 files changed, 30 insertions(+), 27 deletions(-) diff --git a/doc/tutorials/klondike/step3.md b/doc/tutorials/klondike/step3.md index 71124690b33..3669ad6aad9 100644 --- a/doc/tutorials/klondike/step3.md +++ b/doc/tutorials/klondike/step3.md @@ -55,13 +55,14 @@ symbol on the canvas. The sprite object is initialized using the ``` Then comes the static list of all `Suit` objects in the game. Note that we -define it as `late`, meaning that it will be only initialized the first time -it is needed. This is important: as we seen above, the constructor tries to -retrieve an image from the global cache, so it can only be invoked after the -image is loaded into the cache. +define it as static variable so it is evaluated lazily (as if it was marked +with the `late` keyword) meaning that it will be only initialized the first +time it is needed. This is important: as we can see above, the constructor +tries to retrieve an image from the global cache, so it can only be invoked +after the image is loaded into the cache. ```dart - static late final List _singletons = [ + static final List _singletons = [ Suit._(0, '♥', 1176, 17, 172, 183), Suit._(1, '♦', 973, 14, 177, 182), Suit._(2, '♣', 974, 226, 184, 172), @@ -120,7 +121,7 @@ class Rank { final Sprite redSprite; final Sprite blackSprite; - static late final List _singletons = [ + static final List _singletons = [ Rank._(1, 'A', 335, 164, 789, 161, 120, 129), Rank._(2, '2', 20, 19, 15, 322, 83, 125), Rank._(3, '3', 122, 19, 117, 322, 80, 127), @@ -272,7 +273,7 @@ Various properties used in the `_renderBack()` method are defined as follows: const Radius.circular(KlondikeGame.cardRadius), ); static final RRect backRRectInner = cardRRect.deflate(40); - static late final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501); + static final Sprite flameSprite = klondikeSprite(1367, 6, 357, 501); ``` I declared these properties as static because they will all be the same across @@ -307,9 +308,9 @@ depending on whether the card is of a "red" suit or "black": Next, we also need the images for the court cards: ```dart - static late final Sprite redJack = klondikeSprite(81, 565, 562, 488); - static late final Sprite redQueen = klondikeSprite(717, 541, 486, 515); - static late final Sprite redKing = klondikeSprite(1305, 532, 407, 549); + static final Sprite redJack = klondikeSprite(81, 565, 562, 488); + static final Sprite redQueen = klondikeSprite(717, 541, 486, 515); + static final Sprite redKing = klondikeSprite(1305, 532, 407, 549); ``` Note that I'm calling these sprites `redJack`, `redQueen`, and `redKing`. This @@ -325,11 +326,11 @@ blending mode: Color(0x880d8bff), BlendMode.srcATop, ); - static late final Sprite blackJack = klondikeSprite(81, 565, 562, 488) + static final Sprite blackJack = klondikeSprite(81, 565, 562, 488) ..paint = blueFilter; - static late final Sprite blackQueen = klondikeSprite(717, 541, 486, 515) + static final Sprite blackQueen = klondikeSprite(717, 541, 486, 515) ..paint = blueFilter; - static late final Sprite blackKing = klondikeSprite(1305, 532, 407, 549) + static final Sprite blackKing = klondikeSprite(1305, 532, 407, 549) ..paint = blueFilter; ``` diff --git a/doc/tutorials/klondike/step4.md b/doc/tutorials/klondike/step4.md index 2e9a0491879..00371993d0f 100644 --- a/doc/tutorials/klondike/step4.md +++ b/doc/tutorials/klondike/step4.md @@ -576,19 +576,21 @@ it so that it would check whether the card is allowed to be moved before startin ```dart void onDragStart(DragStartEvent event) { if (pile?.canMoveCard(this) ?? false) { - _isDragging = true; + super.onDragStart(event); priority = 100; } } ``` -We have also added the boolean `_isDragging` variable here: make sure to define it, and then to -check this flag in the `onDragUpdate()` method, and to set it back to false in the `onDragEnd()`: +We have also added a call to `super.onDragStart()` which sets an `_isDragged` variable to `true` +in the `DragCallbacks` mixin, we need to check this flag via the public `isDragged` getter in +the `onDragUpdate()` method and use `super.onDragEnd()` in `onDragEnd()` so the flag is set back +to `false`: ```dart @override void onDragUpdate(DragUpdateEvent event) { - if (!_isDragging) { + if (!isDragged) { return; } final cameraZoom = (findGame()! as FlameGame) @@ -600,11 +602,11 @@ check this flag in the `onDragUpdate()` method, and to set it back to false in t @override void onDragEnd(DragEndEvent event) { - _isDragging = false; + super.onDragEnd(event); } ``` -Now the only the proper cards can be dragged, but they still drop at random positions on the table, +Now only the proper cards can be dragged, but they still drop at random positions on the table, so let's work on that. @@ -620,10 +622,10 @@ Thus, my first attempt at revising the `onDragEnd` callback looks like this: ```dart @override void onDragEnd(DragEndEvent event) { - if (!_isDragging) { + if (!isDragged) { return; } - _isDragging = false; + super.onDragEnd(event); final dropPiles = parent! .componentsAtPoint(position + size / 2) .whereType() @@ -790,10 +792,10 @@ Now, putting this all together, the `Card`'s `onDragEnd` method will look like t ```dart @override void onDragEnd(DragEndEvent event) { - if (!_isDragging) { + if (!isDragged) { return; } - _isDragging = false; + super.onDragEnd(event); final dropPiles = parent! .componentsAtPoint(position + size / 2) .whereType() @@ -898,7 +900,7 @@ Heading back into the `Card` class, we can use this method in order to populate @override void onDragStart(DragStartEvent event) { if (pile?.canMoveCard(this) ?? false) { - _isDragging = true; + super.onDragStart(); priority = 100; if (pile is TableauPile) { attachedCards.clear(); @@ -918,7 +920,7 @@ the `onDragUpdate` method: ```dart @override void onDragUpdate(DragUpdateEvent event) { - if (!_isDragging) { + if (!isDragged) { return; } final cameraZoom = (findGame()! as FlameGame) @@ -952,10 +954,10 @@ attached cards into the pile, and the same when it comes to returning the cards ```dart @override void onDragEnd(DragEndEvent event) { - if (!_isDragging) { + if (!isDragged) { return; } - _isDragging = false; + super.onDragEnd(event); final dropPiles = parent! .componentsAtPoint(position + size / 2) .whereType()