-
-
Notifications
You must be signed in to change notification settings - Fork 1k
feat: Promote PathComponent to a standard component #4050
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7604df2
2b79442
9ecc8f0
73c5155
886b412
312e74b
a89df6d
6055c4f
24c69de
7001abe
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,7 +2,6 @@ import 'dart:async'; | |
| import 'dart:ui'; | ||
|
|
||
| import 'package:collection/collection.dart'; | ||
| import 'package:examples/commons/paths.dart'; | ||
| import 'package:flame/collisions.dart'; | ||
| import 'package:flame/extensions.dart'; | ||
| import 'package:flame/geometry.dart'; | ||
|
|
@@ -16,6 +15,9 @@ class PathComponent extends ShapeComponent | |
| with CollisionCallbacks, CollisionPassthrough { | ||
| PathComponent({ | ||
| required Path path, | ||
| this.sampling = 1.0, | ||
| this.tolerance, | ||
| this.hitboxesPriority, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should not live here, the hitboxes should handle their own priority. |
||
| this.addHitboxes = false, | ||
| this.loadHitboxes = true, | ||
| this.renderHitboxes = false, | ||
|
|
@@ -30,16 +32,35 @@ class PathComponent extends ShapeComponent | |
| super.key, | ||
| super.paint, | ||
| super.paintLayers, | ||
| bool renderShape = true, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This shouldn't be here, since it doesn't exist on |
||
| }) : path = path.toOrigin, | ||
| super(size: path.getBounds().size.toVector2()) { | ||
| this.renderShape = renderShape; | ||
| if (addHitboxes) { | ||
| _addHitboxes(); | ||
| } | ||
| } | ||
|
|
||
| /// The default paint used to render hitboxes. | ||
| static Paint hitboxStroke = Paint() | ||
| ..color = const Color(0xffffffff) | ||
| ..style = .stroke; | ||
|
Comment on lines
+44
to
+47
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should follow the same standard for painting this as the other shape components, like |
||
|
|
||
| /// The path to display, already rooted at the origin. | ||
| final Path path; | ||
|
|
||
| /// The step used when sampling the path contours that generate | ||
| /// the hitboxes. | ||
| final double sampling; | ||
|
|
||
| /// The tolerance used when sampling the path contours; if not specified, | ||
| /// it defaults to half the [sampling]. | ||
| final double? tolerance; | ||
|
|
||
| /// The hitboxes priority: if not specified, by default the hitboxes | ||
| /// use a relative priority of 1. | ||
| final int? hitboxesPriority; | ||
|
|
||
| /// Whether the hitboxes are added right away, in the constructor. | ||
| final bool addHitboxes; | ||
|
|
||
|
|
@@ -56,7 +77,7 @@ class PathComponent extends ShapeComponent | |
| final Paint? hitboxesPaint; | ||
|
|
||
| var _hitboxesAdded = false; | ||
| late final _hitboxes = _hitboxesFor(path); | ||
| late final _hitboxes = _createHitboxes(); | ||
|
|
||
| @override | ||
| FutureOr<void> onLoad() async { | ||
|
|
@@ -93,6 +114,7 @@ class PathComponent extends ShapeComponent | |
| _hitboxesAdded = true; | ||
| } | ||
|
|
||
| // Filter the hitboxes by keeping only the largest and all disjoint ones. | ||
| List<PolygonHitbox> _filterHitboxes(List<PolygonHitbox> hitboxes) { | ||
| if (hitboxes.length < 2) { | ||
| return hitboxes; | ||
|
|
@@ -116,14 +138,21 @@ class PathComponent extends ShapeComponent | |
| return hitboxes; | ||
| } | ||
|
|
||
| List<PolygonHitbox> _hitboxesFor(Path path) { | ||
| final count = path.contours.length; | ||
| return [ | ||
| for (var contour = 0; contour < count; contour++) | ||
| PolygonHitbox.fromPath(path, contour: contour) | ||
| ..priority = priority + 1 | ||
| ..paint = hitboxesPaint ?? whiteStroke | ||
| ..renderShape = renderHitboxes, | ||
| ]; | ||
| // Create a hitbox for each path contour with at least three vertices. | ||
| List<PolygonHitbox> _createHitboxes() { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The component should not create hitboxes for itself, we probably need a |
||
| final contours = path.walkContours(sampling, tolerance); | ||
| final boxes = <PolygonHitbox>[]; | ||
| for (var index = 0; index < contours.length; index++) { | ||
| final contour = contours[index]; | ||
| if (contour.length > 2) { | ||
| boxes.add( | ||
| PolygonHitbox(contour.vertices) | ||
| ..priority = hitboxesPriority ?? priority + 1 | ||
| ..paint = hitboxesPaint ?? hitboxStroke | ||
| ..renderShape = renderHitboxes, | ||
| ); | ||
| } | ||
| } | ||
| return boxes; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,88 @@ | ||
| import 'dart:math'; | ||
| import 'dart:ui'; | ||
|
|
||
| import 'package:flame/components.dart'; | ||
| import 'package:flame/src/collisions/hitboxes/polygon_hitbox.dart'; | ||
| import 'package:flame_test/test_paths.dart'; | ||
| import 'package:test/test.dart'; | ||
|
|
||
| void main() { | ||
| test('PathComponent roundRect preserves the path bounds', () { | ||
| const size = Size(64, 64); | ||
| final path = TestPaths.byName('roundRect', size); | ||
|
|
||
| final pathComponent = PathComponent(path: path); | ||
|
|
||
| expect(pathComponent.width, closeTo(size.width, 1e-10)); | ||
| expect(pathComponent.height, closeTo(size.height, 1e-10)); | ||
| }); | ||
|
|
||
| test('PathComponent flame preserves the path bounds', () { | ||
| const size = Size(64, 64); | ||
| final path = TestPaths.byName('flame', size); | ||
| final pathSize = path.getBounds().size; | ||
|
|
||
| final pathComponent = PathComponent(path: path); | ||
|
|
||
| expect(pathComponent.width, pathSize.width); | ||
| expect(pathComponent.height, pathSize.height); | ||
| }); | ||
|
|
||
| test('PathComponent invader1 preserves the aspect ratio', () { | ||
| const size = Size(64, 64); | ||
| final path = TestPaths.byName('invader1', size); | ||
|
|
||
| final invader1 = TestPaths.invader1(); | ||
| final invader1Size = invader1.getBounds().size; | ||
| final scaleX = size.width / invader1Size.width; | ||
| final scaleY = size.height / invader1Size.height; | ||
| final scale = min(scaleX, scaleY); | ||
|
|
||
| final pathComponent = PathComponent(path: path); | ||
| expect(pathComponent.width, closeTo(invader1Size.width * scale, 1e-6)); | ||
| expect(pathComponent.height, closeTo(invader1Size.height * scale, 1e-6)); | ||
| }); | ||
|
|
||
| test('PathComponent invader2 keeps only one disjoint contour', () { | ||
| const size = Size(64, 64); | ||
| final path = TestPaths.byName('invader2', size); | ||
|
|
||
| final pathComponent = PathComponent(path: path, addHitboxes: true); | ||
|
|
||
| expect(pathComponent.children.length, 1); | ||
| }); | ||
|
|
||
| test('PathComponent invader2 explicitly keeps all disjoint contours', () { | ||
| const size = Size(64, 64); | ||
| final path = TestPaths.byName('invader2', size); | ||
|
|
||
| final pathComponent = PathComponent( | ||
| path: path, | ||
| addHitboxes: true, | ||
| filterHitboxes: false, | ||
| ); | ||
|
|
||
| expect(pathComponent.children.length, 3); | ||
| }); | ||
|
|
||
| test('PathComponent alien2 implicitly keeps all disjoint contours', () { | ||
| const size = Size(64, 64); | ||
| final path = TestPaths.byName('alien2', size); | ||
|
|
||
| final pathComponent = PathComponent(path: path, addHitboxes: true); | ||
|
|
||
| expect(pathComponent.children.length, 4); | ||
| }); | ||
|
|
||
| test('PathComponent invader3 respects the given tolerance', () { | ||
| const size = Size(64, 64); | ||
| final path = TestPaths.byName('invader3', size); | ||
|
|
||
| final path1 = PathComponent(path: path, addHitboxes: true); | ||
| final path2 = PathComponent(path: path, addHitboxes: true, tolerance: 1); | ||
| final hitbox1 = path1.firstChild<PolygonHitbox>(); | ||
| final hitbox2 = path2.firstChild<PolygonHitbox>(); | ||
|
|
||
| expect(hitbox1!.vertices.length, greaterThan(hitbox2!.vertices.length)); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Follow
PolygonComponentto see which arguments that should exist on here, remember this is no longer an example helper when moved in here, so it needs to be consistent with the rest of the code.