Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7604df2
feat: Promote PathComponent to a standard component.
adario Sep 19, 2026
2b79442
fix: Use white for the default hitboxStroke paint in PathComponent.
adario Sep 19, 2026
9ecc8f0
refactor: Use PathComponent.hitboxStroke in the examples.
adario Sep 19, 2026
73c5155
feat: Add granularity and hitboxesPriority to the PathComponent const…
adario Sep 19, 2026
886b412
feat: Add renderShape parameter in the PathComponent constructor.
adario Sep 19, 2026
312e74b
docs: Add documentation for PathComponent.
adario Sep 19, 2026
a89df6d
chore: Add test for PathComponent.
adario Sep 19, 2026
6055c4f
fix: Reject open contours before constructing PolygonHitbox.
adario Sep 20, 2026
24c69de
refactor: Rename the 'granularity' parameter in PathComponent to 'sam…
adario Sep 21, 2026
7001abe
feat: Add tolerance parameter in the PathComponent constructor.
adario Sep 21, 2026
70d5112
Merge branch 'main' into feat/path-component
adario Sep 24, 2026
3a673f5
feat: Add tolerance parameter in Polygon/Polygon{Component,Hitbox} co…
adario Sep 26, 2026
191ab97
refactor: [WIP] Move hitboxes out of PathComponent, into the new Path…
adario Sep 27, 2026
e51bde3
refactor: [WIP] Align PathComponent/Hitbox to PolygonComponent/Hitbox.
adario Sep 27, 2026
dbbd134
feat: Add filter parameter in PathComponent/Hitbox constructor.
adario Sep 27, 2026
793941e
Merge branch 'main' into feat/path-component
adario Sep 27, 2026
8bc981f
refactor: Move CollidablePathComponent in the examples to its own sou…
adario Sep 27, 2026
bd0e8d3
fix: Propagate missing overlappingRect parameter in *Path*Intersections.
adario Sep 27, 2026
d019b4d
refactor: Share the polygon containment, orientation and ray algorithms
spydon Sep 28, 2026
de0a2ac
refactor: Keep the PathComponent polygons as vertices and make PathHi…
spydon Sep 28, 2026
7243ad1
feat: Align the PathHitbox constructor with PolygonHitbox
spydon Sep 28, 2026
a3fb3f2
fix: Give CollidablePath in MultipleShapesExample a PathHitbox like t…
spydon Sep 28, 2026
b361f70
docs: Describe PathComponent as it is and add the PathHitbox section
spydon Sep 28, 2026
8af6411
test: Cover the PathHitbox aabb, containment, collisions and ray casting
spydon Sep 28, 2026
cf57e3b
Merge branch 'main' into feat/path-component
spydon Sep 28, 2026
14510ec
chore: Remove the adario entry from the cspell usernames dictionary
spydon Sep 28, 2026
d60c613
docs: Say when the CollidablePathComponent hitbox polygons are rendered
spydon Sep 28, 2026
7ebab5f
Merge branch 'main' into feat/path-component
adario Sep 28, 2026
4343ce1
docs: Describe PathComponent and PathHitbox as they are and document …
spydon Sep 28, 2026
1426788
refactor: Build the PathHitbox in each example instead of a shared Co…
spydon Sep 28, 2026
7e9d2c6
Merge branch 'main' into feat/path-component
adario Sep 28, 2026
88a5b98
Merge branch 'main' into feat/path-component
adario Sep 29, 2026
435abea
Merge branch 'main' into feat/path-component
spydon Sep 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions doc/flame/collision_detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,38 @@ follows the outline closely enough for your game. The contour is walked when the
so create it once and not in every tick.


### PathHitbox

A `PathHitbox` follows all the closed contours of a `Path` at once, with one polygon for each of
them, where `PolygonHitbox.fromPath` follows a single contour. The polygons form a single hitbox:
it collides, contains points and is hit by rays as a whole, whichever of its polygons is involved,
and it reports one collision to its parent even when several of its polygons touch the other
hitbox. A ray hits the nearest of the polygons.

