UNPKG

@dominicstop/utils

Version:

Yet another event emitter written in typescript.

306 lines (239 loc) 9.15 kB
import { Rect, Vector2D } from "../geometry"; import { AnyParticle } from "./AnyParticle"; import { DampingForce } from "./DampingForce"; import { isSomeParticleForce, SomeParticleForce } from "./SomeParticleForce"; import { ParticleMetadataMap, SomePhysicsEngine } from "./SomePhysicsEngine"; import { isSomeSystemForce, SomeSystemForce } from "./SomeSystemForce"; export class PhysicsEngine< SomeParticle extends AnyParticle > implements SomePhysicsEngine<SomeParticle> { particles: Array<SomeParticle> = []; particleMetadataMap: ParticleMetadataMap = {}; particleForces: Array<SomeParticleForce> = []; systemForces: Array<SomeSystemForce> = []; worldBounds: Rect | null = null; gravity: Vector2D = Vector2D.zero; restitutionCoefficient: number = 0.6; collisionIterations: number = 1; dampingFactor: number = 0.98; constructor() { if(this.dampingFactor < 1) { const dampingForce = new DampingForce(this.dampingFactor); this.addForce(dampingForce); }; } addParticle(particle: SomeParticle): void { this.particles.push(particle); }; removeParticle(particleOrId: SomeParticle | string): boolean { 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: string): SomeParticle | undefined { return this.particles.find(p => p.id === id); }; addForce(force: SomeParticleForce | SomeSystemForce): void { if (isSomeParticleForce(force)) { this.particleForces.push(force); } else if (isSomeSystemForce(force)) { this.systemForces.push(force); }; }; removeForce(force: SomeParticleForce | SomeSystemForce): boolean { 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 as any); if (index !== -1) { forceList.splice(index, 1); return true; }; return false; }; update(deltaTime: number): void { 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(): void { // 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: Vector2D = (() => { // 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: SomeParticle): void { 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(): void { this.particles = []; this.particleForces = []; this.systemForces = []; } getTotalKineticEnergy(): number { return this.particles.reduce((sum, p) => sum + p.getKineticEnergy(), 0); } logState(): void { 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(): number { return this.particles.length; } setGravity(gravity: Vector2D): void { this.gravity = gravity; } setWorldBounds(bounds: Rect | null): void { this.worldBounds = bounds; } setRestitutionCoefficient(restitution: number): void { this.restitutionCoefficient = restitution; } setCollisionIterations(iterations: number): void { this.collisionIterations = iterations; } checkIfAllParticlesAtRest(): boolean { return this.particles.every(p => p.checkIsAtRest()); }; }