UNPKG

@dominicstop/utils

Version:

Yet another event emitter written in typescript.

264 lines (263 loc) 10.1 kB
import { Vector2D } from "../geometry"; import { DampingForce } from "./DampingForce"; import { isSomeParticleForce } from "./SomeParticleForce"; import { isSomeSystemForce } from "./SomeSystemForce"; export class PhysicsEngine { constructor() { this.particles = []; this.particleMetadataMap = {}; this.particleForces = []; this.systemForces = []; this.worldBounds = null; this.gravity = Vector2D.zero; this.restitutionCoefficient = 0.6; this.collisionIterations = 1; this.dampingFactor = 0.98; if (this.dampingFactor < 1) { const dampingForce = new DampingForce(this.dampingFactor); this.addForce(dampingForce); } ; } addParticle(particle) { this.particles.push(particle); } ; removeParticle(particleOrId) { const id = typeof particleOrId === "string" ? particleOrId : particleOrId.id; const index = this.particles.findIndex(p => p.id === id); if (index <= -1) { return false; } ; this.particles.splice(index, 1); return true; } ; getParticleById(id) { return this.particles.find(p => p.id === id); } ; addForce(force) { if (isSomeParticleForce(force)) { this.particleForces.push(force); } else if (isSomeSystemForce(force)) { this.systemForces.push(force); } ; } ; removeForce(force) { const forceList = (() => { if (isSomeParticleForce(force)) { return this.particleForces; } ; if (isSomeSystemForce(force)) { return this.systemForces; } ; return undefined; })(); if (forceList == null) { return false; } ; const index = forceList.indexOf(force); if (index !== -1) { forceList.splice(index, 1); return true; } ; return false; } ; update(deltaTime) { for (const particle of this.particles) { particle.resetAcceleration(); if (!particle.isStatic) { particle.applyForce(this.gravity.multipliedByScalar(particle.mass)); } } for (const force of this.systemForces) { force.applyToAll(this.particles); } for (const force of this.particleForces) { for (const particle of this.particles) { force.apply(particle); } } for (const particle of this.particles) { particle.update(deltaTime); if (this.worldBounds) { this.handleBoundaryConditions(particle); } ; } ; for (let i = 0; i < this.collisionIterations; i++) { this.resolveCollisions(); } ; } resolveCollisions() { // loop through all possible pairs of particles // e.g. `[01, 02, 03, ..., 41, 42...]` for (let i = 0; i < this.particles.length; i++) { const particleA = this.particles[i]; for (let j = i + 1; j < this.particles.length; j++) { const particleB = this.particles[j]; if (particleA.isStatic && particleB.isStatic) continue; // check if the two particles are colliding, // only continue if they have collision const hasCollision = particleA.isCollidingWithOther(particleB); if (!hasCollision) continue; // calculate the amount of overlap const overlapVector = particleA.computeOverlapVectorWith(particleB); if (overlapVector.isZero) continue; // calculate the total inverse mass (used for distributing correction) // skip if total is 0 (i.e. to avoid division by zero) const totalInverseMass = particleA.inverseMass + particleB.inverseMass; if (totalInverseMass === 0) continue; // compute adj amount // i.e. how much each particle should move to resolve the overlap const correctionVector = overlapVector.dividedByScalar(totalInverseMass); // add position correction to `particleA` (if needed) particleA.setPosition((() => { if (particleA.isStatic) { return particleA.position; } ; // * remember: `correctionVector` is the total overlap that needs to be // resolved/min. // // * each particle should move proportionally to its inverse mass (in other words: // lighter particles move more). // // * so scale the correction vector by `particleA.inverseMass` // to get particleA's share of the movement. // const positionAdjScaled = correctionVector.multipliedByScalar(particleA.inverseMass); return particleA.position.addedWithOther(positionAdjScaled); })()); // add position correction for `particleB` particleB.setPosition((() => { if (particleB.isStatic) { return particleB.position; } ; const positionAdjustmentB = correctionVector.multipliedByScalar(particleB.inverseMass); return particleB.position.subtractedWithOther(positionAdjustmentB); })()); // get delta of particle velocity // calculate relative velocity between the two particles const relativeVelocity = particleA.velocity.subtractedWithOther(particleB.velocity); // normalize to get the direction of the collision const collisionDirection = overlapVector.normalized; // scale/re-projecr relative velocity towards collision // direction (collision normal) const velocityAlongCollisionDirection = relativeVelocity.dotProductWithOtherVector(collisionDirection); // skip if particles are moving apart if (velocityAlongCollisionDirection > 0) continue; // compute velocity const impulse = (() => { // calculate impulse scalar using restitution (bounciness/elasticity) const restitution = this.restitutionCoefficient; const impulseAmount = -(1 + restitution) * velocityAlongCollisionDirection / totalInverseMass; // calculate the impulse vector (direction + amount) return collisionDirection.multipliedByScalar(impulseAmount); })(); // apply impulse to `particleA`'s velocity particleA.setVelocity((() => { if (particleA.isStatic) { return particleA.velocity; } ; const velocityScaled = impulse.multipliedByScalar(particleA.inverseMass); return particleA.velocity.addedWithOther(velocityScaled); })()); // apply impulse to `particleB`'s velocity particleB.setVelocity((() => { if (particleB.isStatic) { return particleB.velocity; } ; const velocityScaled = impulse.multipliedByScalar(particleB.inverseMass); return particleB.velocity.subtractedWithOther(velocityScaled); })()); } } } handleBoundaryConditions(particle) { if (!this.worldBounds || particle.isStatic) return; const bounds = this.worldBounds; const pos = particle.position; const vel = particle.velocity; let bounced = false; if (pos.dx < bounds.minX) { pos.dx = bounds.minX; vel.dx *= -this.restitutionCoefficient; bounced = true; } else if (pos.dx > bounds.maxX) { pos.dx = bounds.maxX; vel.dx *= -this.restitutionCoefficient; bounced = true; } if (pos.dy < bounds.minY) { pos.dy = bounds.minY; vel.dy *= -this.restitutionCoefficient; bounced = true; } else if (pos.dy > bounds.maxY) { pos.dy = bounds.maxY; vel.dy *= -this.restitutionCoefficient; bounced = true; } if (bounced) { particle.setPosition(pos); particle.setVelocity(vel); } } clear() { this.particles = []; this.particleForces = []; this.systemForces = []; } getTotalKineticEnergy() { return this.particles.reduce((sum, p) => sum + p.getKineticEnergy(), 0); } logState() { for (const p of this.particles) { console.log(`Particle ${p.id}: pos=(${p.position.dx.toFixed(2)}, ${p.position.dy.toFixed(2)}), vel=(${p.velocity.dx.toFixed(2)}, ${p.velocity.dy.toFixed(2)})`); } } getParticleCount() { return this.particles.length; } setGravity(gravity) { this.gravity = gravity; } setWorldBounds(bounds) { this.worldBounds = bounds; } setRestitutionCoefficient(restitution) { this.restitutionCoefficient = restitution; } setCollisionIterations(iterations) { this.collisionIterations = iterations; } checkIfAllParticlesAtRest() { return this.particles.every(p => p.checkIsAtRest()); } ; }