From d68740e94cd00e43ff2c92e1e1c5b7259036ecce Mon Sep 17 00:00:00 2001 From: Erik Onarheim Date: Tue, 23 Jul 2024 23:21:47 -0500 Subject: [PATCH] fix: CircleCollider tangent raycast did not work correctly --- CHANGELOG.md | 1 + .../Collision/Colliders/CircleCollider.ts | 25 ++++++++++++------- src/engine/Math/util.ts | 8 ++++++ src/spec/CollisionShapeSpec.ts | 19 ++++++++++++++ 4 files changed, 44 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fb603439a..ec3239304 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -70,6 +70,7 @@ are doing mtv adjustments during precollision. ### Fixed +- Fixed issue where CircleCollider tangent raycast did not work correctly - Fixed issue where you were required to provide a transition if you provided a loader in the `ex.Engine.start('scene', { loader })` - Fixed issue where `ex.Scene.onPreLoad(loader: ex.DefaultLoader)` would lock up the engine if there was an empty loader - Fixed issue where `ex.Scene` scoped input events would preserve state and get stuck causing issues when switching back to the original scene. diff --git a/src/engine/Collision/Colliders/CircleCollider.ts b/src/engine/Collision/Colliders/CircleCollider.ts index d735e8e8c..0bbbccb18 100644 --- a/src/engine/Collision/Colliders/CircleCollider.ts +++ b/src/engine/Collision/Colliders/CircleCollider.ts @@ -17,6 +17,7 @@ import { Transform } from '../../Math/transform'; import { AffineMatrix } from '../../Math/affine-matrix'; import { BodyComponent } from '../Index'; import { RayCastHit } from '../Detection/RayCastHit'; +import { approximatelyEqual } from '../../Math/util'; export interface CircleColliderOptions { /** @@ -115,20 +116,24 @@ export class CircleCollider extends Collider { * @param ray */ public rayCast(ray: Ray, max: number = Infinity): RayCastHit | null { - //https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection - const c = this.center; - const dir = ray.dir; - const orig = ray.pos; + // https://en.wikipedia.org/wiki/Intersection_(geometry)#A_line_and_a_circle + const c = this.center; //? + const dir = ray.dir; //? + const orig = ray.pos; //? - const discriminant = Math.sqrt(Math.pow(dir.dot(orig.sub(c)), 2) - Math.pow(orig.sub(c).distance(), 2) + Math.pow(this.radius, 2)); + const u = c.sub(orig); - if (discriminant < 0) { - // no intersection + const u1 = dir.scale(u.dot(dir)); + const u2 = u.sub(u1); + + const d = u2.size; + + if (d > this.radius) { return null; } else { - let toi = 0; // tangent case - if (discriminant === 0) { + let toi = 0; + if (approximatelyEqual(d, this.radius, 0.0001)) { toi = -dir.dot(orig.sub(c)); if (toi > 0 && toi < max) { const point = ray.getPoint(toi); @@ -143,6 +148,8 @@ export class CircleCollider extends Collider { return null; } else { // two point + const discriminant = Math.sqrt(Math.pow(dir.dot(orig.sub(c)), 2) - Math.pow(orig.sub(c).distance(), 2) + Math.pow(this.radius, 2)); + const toi1 = -dir.dot(orig.sub(c)) + discriminant; const toi2 = -dir.dot(orig.sub(c)) - discriminant; diff --git a/src/engine/Math/util.ts b/src/engine/Math/util.ts index 4572fc594..d84898cc3 100644 --- a/src/engine/Math/util.ts +++ b/src/engine/Math/util.ts @@ -34,6 +34,14 @@ export function clamp(val: number, min: number, max: number) { return Math.min(Math.max(min, val), max); } +/** + * Approximately equals + */ +export function approximatelyEqual(val1: number, val2: number, tolerance: number) { + // https://dev.to/alldanielscott/how-to-compare-numbers-correctly-in-javascript-1l4i + return Math.abs(val1 - val2) < tolerance; +} + /** * Convert an angle to be the equivalent in the range [0, 2PI] */ diff --git a/src/spec/CollisionShapeSpec.ts b/src/spec/CollisionShapeSpec.ts index f5b26cacf..9f69a1568 100644 --- a/src/spec/CollisionShapeSpec.ts +++ b/src/spec/CollisionShapeSpec.ts @@ -181,6 +181,25 @@ describe('Collision Shape', () => { expect(point.y).toBe(0); }); + it('can be raycast at a tangent', () => { + const circle = new ex.CircleCollider({ + offset: ex.vec(10, -5), + radius: 5 + }); + const ray = new ex.Ray(new ex.Vector(0, 0), ex.Vector.Right.clone()); + const ray2 = new ex.Ray(new ex.Vector(5, 0), ex.Vector.Up.clone()); + + const hit = circle.rayCast(ray); + + expect(hit.point.x).toBe(10); + expect(hit.point.y).toBe(0); + + const hit2 = circle.rayCast(ray2); + + expect(hit2.point.x).toBe(5); + expect(hit2.point.y).toBe(-5); + }); + it("doesn't have axes", () => { // technically circles have infinite axes expect(circle.axes).toEqual([]);