This document describes the custom physics engine implementation for the Plinko game, based on advanced computational dynamics principles. The engine replaces Matter.js with a lightweight, purpose-built solution that implements Semi-Implicit Euler integration, fixed timestep accumulation, impulse-based collision resolution, and spatial partitioning.
Each rigid body in the simulation is represented by a state vector:
S = {r, v, θ, ω}
Where:
r = position vector (x, y)v = velocity vector (vx, vy)θ = angle (radians)ω = angular velocity (radians/second)The derivative of this state follows Newton’s laws:
dS/dt = {v, a, ω, α}
Where:
a = acceleration = F/m (from F = ma)α = angular acceleration = τ/I (from τ = Iα)The engine uses Semi-Implicit Euler (also called Symplectic Euler) integration, which is superior to Explicit Euler for physics simulation:
Explicit Euler (UNSTABLE):
x_{t+Δt} = x_t + v_t * Δt
v_{t+Δt} = v_t + a_t * Δt
Semi-Implicit Euler (STABLE):
v_{t+Δt} = v_t + a_t * Δt
x_{t+Δt} = x_t + v_{t+Δt} * Δt // Use NEW velocity
Key Advantage: Semi-Implicit Euler is symplectic, meaning it conserves the phase-space volume. This prevents energy from exploding (like Explicit Euler) or damping (like Implicit Euler). The system energy oscillates around the true value but remains bounded.
Variable timesteps cause numerical instability and non-deterministic behavior. The engine implements a fixed timestep accumulator:
accumulator += frameTime;
while (accumulator >= fixedDeltaTime) {
step(fixedDeltaTime); // Fixed Δt = 1/60 seconds
accumulator -= fixedDeltaTime;
}
alpha = accumulator / fixedDeltaTime; // For interpolation
Benefits:
alpha enables smooth renderingTraditional force-based collision handling can fail for stiff constraints (like hard walls). The engine uses impulse-based resolution:
Impulse Definition:
J = ∫ F dt = Δp = m * Δv
Therefore:
Δv = J/m
Collision Response Algorithm:
v_rel = v_B - v_A
v_normal = v_rel · n
j = -(1 + e) * v_normal / (1/m_A + 1/m_B)
Where e is restitution (bounciness)
v_A = v_A - (j * n) / m_A
v_B = v_B + (j * n) / m_B
t = perpendicular(n)
j_friction = min(μ * j, -v_tangent * (1/m_A + 1/m_B))
Naive collision detection is O(N²). With N=100 objects, that’s 4,950 checks per frame. The engine uses Spatial Hashing to reduce this to ~O(N).
Hash Function:
H(x, y) = (x * p1 + y * p2) | 0
Where p1 = 73856093 and p2 = 19349663 are large primes.
Algorithm:
gravity = { x: 0, y: 980 } // pixels/second²
At canvas scale (~100 pixels/meter): 980 pixels/s² ≈ 9.8 m/s² (Earth gravity)
Pegs (Hard Plastic):
Balls (Metal):
These values were chosen to match real-world physics:
| Metric | Value | Notes |
|---|---|---|
| Collision Detection | O(N) average | Spatial hash with 50px cells |
| Integration | O(N) | Linear in number of bodies |
| Frame Budget | 16.6ms | 60 FPS target |
| Typical Bodies | 50-100 | Pegs + active balls |
| Memory | ~2KB/body | Lightweight JS objects |
| Feature | Custom Engine | Matter.js |
|---|---|---|
| Integration | Semi-Implicit Euler | Verlet-like |
| Timestep | Fixed (accumulator) | Variable |
| Collision | Impulse-based | Constraint solver |
| Size | ~21KB | ~500KB |
| Complexity | Simple | Complex |
| Determinism | Guaranteed | Good |
| Performance | Excellent | Good |
The engine has been validated through:
physics-engine.js
├── Vec2 // 2D vector mathematics
├── RigidBody // Body state and properties
├── SpatialHash // Broad-phase collision detection
└── PhysicsEngine // Main simulation loop
├── update() // Fixed timestep accumulator
├── step() // Semi-Implicit Euler integration
├── detectCollisions() // Broad + narrow phase
└── resolveCollision() // Impulse-based resolution
Potential improvements (not currently implemented):