@rpgjs/physic
Version:
A deterministic 2D top-down physics library for RPG, sandbox and MMO games
230 lines (229 loc) • 6.86 kB
JavaScript
import { Vector2 } from "./index2.js";
import { AABB } from "./index4.js";
import { CircleCollider } from "./index11.js";
import { AABBCollider } from "./index12.js";
class CapsuleCollider {
constructor(entity) {
this.entity = entity;
}
getBounds() {
const { radius, height } = this.getCapsuleConfig();
const pos = this.entity.position;
const halfHeight = Math.max(0, height / 2 - radius);
return new AABB(
pos.x - radius,
pos.y - halfHeight - radius,
pos.x + radius,
pos.y + halfHeight + radius
);
}
getEntity() {
return this.entity;
}
raycast(ray) {
const bounds = this.getBounds();
const tMin = (bounds.minX - ray.origin.x) / ray.direction.x;
const tMax = (bounds.maxX - ray.origin.x) / ray.direction.x;
const tymin = (bounds.minY - ray.origin.y) / ray.direction.y;
const tymax = (bounds.maxY - ray.origin.y) / ray.direction.y;
const t1 = Math.min(tMin, tMax);
const t2 = Math.max(tMin, tMax);
const t3 = Math.min(tymin, tymax);
const t4 = Math.max(tymin, tymax);
const tNear = Math.max(t1, t3);
const tFar = Math.min(t2, t4);
if (tNear > tFar || tFar < 0) return null;
if (tNear > ray.length) return null;
const t = tNear < 0 ? tFar : tNear;
if (t < 0) return null;
const point = ray.getPoint(t);
let normal = new Vector2(0, 0);
if (Math.abs(point.x - bounds.minX) < 1e-5) normal.set(-1, 0);
else if (Math.abs(point.x - bounds.maxX) < 1e-5) normal.set(1, 0);
else if (Math.abs(point.y - bounds.minY) < 1e-5) normal.set(0, -1);
else if (Math.abs(point.y - bounds.maxY) < 1e-5) normal.set(0, 1);
return {
entity: this.entity,
point,
normal,
distance: t
};
}
testCollision(other) {
if (other instanceof CircleCollider) {
return this.testCircle(other);
} else if (other instanceof AABBCollider) {
return this.testAABB(other);
} else if (other instanceof CapsuleCollider) {
return this.testCapsule(other);
}
return other.testCollision(this);
}
getContactPoints(other) {
const collision = this.testCollision(other);
return collision ? collision.contacts : [];
}
getCapsuleConfig() {
if (this.entity.capsule) {
return this.entity.capsule;
}
return { radius: this.entity.radius || 10, height: this.entity.height || 30 };
}
getSegment() {
const { radius, height } = this.getCapsuleConfig();
const pos = this.entity.position;
const halfSegment = Math.max(0, height / 2 - radius);
return {
a: new Vector2(pos.x, pos.y - halfSegment),
b: new Vector2(pos.x, pos.y + halfSegment)
};
}
testCircle(circle) {
const seg = this.getSegment();
const circleCenter = circle.getCenter();
const circleRadius = circle.getRadius();
const capRadius = this.getCapsuleConfig().radius;
const closest = this.closestPointOnSegment(seg.a, seg.b, circleCenter);
const distSq = closest.distanceToSquared(circleCenter);
const minDist = capRadius + circleRadius;
if (distSq > minDist * minDist) {
return null;
}
const dist = Math.sqrt(distSq);
const normal = dist > 0 ? circleCenter.sub(closest).normalize() : new Vector2(1, 0);
const depth = minDist - dist;
return {
entityA: this.entity,
entityB: circle.getEntity(),
// Convention: Normal points from A to B.
// Here A is Capsule, B is Circle.
// Vector from closest (on capsule) to center (circle) is B - A.
// So normal should be (circleCenter - closest).normalized().
// Wait, if I return normal, it should be the direction to push B out of A?
// Usually normal points from A to B.
// Let's stick to: normal points from A to B.
contacts: [{
point: closest.add(normal.mul(capRadius)),
normal,
depth
}],
normal,
depth
};
}
testAABB(box) {
const seg = this.getSegment();
const capRadius = this.getCapsuleConfig().radius;
const boxBounds = box.getBounds();
const closestOnSeg = this.closestPointOnSegment(seg.a, seg.b, box.getBounds().getCenter());
const closestOnBox = boxBounds.clamp(closestOnSeg);
const distSq = closestOnSeg.distanceToSquared(closestOnBox);
if (distSq > capRadius * capRadius) {
return null;
}
const dist = Math.sqrt(distSq);
const normal = dist > 0 ? closestOnBox.sub(closestOnSeg).normalize() : new Vector2(0, 1);
const depth = capRadius - dist;
return {
entityA: this.entity,
entityB: box.getEntity(),
contacts: [{
point: closestOnBox,
normal,
depth
}],
normal,
depth
};
}
testCapsule(other) {
const segA = this.getSegment();
const segB = other.getSegment();
const rA = this.getCapsuleConfig().radius;
const rB = other.getCapsuleConfig().radius;
const { p1, p2 } = this.closestPointsSegmentSegment(segA.a, segA.b, segB.a, segB.b);
const distSq = p1.distanceToSquared(p2);
const minDist = rA + rB;
if (distSq > minDist * minDist) {
return null;
}
const dist = Math.sqrt(distSq);
const normal = dist > 0 ? p2.sub(p1).normalize() : new Vector2(1, 0);
const depth = minDist - dist;
return {
entityA: this.entity,
entityB: other.getEntity(),
contacts: [{
point: p1.add(normal.mul(rA)),
normal,
depth
}],
normal,
depth
};
}
closestPointOnSegment(a, b, p) {
const ab = b.sub(a);
const t = p.sub(a).dot(ab) / ab.dot(ab);
return a.add(ab.mul(Math.max(0, Math.min(1, t))));
}
// http://geomalgorithms.com/a07-_distance.html
closestPointsSegmentSegment(p1, p2, p3, p4) {
const u = p2.sub(p1);
const v = p4.sub(p3);
const w = p1.sub(p3);
const a = u.dot(u);
const b = u.dot(v);
const c = v.dot(v);
const d = u.dot(w);
const e = v.dot(w);
const D = a * c - b * b;
let sc, sN, sD = D;
let tc, tN, tD = D;
if (D < 1e-8) {
sN = 0;
sD = 1;
tN = e;
tD = c;
} else {
sN = b * e - c * d;
tN = a * e - b * d;
if (sN < 0) {
sN = 0;
tN = e;
tD = c;
} else if (sN > sD) {
sN = sD;
tN = e + b;
tD = c;
}
}
if (tN < 0) {
tN = 0;
if (-d < 0) sN = 0;
else if (-d > a) sN = sD;
else {
sN = -d;
sD = a;
}
} else if (tN > tD) {
tN = tD;
if (-d + b < 0) sN = 0;
else if (-d + b > a) sN = sD;
else {
sN = -d + b;
sD = a;
}
}
sc = Math.abs(sN) < 1e-8 ? 0 : sN / sD;
tc = Math.abs(tN) < 1e-8 ? 0 : tN / tD;
return {
p1: p1.add(u.mul(sc)),
p2: p3.add(v.mul(tc))
};
}
}
export {
CapsuleCollider
};
//# sourceMappingURL=index13.js.map