UNPKG

2d-physics-engine

Version:

A lightweight, flexible 2D physics engine with ECS architecture, built with TypeScript

213 lines 9.76 kB
import { CollisionDetector } from './CollisionDetector'; import { CollisionResolver } from './CollisionResolver'; import { Collider } from '../../components/ColliderComponents/Collider.abstract'; import { Rigidbody } from '../../components/Rigidbody.component'; import { Transform } from '../../components/Transform.component'; import { System } from '../System.abstract'; import { Vector2 } from '../../math/Vector2'; export class Physics extends System { constructor() { super(...arguments); Object.defineProperty(this, "needsFixedUpdate", { enumerable: true, configurable: true, writable: true, value: true }); Object.defineProperty(this, "collisionDetector", { enumerable: true, configurable: true, writable: true, value: new CollisionDetector() }); Object.defineProperty(this, "collisionResolver", { enumerable: true, configurable: true, writable: true, value: new CollisionResolver() }); Object.defineProperty(this, "currentCollisions", { enumerable: true, configurable: true, writable: true, value: new Map() }); } update(deltaTime, scene) { const entities = scene.getEntities(); // Step 0: Detect and resolve collisions after updating positions const collisions = this.detectCollisions(entities); this.handleCollisionEvents(collisions, entities); this.resolveCollisions(collisions); for (const entity of entities) { const transform = entity.getComponent(Transform); const rigidbody = entity.getComponent(Rigidbody); if (!transform || !rigidbody) continue; // Step 1: Calculate new velocities from accumulated forces this.integrateForces(rigidbody, deltaTime); // Step 2: Update positions using new velocities this.integrateVelocities(transform, rigidbody, deltaTime); // Step 4: Clear accumulated forces for next frame rigidbody.clearForces(); } } integrateForces(rigidbody, deltaTime) { const velocity = rigidbody.getVelocity(); // Apply friction force based on current velocity if (velocity.getMagnitude() !== 0) { // Clamp friction between 0 and 1 const frictionCoeff = Math.max(0, Math.min(1, rigidbody.getFriction())); const frictionForce = velocity.getNormal().scale(-frictionCoeff * velocity.getMagnitude()); rigidbody.addForce(frictionForce); } // Update linear velocity from all forces (including friction) const acceleration = rigidbody.getAccumulatedForces().scale(rigidbody.getInverseMass()); const deltaV = acceleration.scale(deltaTime); rigidbody.setVelocity(velocity.add(deltaV)); // Angular velocity decay with clamped friction const angularVelocity = rigidbody.getAngularVelocity(); if (angularVelocity !== 0) { const frictionCoeff = Math.max(0, Math.min(1, rigidbody.getFriction())); const angularFriction = angularVelocity * (1 - frictionCoeff * deltaTime); rigidbody.setAngularVelocity(angularFriction); } } integrateVelocities(transform, rigidbody, deltaTime) { // Update position const deltaPosition = rigidbody.getVelocity().scale(deltaTime); transform.setPosition(transform.getPosition().add(deltaPosition)); // Update rotation (if you're tracking rotation in Transform) const deltaRotation = rigidbody.getAngularVelocity() * deltaTime; transform.setRotation(transform.getRotation() + deltaRotation); } detectCollisions(entities) { const collisions = []; for (let i = 0; i < entities.length - 1; i++) { const entityA = entities[i]; const colliderA = entityA.getComponent(Collider); const transformA = entityA.getComponent(Transform); const rigidbodyA = entityA.getComponent(Rigidbody); if (!colliderA || !transformA) continue; for (let j = i + 1; j < entities.length; j++) { const entityB = entities[j]; const colliderB = entityB.getComponent(Collider); const transformB = entityB.getComponent(Transform); const rigidbodyB = entityB.getComponent(Rigidbody); if (!colliderB || !transformB) continue; const collisionInfo = this.collisionDetector.detectCollision(transformA, transformB, colliderA, colliderB); if (!collisionInfo) continue; collisions.push({ colliderA, colliderB, transformA, transformB, info: collisionInfo, rigidbodyA, rigidbodyB, entityA, // Add entity info for collision events entityB, }); } } return collisions; } resolveCollisions(collisions) { for (const collision of collisions) { this.collisionResolver.resolveCollision(collision); } } handleCollisionEvents(collisions, entities) { const newCollisions = new Map(); // Process current collisions and detect entry events for (const collision of collisions) { // Get entities from collision if available, otherwise find them const entityA = collision.entityA || this.findEntityWithCollider(entities, collision.colliderA); const entityB = collision.entityB || this.findEntityWithCollider(entities, collision.colliderB); if (!entityA || !entityB) continue; const pair = this.getCollisionPair(entityA, entityB, collision.colliderA, collision.colliderB); // Store this collision newCollisions.set(pair, { colliderA: collision.colliderA, colliderB: collision.colliderB, entityA, entityB, transformA: collision.transformA, transformB: collision.transformB, }); // Create collision events for both colliders const eventA = { otherEntity: entityB, otherCollider: collision.colliderB, otherTransform: collision.transformB, collisionInfo: collision.info, }; const eventB = { otherEntity: entityA, otherCollider: collision.colliderA, otherTransform: collision.transformA, collisionInfo: { ...collision.info, normal: collision.info.normal.scale(-1), // Reverse normal for B's perspective }, }; // Check if this is a new collision (entry event) if (!this.currentCollisions.has(pair)) { // Fire onCollideEntry for both colliders collision.colliderA.onCollideEntry?.(eventA); collision.colliderB.onCollideEntry?.(eventB); } else { // Fire onCollideStay for ongoing collisions collision.colliderA.onCollideStay?.(eventA); collision.colliderB.onCollideStay?.(eventB); } } // Detect exit events (collisions that existed before but not now) for (const [pair, storedCollision] of this.currentCollisions.entries()) { if (!newCollisions.has(pair)) { // This collision ended - fire exit events // Fire onCollideExit for colliderA const eventA = { otherEntity: storedCollision.entityB, otherCollider: storedCollision.colliderB, otherTransform: storedCollision.transformB, collisionInfo: { normal: new Vector2(0, 0), point: new Vector2(0, 0), penetration: 0, }, }; storedCollision.colliderA.onCollideExit?.(eventA); // Fire onCollideExit for colliderB const eventB = { otherEntity: storedCollision.entityA, otherCollider: storedCollision.colliderA, otherTransform: storedCollision.transformA, collisionInfo: { normal: new Vector2(0, 0), point: new Vector2(0, 0), penetration: 0, }, }; storedCollision.colliderB.onCollideExit?.(eventB); } } // Update current collisions for next frame this.currentCollisions = newCollisions; } getCollisionPair(entityA, entityB, colliderA, colliderB) { // Create a unique pair key using entity names and collider IDs (order-independent) const entityKeyA = `${entityA.name}:${colliderA.componentId.toString()}`; const entityKeyB = `${entityB.name}:${colliderB.componentId.toString()}`; return entityKeyA < entityKeyB ? `${entityKeyA}:${entityKeyB}` : `${entityKeyB}:${entityKeyA}`; } findEntityWithCollider(entities, collider) { return entities.find((entity) => entity.getComponent(Collider) === collider); } } //# sourceMappingURL=Physics.system.js.map