Skip to content

Commit

Permalink
feat: Add a hitboxFilter argument to raycast() (#2968)
Browse files Browse the repository at this point in the history
Adds a new argument callback to all `raycast*` methods that lets the
user ignore hitboxes dynamically. The callback is called with every
prospective hitbox, and the hitbox is only considered when the callback
returns `true`.

This is faster, in the general case, than the current `ignoreHitboxes`
aproach. And it lets the developer have dynamic rules about what the
rays collide with. (For example, a line-of-fire raycast should take
friendlies into account, but could easily ignore other enemies that get
in the way.)



Closes #2966
  • Loading branch information
filiph committed Jan 9, 2024
1 parent 74309c1 commit d7c53e2
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 10 deletions.
39 changes: 30 additions & 9 deletions packages/flame/lib/src/collisions/collision_detection.dart
Original file line number Diff line number Diff line change
Expand Up @@ -101,15 +101,22 @@ abstract class CollisionDetection<T extends Hitbox<T>,
/// [maxDistance] can be provided to limit the raycast to only return hits
/// within this distance from the ray origin.
///
/// [ignoreHitboxes] can be used if you want to ignore certain hitboxes, i.e.
/// the rays will go straight through them. For example the hitbox of the
/// component that you might be casting the rays from.
/// You can provide a [hitboxFilter] callback to define which hitboxes
/// to consider and which to ignore. This callback will be called with
/// every prospective hitbox, and only if the callback returns `true`
/// will the hitbox be considered. Otherwise, the ray will go straight
/// through it. One common use case is ignoring the component that is
/// shooting the ray.
///
/// If you have a list of hitboxes to ignore in advance,
/// you can provide them via the [ignoreHitboxes] argument.
///
/// If [out] is provided that object will be modified and returned with the
/// result.
RaycastResult<T>? raycast(
Ray2 ray, {
double? maxDistance,
bool Function(T candidate)? hitboxFilter,
List<T>? ignoreHitboxes,
RaycastResult<T>? out,
});
Expand All @@ -127,9 +134,15 @@ abstract class CollisionDetection<T extends Hitbox<T>,
/// If there are less objects in [rays] than the operation requires, the
/// missing [Ray2] objects will be created and added to [rays].
///
/// [ignoreHitboxes] can be used if you want to ignore certain hitboxes, i.e.
/// the rays will go straight through them. For example the hitbox of the
/// component that you might be casting the rays from.
/// You can provide a [hitboxFilter] callback to define which hitboxes
/// to consider and which to ignore. This callback will be called with
/// every prospective hitbox, and only if the callback returns `true`
/// will the hitbox be considered. Otherwise, the ray will go straight
/// through it. One common use case is ignoring the component that is
/// shooting the ray.
///
/// If you have a list of hitboxes to ignore in advance,
/// you can provide them via the [ignoreHitboxes] argument.
///
/// If [out] is provided the [RaycastResult]s in that list be modified and
/// returned with the result. If there are less objects in [out] than the
Expand All @@ -141,6 +154,7 @@ abstract class CollisionDetection<T extends Hitbox<T>,
double sweepAngle = tau,
double? maxDistance,
List<Ray2>? rays,
bool Function(T candidate)? hitboxFilter,
List<T>? ignoreHitboxes,
List<RaycastResult<T>>? out,
});
Expand All @@ -152,16 +166,23 @@ abstract class CollisionDetection<T extends Hitbox<T>,
/// [maxDepth] is how many times the ray should collide before returning a
/// result, defaults to 10.
///
/// [ignoreHitboxes] can be used if you want to ignore certain hitboxes, i.e.
/// the rays will go straight through them. For example the hitbox of the
/// component that you might be casting the rays from.
/// You can provide a [hitboxFilter] callback to define which hitboxes
/// to consider and which to ignore. This callback will be called with
/// every prospective hitbox, and only if the callback returns `true`
/// will the hitbox be considered. Otherwise, the ray will go straight
/// through it. One common use case is ignoring the component that is
/// shooting the ray.
///
/// If you have a list of hitboxes to ignore in advance,
/// you can provide them via the [ignoreHitboxes] argument.
///
/// If [out] is provided the [RaycastResult]s in that list be modified and
/// returned with the result. If there are less objects in [out] than the
/// result requires, the missing [RaycastResult] objects will be created.
Iterable<RaycastResult<T>> raytrace(
Ray2 ray, {
int maxDepth = 10,
bool Function(T candidate)? hitboxFilter,
List<T>? ignoreHitboxes,
List<RaycastResult<T>>? out,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>>
RaycastResult<ShapeHitbox>? raycast(
Ray2 ray, {
double? maxDistance,
bool Function(ShapeHitbox candidate)? hitboxFilter,
List<ShapeHitbox>? ignoreHitboxes,
RaycastResult<ShapeHitbox>? out,
}) {
Expand All @@ -81,6 +82,11 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>>
if (ignoreHitboxes?.contains(item) ?? false) {
continue;
}
if (hitboxFilter != null) {
if (!hitboxFilter(item)) {
continue;
}
}
if (!item.aabb.intersectsWithAabb2(_temporaryRayAabb)) {
continue;
}
Expand Down Expand Up @@ -109,6 +115,7 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>>
double sweepAngle = tau,
double? maxDistance,
List<Ray2>? rays,
bool Function(ShapeHitbox candidate)? hitboxFilter,
List<ShapeHitbox>? ignoreHitboxes,
List<RaycastResult<ShapeHitbox>>? out,
}) {
Expand Down Expand Up @@ -140,6 +147,7 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>>
result = raycast(
ray,
maxDistance: maxDistance,
hitboxFilter: hitboxFilter,
ignoreHitboxes: ignoreHitboxes,
out: result,
);
Expand All @@ -155,6 +163,7 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>>
Iterable<RaycastResult<ShapeHitbox>> raytrace(
Ray2 ray, {
int maxDepth = 10,
bool Function(ShapeHitbox candidate)? hitboxFilter,
List<ShapeHitbox>? ignoreHitboxes,
List<RaycastResult<ShapeHitbox>>? out,
}) sync* {
Expand All @@ -166,6 +175,7 @@ class StandardCollisionDetection<B extends Broadphase<ShapeHitbox>>
hasResultObject ? out![i] : RaycastResult<ShapeHitbox>();
final currentResult = raycast(
currentRay,
hitboxFilter: hitboxFilter,
ignoreHitboxes: ignoreHitboxes,
out: storeResult,
);
Expand Down
33 changes: 32 additions & 1 deletion packages/flame/test/collisions/collision_detection_test.dart
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import 'package:flame/collisions.dart';
import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/geometry.dart';
import 'package:flame/geometry.dart' as geometry;
import 'package:flame/geometry.dart';
import 'package:flame_test/flame_test.dart';
import 'package:test/test.dart';

Expand Down Expand Up @@ -1094,6 +1094,37 @@ void main() {
closeToVector(Vector2(-1, 1)..normalize()),
);
},
'multiple hitboxes after each other with filter':
(collisionSystem) async {
final game = collisionSystem as FlameGame;
final world = game.world;
await world.ensureAddAll([
for (var i = 0.0; i < 10; i++)
PositionComponent(
position: Vector2.all(100 + i * 10),
size: Vector2.all(20 - i),
anchor: Anchor.center,
)..add(RectangleHitbox()),
]);
await game.ready();
final ray = Ray2(
origin: Vector2.zero(),
direction: Vector2.all(1)..normalize(),
);
final result = collisionSystem.collisionDetection.raycast(
ray,
hitboxFilter: (hitbox) => hitbox.parent != world.children.first,
);
expect(result?.hitbox?.parent, game.world.children.toList()[1]);
expect(
result?.reflectionRay?.origin,
closeToVector(Vector2.all(100.5)),
);
expect(
result?.reflectionRay?.direction,
closeToVector(Vector2(-1, 1)..normalize()),
);
},
'ray with origin on hitbox corner': (collisionSystem) async {
final game = collisionSystem as FlameGame;
final world = game.world;
Expand Down

0 comments on commit d7c53e2

Please sign in to comment.