Skip to content

Commit

Permalink
docs: Add more guidance to collision detection algorithm choices (#2624)
Browse files Browse the repository at this point in the history
This is adding a bit more precise language and more context to API docs
of `HasCollisionDetection`, `Broadphase` and
`HasQuadTreeCollisionDetection`.
  • Loading branch information
filiph authored Jul 26, 2023
1 parent b4f6e27 commit 781e898
Show file tree
Hide file tree
Showing 4 changed files with 86 additions and 44 deletions.
103 changes: 64 additions & 39 deletions doc/flame/collision_detection.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down Expand Up @@ -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 {
Expand All @@ -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:

Expand All @@ -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:

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
15 changes: 10 additions & 5 deletions packages/flame/lib/src/collisions/broadphase/broadphase.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<T extends Hitbox<T>> {
Broadphase();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<B extends Broadphase<ShapeHitbox>> on Component {
CollisionDetection<ShapeHitbox, B> _collisionDetection =
StandardCollisionDetection();
Expand Down

0 comments on commit 781e898

Please sign in to comment.