The `PathHitbox` has the same constructor as the [](components/shape_components.md#pathcomponent),
see that section for the `sampling`, `tolerance` and `filter` arguments, and it takes the
`collisionType` like the other hitboxes. When rendered, it draws its polygons and not the path, so
that you can see what actually collides:

```dart
class Spaceship extends SpriteComponent with CollisionCallbacks {
Spaceship(this.outline);

final Path outline;

@override
Future<void> onLoad() async {
await super.onLoad();
add(PathHitbox(path: outline, sampling: 2));
}
}
```

Every polygon takes part in the collision detection, so a path with many contours or many vertices
costs accordingly. Keep the `filter` on unless the inner contours matter, and use the highest
`sampling` that still follows the outline closely enough.


### RectangleHitbox

The `RectangleHitbox` has the same constructors as the [](components/shape_components.md#rectanglecomponent),
Expand Down
51 changes: 44 additions & 7 deletions doc/flame/components/shape_components.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,20 +97,22 @@ The component gets the size of the contour, so that curves which reach the bound
not cut short. If no `position` is given the polygon ends up where the contour is in the coordinates
of the path.

There are two arguments that control how the path is followed:
There are three arguments that control how the path is followed:

- `contour`: A path has one contour for each shape that was added to it, and for each `moveTo`.
This is the index of the one that the polygon is made from, and it defaults to the first one. An
index that the path does not have results in a `RangeError`.
- `sampling`: The step that curves are followed with, in the units of the path, which defaults
to `1.0`. The polygon stays within about half of it from the path. A higher value gives fewer
vertices, which makes collision detection and ray casting cheaper, and a lower value follows the
curves more closely. A path that is defined in small units, like meters, needs a sampling that
is small compared to its size. Straight stretches cost the same whatever the sampling is.
to `1.0`. A higher value gives fewer vertices, which makes collision detection and ray casting
cheaper, and a lower value follows the curves more closely. A path that is defined in small
units, like meters, needs a sampling that is small compared to its size. Straight stretches cost
the same whatever the sampling is.
- `tolerance`: How far the polygon may stray from the path, in the units of the path. The samples
that are not needed to stay within it are left out. It defaults to half of the `sampling`.

The constructor is built on the `walkContours`, `walkContourAt` and `walkContour` extension methods
on `Path` and `PathMetric`, which return the vertices as lists of `Offset`s. Those also accept a
`tolerance`, in case the simplification of the sampled contour should not follow the sampling.
on `Path` and `PathMetric`, which take the same `sampling` and `tolerance` and return the vertices
as lists of `Offset`s.

```dart
void main() {
Expand All @@ -127,6 +129,41 @@ void main() {
```


## PathComponent

When a whole `Path` is needed instead of a single contour, a `PathComponent` renders the path as it
is and follows each of its closed contours with a polygon, in the same way as
`PolygonComponent.fromPath` follows one contour. The polygons decide whether a point is inside of
the component, so taps and drags only count on the shapes of the path and not in the space between
them. The component gets the size of the bounds of the path, and the path is moved so that those
bounds start at the origin of the component, so the anchor and the transforms apply to it like to
any other shape.

Using the previous two-contour `Path`, a `PathComponent` that renders and covers both shapes is
created like this:

```dart
void main() {
final path = Path()
..addOval(const Rect.fromLTWH(0, 0, 100, 60))
..addRect(const Rect.fromLTWH(200, 0, 50, 50));

final component = PathComponent(path: path);
}
```

The `sampling` and `tolerance` arguments control how the contours are followed, see
[](#from-a-path). The vertices of each polygon are available in `polygons`.

Contours with fewer than three vertices, like open lines, are rendered but do not become polygons.
By default, the polygons whose vertices all lie inside of the largest polygon are left out as well,
since the largest one already covers them; the eyes of a face are an example of this. Pass
`filter: false` to keep every polygon, for example when the inner contours should be hit by rays.

The `PathHitbox` is the hitbox counterpart of the `PathComponent`, see
[](../collision_detection.md#pathhitbox).


## RectangleComponent

A `RectangleComponent` is created very similarly to how a `PositionComponent` is created, since it
Expand Down
129 changes: 0 additions & 129 deletions examples/lib/commons/path_component.dart

This file was deleted.

22 changes: 16 additions & 6 deletions examples/lib/commons/paths.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:math';
import 'dart:ui';

import 'package:examples/commons/path_component.dart';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/palette.dart';
Expand Down Expand Up @@ -88,6 +88,7 @@ PathComponent pathComponent(
List<Paint>? paintLayers,
Paint? contourPaint,
bool? renderHitboxes,
bool? filter,
Anchor? anchor,
}) {
// Create a standard test path that fits within our chosen size with its
Expand All @@ -101,6 +102,7 @@ PathComponent pathComponent(
paintLayers: paintLayers,
contourPaint: contourPaint,
renderHitboxes: renderHitboxes,
filter: filter,
anchor: anchor,
);
}
Expand All @@ -114,22 +116,30 @@ PathComponent pathComponentWith(
List<Paint>? paintLayers,
Paint? contourPaint,
bool? renderHitboxes,
bool? filter,
Anchor? anchor,
}) {
// Adjust the path such that fits within our chosen size with its
// original aspect ratio.
final path = resize ? srcPath.resizeTo(size, keepRatio: true) : srcPath;

// Create a component that displays the whole path: we filter all hitboxes
// that are (approximately) fully enclosed in the largest one.
// The hitbox follows the same path as the component, so that the component
// collides and reacts to gestures as a whole. By default, the polygons that
// lie inside of the largest one are left out of both.
final hitbox = PathHitbox(path: path, filter: filter ?? true);
if (renderHitboxes ?? false) {
hitbox
..renderShape = true
..paint = contourPaint ?? whiteStroke;
}
return PathComponent(
path: path,
priority: shapePriority,
position: position ?? Vector2.zero(),
anchor: anchor ?? Anchor.center,
paint: paint ?? pathStroke,
paintLayers: paintLayers,
hitboxesPaint: contourPaint,
renderHitboxes: renderHitboxes ?? false,
)..renderShape = true;
filter: filter ?? true,
children: [hitbox],
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import 'dart:math';

import 'package:examples/commons/path_component.dart';
import 'package:examples/commons/paths.dart';
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
Expand Down Expand Up @@ -198,62 +197,22 @@ class CollidablePolygon extends MyCollidable {
}
}

class CollidablePath extends MyCollidable with CollisionPassthrough {
class CollidablePath extends MyCollidable {
CollidablePath(
super.position,
super.size,
super.velocity,
super.screenHitbox,
) {
// The path is centered on the origin, so the hitbox is placed in the
// middle of the component.
final path = randomPath(size.toSize());
_pathPaint = Paint.from(pathStroke)..color = defaultColor;
_component = pathComponentWith(
path,
size.toSize(),
paint: _pathPaint,
anchor: .center,
);
add(_component);
}

@override
bool containsLocalPoint(Vector2 point) {
var result = super.containsLocalPoint(point);
if (!result) {
final area = Rect.fromCenter(center: .zero, width: width, height: height);
result = area.containsPoint(point);
}
return result;
}

@override
void render(Canvas canvas) {
if (isDragged) {
canvas.drawCircle(.zero, 5, dragIndicatorPaint);
}
}

@override
void onCollisionStart(
List<Vector2> intersectionPoints,
PositionComponent other,
) {
super.onCollisionStart(intersectionPoints, other);
_pathPaint.color = other is ScreenHitbox ? screenColor : collisionColor;
}

@override
void onCollisionEnd(PositionComponent other) {
super.onCollisionEnd(other);
if (!isColliding) {
_pathPaint.color = defaultColor;
}
// The path keeps its aspect ratio within the size, so the hitbox is
// centered in the component.
hitbox = PathHitbox(
path: randomPath(size.toSize()),
position: size / 2,
anchor: Anchor.center,
)..renderShape = true;
add(hitbox!);
}

late final PathComponent _component;
late final Paint _pathPaint;
}

class CollidableRectangle extends MyCollidable {
Expand Down
Loading
Loading