UNPKG

@rpgjs/physic

Version:

A deterministic 2D top-down physics library for RPG, sandbox and MMO games

309 lines (308 loc) 9.78 kB
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