From a35218be390da039df721b9d18c2f6ffb8f6dd57 Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Mon, 12 Feb 2024 20:16:11 -0600 Subject: [PATCH] feat: Add additional rayCast options `ignoreCollisionGroupAll` and `filter: (hit: RayCastHit) => boolean` --- CHANGELOG.md | 3 ++ .../DynamicTreeCollisionProcessor.ts | 28 ++++++++++- src/spec/PhysicsWorldSpec.ts | 47 +++++++++++++++++++ 3 files changed, 77 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de5583c4..acf11edf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,9 @@ This project adheres to [Semantic Versioning](http://semver.org/). ### Added +- Added additional options in rayCast options + * `ignoreCollisionGroupAll: boolean` will ignore testing against anything with the `CollisionGroup.All` which is the default for all + * `filter: (hit: RayCastHit) => boolean` will allow people to do arbitrary filtering on raycast results, this runs very last after all other collision group/collision mask decisions have been made - Added additional data `side` and `lastContact` to `onCollisionEnd` and `collisionend` events - Added configuration option to `ex.PhysicsConfig` to configure composite collider onCollisionStart/End behavior - Added configuration option to `ex.TileMap({ meshingLookBehind: Infinity })` which allows users to configure how far the TileMap looks behind for matching colliders (default is 10). diff --git a/src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts b/src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts index a912bdb12..30d8b607a 100644 --- a/src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts +++ b/src/engine/Collision/Detection/DynamicTreeCollisionProcessor.ts @@ -34,6 +34,21 @@ export interface RayCastOptions { * Optionally specify to search for all colliders that intersect the ray cast, not just the first which is the default */ searchAllColliders?: boolean; + /** + * Optionally ignore things with CollisionGroup.All and only test against things with an explicit group + * + * Default false + */ + ignoreCollisionGroupAll?: boolean; + + /** + * Optionally provide a any filter function to filter on arbitrary qualities of a ray cast hit + * + * Filters run after any collision mask/collision group filtering, it is the last decision + * + * Returning true means you want to include the collider in your results, false means exclude it + */ + filter?: (hit: RayCastHit) => boolean; } /** @@ -65,6 +80,10 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor { const owner = collider.owner; const maybeBody = owner.get(BodyComponent); + if (options?.ignoreCollisionGroupAll && maybeBody.group === CollisionGroup.All) { + return false; + } + const canCollide = (collisionMask & maybeBody.group.category) !== 0; // Early exit if not the right group @@ -73,8 +92,15 @@ export class DynamicTreeCollisionProcessor implements CollisionProcessor { } const hit = collider.rayCast(ray, maxDistance); + if (hit) { - results.push(hit); + if (options?.filter) { + if (options.filter(hit)) { + results.push(hit); + } + } else { + results.push(hit); + } if (!searchAllColliders) { // returning true exits the search return true; diff --git a/src/spec/PhysicsWorldSpec.ts b/src/spec/PhysicsWorldSpec.ts index 41cd6323b..9d0816a54 100644 --- a/src/spec/PhysicsWorldSpec.ts +++ b/src/spec/PhysicsWorldSpec.ts @@ -117,4 +117,51 @@ describe('A physics world', () => { expect(hits[0].distance).toBe(75); expect(hits[0].point).toEqual(ex.vec(75, 0)); }); + + it('can rayCast with ignoreCollisionGroupAll, returns 1 hit', () => { + const sut = TestUtils.engine(); + const actor1 = new ex.Actor({x: 100, y: 0, width: 50, height: 50}); + sut.currentScene.add(actor1); + const actor2 = new ex.Actor({x: 200, y: 0, width: 50, height: 50}); + sut.currentScene.add(actor2); + const actor3 = new ex.Actor({x: 300, y: 0, width: 50, height: 50, collisionGroup: new ex.CollisionGroup('test', 0b1, ~0b1)}); + sut.currentScene.add(actor3); + + const ray = new ex.Ray(ex.vec(0, 0), ex.Vector.Right); + const hits = sut.currentScene.physics.rayCast(ray, { + searchAllColliders: true, + collisionMask: 0b1, + ignoreCollisionGroupAll: true + }); + + expect(hits.length).toBe(1); + expect(hits[0].body).toEqual(actor3.body); + expect(hits[0].collider).toEqual(actor3.collider.get()); + expect(hits[0].distance).toBe(275); + expect(hits[0].point).toEqual(ex.vec(275, 0)); + }); + + it('can rayCast with filter, returns 1 hit', () => { + const sut = TestUtils.engine(); + const actor1 = new ex.Actor({x: 100, y: 0, width: 50, height: 50}); + sut.currentScene.add(actor1); + const actor2 = new ex.Actor({x: 200, y: 0, width: 50, height: 50}); + sut.currentScene.add(actor2); + const actor3 = new ex.Actor({x: 300, y: 0, width: 50, height: 50, collisionGroup: new ex.CollisionGroup('test', 0b1, ~0b1)}); + sut.currentScene.add(actor3); + + const ray = new ex.Ray(ex.vec(0, 0), ex.Vector.Right); + const hits = sut.currentScene.physics.rayCast(ray, { + searchAllColliders: true, + filter: (hit) => { + return hit.body.group.name === 'test'; + } + }); + + expect(hits.length).toBe(1); + expect(hits[0].body).toEqual(actor3.body); + expect(hits[0].collider).toEqual(actor3.collider.get()); + expect(hits[0].distance).toBe(275); + expect(hits[0].point).toEqual(ex.vec(275, 0)); + }); }); \ No newline at end of file