@rpgjs/physic
Version:
A deterministic 2D top-down physics library for RPG, sandbox and MMO games
309 lines (308 loc) • 9.78 kB
JavaScript
import { AABB } from "./index4.js";
import { Vector2 } from "./index2.js";
import { Entity } from "./index7.js";
import { AABBCollider } from "./index12.js";
import { CircleCollider } from "./index11.js";
const entityToPolygonConfig = /* @__PURE__ */ new WeakMap();
function assignPolygonCollider(entity, config) {
entityToPolygonConfig.set(entity, config);
}
class PolygonCollider {
// local-space convex polygons
constructor(entity) {
this.entity = entity;
const cfg = entityToPolygonConfig.get(entity);
if (!cfg) {
this.convexParts = [];
return;
}
if (cfg.parts && cfg.parts.length > 0) {
this.convexParts = cfg.parts.map((p) => p.slice());
} else if (cfg.vertices && cfg.vertices.length >= 3) {
this.convexParts = [cfg.vertices.slice()];
} else {
this.convexParts = [];
}
}
getEntity() {
return this.entity;
}
getBounds() {
const rotation = this.entity.rotation;
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
let minX = Number.POSITIVE_INFINITY;
let minY = Number.POSITIVE_INFINITY;
let maxX = Number.NEGATIVE_INFINITY;
let maxY = Number.NEGATIVE_INFINITY;
for (const part of this.convexParts) {
for (const v of part) {
const x = v.x * cos - v.y * sin + this.entity.position.x;
const y = v.x * sin + v.y * cos + this.entity.position.y;
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
}
}
if (minX === Number.POSITIVE_INFINITY) {
minX = maxX = this.entity.position.x;
minY = maxY = this.entity.position.y;
}
return new AABB(minX, minY, maxX, maxY);
}
getContactPoints(other) {
const info = this.testCollision(other);
return info ? info.contacts : [];
}
testCollision(other) {
if (other instanceof CircleCollider) {
return this.testPolygonCircle(other);
}
if (other instanceof AABBCollider) {
return this.testPolygonAABB(other);
}
if (other instanceof PolygonCollider) {
return this.testPolygonPolygon(other);
}
return null;
}
testPolygonAABB(other) {
const b = other.getBounds();
const halfW = (b.maxX - b.minX) / 2;
const halfH = (b.maxY - b.minY) / 2;
const center = new Vector2((b.minX + b.maxX) / 2, (b.minY + b.maxY) / 2);
const aabbPolyLocal = [
new Vector2(-halfW, -halfH),
new Vector2(halfW, -halfH),
new Vector2(halfW, halfH),
new Vector2(-halfW, halfH)
];
const tempEntity = new Entity({ position: center, rotation: 0, mass: 0 });
entityToPolygonConfig.set(tempEntity, { vertices: aabbPolyLocal, isConvex: true });
const tempPoly = new PolygonCollider(tempEntity);
const result = this.testPolygonPolygon(tempPoly);
if (!result) return null;
return {
entityA: result.entityA === tempEntity ? this.entity : result.entityA,
entityB: other.getEntity(),
contacts: result.contacts,
normal: result.normal,
depth: result.depth
};
}
testPolygonCircle(other) {
let bestInfo = null;
const center = other.getCenter();
const r = other.getRadius();
for (const part of this.getWorldParts()) {
let minDistSq = Number.POSITIVE_INFINITY;
let closestPoint = null;
for (let i = 0; i < part.length; i++) {
const a = part[i];
const b = part[(i + 1) % part.length];
if (!a || !b) continue;
const cp = closestPointOnSegment(a, b, center);
const dsq = center.distanceToSquared(cp);
if (dsq < minDistSq) {
minDistSq = dsq;
closestPoint = cp;
}
}
if (!closestPoint) continue;
const dist = Math.sqrt(minDistSq);
if (dist <= r) {
const normal = dist > 1e-6 ? center.sub(closestPoint).normalize() : new Vector2(1, 0);
const depth = r - dist;
const info = {
entityA: this.entity,
entityB: other.getEntity(),
contacts: [{ point: closestPoint, normal, depth }],
normal,
depth
};
if (!bestInfo || info.depth > bestInfo.depth) bestInfo = info;
}
}
return bestInfo;
}
testPolygonPolygon(other) {
let bestDepth = Number.POSITIVE_INFINITY;
let bestAxis = null;
let flipNormal = false;
const partsA = this.getWorldParts();
const partsB = other.getWorldParts();
let collided = false;
for (const a of partsA) {
for (const b of partsB) {
const axes = gatherSATAxes(a, b);
let overlapDepth = Number.POSITIVE_INFINITY;
let axisForPair = null;
for (const axis of axes) {
const projA = projectOntoAxis(a, axis);
const projB = projectOntoAxis(b, axis);
const overlap = intervalOverlap(projA.min, projA.max, projB.min, projB.max);
if (overlap <= 0) {
overlapDepth = -1;
break;
}
if (overlap < overlapDepth) {
overlapDepth = overlap;
axisForPair = axis;
}
}
if (overlapDepth > 0 && axisForPair) {
collided = true;
if (overlapDepth < bestDepth) {
bestDepth = overlapDepth;
bestAxis = axisForPair;
const cA = polygonCentroid(a);
const cB = polygonCentroid(b);
flipNormal = cA.sub(cB).dot(bestAxis) > 0;
}
}
}
}
if (!collided || !bestAxis) return null;
const normal = flipNormal ? bestAxis.mul(-1) : bestAxis.clone();
const depth = bestDepth;
const centroidA = polygonCentroid(partsA[0]);
const contactPoint = centroidA.add(normal.mul(0));
return {
entityA: this.entity,
entityB: other.getEntity(),
contacts: [{ point: contactPoint, normal, depth }],
normal,
depth
};
}
/**
* @inheritdoc
*/
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;
let closestHit = null;
const parts = this.getWorldParts();
for (const part of parts) {
for (let i = 0; i < part.length; i++) {
const p1 = part[i];
const p2 = part[(i + 1) % part.length];
if (!p1 || !p2) continue;
const hit = this.rayCastSegment(ray, p1, p2);
if (hit) {
if (!closestHit || hit.distance < closestHit.distance) {
closestHit = hit;
}
}
}
}
return closestHit;
}
rayCastSegment(ray, p1, p2) {
const v1 = ray.origin;
const v2 = ray.origin.add(ray.direction);
const v3 = p1;
const v4 = p2;
const den = (v1.x - v2.x) * (v3.y - v4.y) - (v1.y - v2.y) * (v3.x - v4.x);
if (den === 0) return null;
const t = ((v1.x - v3.x) * (v3.y - v4.y) - (v1.y - v3.y) * (v3.x - v4.x)) / den;
const u = -((v1.x - v2.x) * (v1.y - v3.y) - (v1.y - v2.y) * (v1.x - v3.x)) / den;
if (t >= 0 && t <= ray.length && u >= 0 && u <= 1) {
const point = new Vector2(
v1.x + t * (v2.x - v1.x),
v1.y + t * (v2.y - v1.y)
);
const segmentDir = p2.sub(p1).normalize();
let normal = new Vector2(-segmentDir.y, segmentDir.x);
if (normal.dot(ray.direction) > 0) {
normal = normal.mul(-1);
}
return {
entity: this.entity,
point,
normal,
distance: t
};
}
return null;
}
getWorldParts() {
const rotation = this.entity.rotation;
const cos = Math.cos(rotation);
const sin = Math.sin(rotation);
const px = this.entity.position.x;
const py = this.entity.position.y;
const worldParts = [];
for (const part of this.convexParts) {
const w = new Array(part.length);
for (let i = 0; i < part.length; i++) {
const v = part[i];
if (!v) continue;
w[i] = new Vector2(v.x * cos - v.y * sin + px, v.x * sin + v.y * cos + py);
}
worldParts.push(w);
}
return worldParts;
}
}
function closestPointOnSegment(a, b, p) {
const ab = b.sub(a);
const ap = p.sub(a);
const t = Math.max(0, Math.min(1, ap.dot(ab) / ab.dot(ab)));
return a.add(ab.mul(t));
}
function gatherSATAxes(a, b) {
const axes = [];
const pushAxis = (p, i) => {
const p0 = p[i];
const p1 = p[(i + 1) % p.length];
if (!p0 || !p1) return;
const edge = p1.sub(p0);
const axis = new Vector2(-edge.y, edge.x).normalize();
axes.push(axis);
};
for (let i = 0; i < a.length; i++) pushAxis(a, i);
for (let i = 0; i < b.length; i++) pushAxis(b, i);
return axes;
}
function projectOntoAxis(poly, axis) {
let min = Number.POSITIVE_INFINITY;
let max = Number.NEGATIVE_INFINITY;
for (const v of poly) {
const p = v.dot(axis);
if (p < min) min = p;
if (p > max) max = p;
}
return { min, max };
}
function intervalOverlap(minA, maxA, minB, maxB) {
return Math.min(maxA, maxB) - Math.max(minA, minB);
}
function polygonCentroid(poly) {
let cx = 0;
let cy = 0;
for (const v of poly) {
cx += v.x;
cy += v.y;
}
const n = poly.length > 0 ? poly.length : 1;
return new Vector2(cx / n, cy / n);
}
export {
PolygonCollider,
assignPolygonCollider,
entityToPolygonConfig
};
//# sourceMappingURL=index19.js.map