Build Your Own Physics Engine
"Realistic physics in games is the art of approximating reality just enough that nobody notices the lies." — Erin Catto (paraphrased)
A 2D physics engine is the cleanest path into computational geometry, numerical integration, and constraint solving. By the end you have an engine that handles gravity, collisions, friction, and stacking — enough for a rigid-body sandbox or a 2D game.
1. Overview & motivation
A rigid-body physics engine cycles through three stages every frame:
1. Integrate velocity from forces, position from velocity
2. Detect collisions broad phase + narrow phase
3. Resolve apply impulses to separate and stop interpenetration
What you can only learn by building one:
- Why explicit Euler integration is unstable and why physics engines use semi-implicit or symplectic methods.
- Why collision detection is two problems: broad phase (which pairs might collide?) and narrow phase (do they actually?).
- Why sequential impulses (Erin Catto's algorithm, used in Box2D) is the dominant constraint solver in game physics.
- Why stable stacking is much harder than colliding two objects.
- Why physics determinism is a real cross-platform challenge.
2. Where this fits in the degree
- Phase: Production
- Semester: Capstone elective
- Modules deepened: Sem 8 Module 4 (performance) — physics is hot-loop code.
Cross-phase relevance:
- Pairs naturally with the Game Engine tutorial.
- Uses linear algebra from Sem 1 and exercises numerical methods.
3. Prerequisites
- Linear algebra: vectors, dot product, cross product (in 2D scalar form), 2x2 matrices.
- Calculus refresher: derivatives, Newton's laws.
- C++ or Rust. JavaScript works for browser demos.
- A graphics library to see what you're doing (SDL2, raylib, browser canvas).
4. Theory & research
Required reading
- Randy Gaul, "How to Create a Custom Physics Engine" (tutsplus.com series, archived) — clean, concise walkthrough. ⭐ recommended primary.
- Erin Catto, "Iterative Dynamics with Temporal Coherence" (GDC slides) — the foundational presentation for Box2D's algorithm. PDF freely available.
Strongly recommended
- Box2D source code (github.com/erincatto/box2d) — the canonical 2D physics engine. Production C++. Readable.
- Christer Ericson, Real-Time Collision Detection — the book on collision algorithms. Comprehensive.
- Glenn Fiedler, "Gaffer on Games" physics articles (gafferongames.com) — networking, prediction, and physics.
For specific topics
- GJK algorithm — Gilbert–Johnson–Keerthi distance algorithm. The "how do convex polygons collide" gold standard.
- EPA algorithm — Expanding Polytope Algorithm. Used after GJK confirms collision to find penetration depth.
5. Curated tutorial list (from BYO-X)
- C: Video Game Physics Tutorial — toptal.com 3-part series — excellent overview
- C++: Game physics series by Allen Chou — allenchou.net/category/game-physics/ — deep technical articles
- C++: How to Create a Custom Physics Engine — Randy Gaul ⭐ recommended primary
- C++: 3D Physics Engine Tutorial [video] — 3D extension
- JavaScript: How Physics Engines Work — buildnewgames.com
- JavaScript: Broad Phase Collision Detection Using Spatial Partitioning
- JavaScript: Build a simple 2D physics engine for JavaScript games
6. Recommended primary path
Randy Gaul, "How to Create a Custom Physics Engine" (6-part series).
Covers:
- Introduction and basic physics.
- AABB collisions.
- Circle vs circle / circle vs AABB.
- Convex polygon collisions (separating-axis theorem).
- Impulse resolution.
- Optimizations and broad phase.
After Gaul, the natural next read is Box2D's source, particularly b2Island.cpp (the solver), and Erin Catto's GDC slides for the algorithmic motivation.
For 3D: read Ian Millington's Game Physics Engine Development (textbook), or extend the 2D engine to 3D.
7. Implementation milestones
Milestone 1: Integration (Verlet or semi-implicit Euler)
Each body has position, velocity, mass. Forces accumulate, then update velocity, then position.
Symplectic / semi-implicit Euler (better than naive Euler for stability):
for body in bodies {
let acceleration = body.force * body.inv_mass;
body.velocity += acceleration * dt;
body.position += body.velocity * dt;
body.force = Vec2::ZERO;
}
Apply gravity: body.force += Vec2::new(0.0, body.mass * 9.81).
Evidence: A ball under gravity falls realistically. Frame-rate independent.
Milestone 2: Circle vs circle collision
fn circle_vs_circle(a: &Circle, b: &Circle) -> Option<Collision> {
let normal = b.center - a.center;
let dist_sq = normal.length_squared();
let r = a.radius + b.radius;
if dist_sq >= r * r { return None; }
let dist = dist_sq.sqrt();
Some(Collision {
normal: if dist > 0.0 { normal / dist } else { Vec2::X },
penetration: r - dist,
})
}
Evidence: Two balls collide and visibly intersect (not yet resolved).
Milestone 3: Collision resolution (impulse-based)
Two colliding bodies need to:
- Separate (positional correction) — fix interpenetration.
- Bounce or stop — apply an impulse along the collision normal.
let rv = b.velocity - a.velocity; // relative velocity
let vel_along_normal = rv.dot(collision.normal);
if vel_along_normal > 0.0 { return; } // already separating
let e = (a.restitution + b.restitution) / 2.0; // average bounciness
let j = -(1.0 + e) * vel_along_normal / (a.inv_mass + b.inv_mass);
let impulse = collision.normal * j;
a.velocity -= impulse * a.inv_mass;
b.velocity += impulse * b.inv_mass;
Evidence: Two balls bounce off each other realistically. Different restitutions produce different bounces.
Milestone 4: AABB and AABB vs circle collisions
AABB-AABB: check that the overlap on both axes is positive. The smaller overlap is the penetration; that axis is the normal.
AABB-Circle: find the closest point on the AABB to the circle center; if its distance from center is less than radius, collision.
Evidence: A circle rolls along a flat AABB ground.
Milestone 5: Friction
Apply tangential impulse opposing relative tangential velocity. Magnitude limited by Coulomb friction (μ * normal_impulse).
Evidence: A circle on a flat ground stops sliding. Different friction coefficients produce different sliding distances.
Milestone 6: Convex polygons (Separating Axis Theorem)
Two convex polygons collide iff no separating axis exists between them. The candidate axes are the normals of all polygon edges.
fn polygon_vs_polygon(a: &Polygon, b: &Polygon) -> Option<Collision> {
let mut best_overlap = f32::MAX;
let mut best_axis = Vec2::ZERO;
for axis in a.normals().chain(b.normals()) {
let (a_min, a_max) = a.project_onto(axis);
let (b_min, b_max) = b.project_onto(axis);
let overlap = (a_max.min(b_max)) - (a_min.max(b_min));
if overlap < 0.0 { return None; } // separating axis
if overlap < best_overlap {
best_overlap = overlap;
best_axis = axis;
}
}
Some(Collision { normal: best_axis, penetration: best_overlap })
}
Evidence: Two arbitrary polygons collide and resolve correctly.
Milestone 7: Rotation and angular velocity
Each body now has orientation (radians) and angular velocity (radians/sec). Forces applied off-center produce torque, which produces angular acceleration.
The collision impulse calculation gets more complex — it includes the velocity at the contact point, not just center velocity.
This is when "rigid body" physics becomes real.
Evidence: A box hit off-center spins.
Milestone 8: Sequential impulses (Catto's algorithm)
The naive impulse calculation doesn't handle multiple contacts simultaneously. With many contacts (e.g., a stacked box on top of another), iterating impulse resolutions multiple times per frame produces stable stacking.
for iteration in 0..N_ITERATIONS {
for contact in contacts {
resolve_contact(contact);
}
}
N_ITERATIONS ~ 8 is typical. Box2D uses this.
Evidence: A stack of 10 boxes stands stably. Without sequential impulses, the stack jitters and collapses.
Milestone 9 (broad phase): Spatial partitioning
With N bodies, naive checking is O(N²). Spatial hash, sweep-and-prune, or quadtree reduces to nearly O(N).
let mut grid: HashMap<(i32, i32), Vec<usize>> = HashMap::new();
for (i, body) in bodies.iter().enumerate() {
let cell = (body.x as i32 / CELL_SIZE, body.y as i32 / CELL_SIZE);
grid.entry(cell).or_default().push(i);
}
// Only check pairs within the same cell (or adjacent cells).
Evidence: 1,000 bodies in motion at 60 FPS.
Milestone 10 (optional): Constraints
Pin joint, distance joint, hinge. Modeled as constraints solved by impulses each frame.
8. Tests & evidence
| Test | How |
|---|---|
| Free fall | Ball falls under gravity at expected acceleration |
| Restitution | Perfectly elastic bounce returns to original height |
| Static stacking | 10 boxes stable indefinitely |
| Dynamic load | 1,000 bodies at 60 FPS |
| Determinism | Same seed → identical simulation |
| Real game | Build a small game (Angry Birds clone, vehicle physics demo) |
The strongest evidence: a video of a stable stack of boxes that doesn't jitter, plus a 1,000-body benchmark.
9. Common pitfalls
- Naive Euler. Unstable. Use semi-implicit or RK4.
- No positional correction. Resolution only acts on velocity; bodies stay overlapping. Add a small position correction each step.
- Tunneling. A fast object passes through a thin wall in one step. Solutions: smaller timestep, continuous collision detection (CCD).
- Mass = 0 confusion. Static bodies have infinite mass, which in code means inverse mass = 0.
- Floating-point determinism across platforms. SSE optimizations can produce different results. If determinism matters, use fixed-point math or compile with strict floating-point.
- No sleep / wake. Bodies that should be resting use CPU forever. Mark them sleeping when velocity is near zero; wake on impact.
- Iteration count too low. With many contacts, stacks jitter.
- Trying to do everything at once. Start with circles only. Add polygons last.
10. Extensions
- 3D. All the algorithms generalize, but with more linear algebra (3x3 matrices, quaternions).
- Soft bodies. Spring-mass systems. Cloth, rope, jello.
- Fluid simulation. SPH (smoothed particle hydrodynamics).
- Vehicles. A car is a rigid body + wheels + constraints.
- Ragdolls. Multiple bodies + constraints.
- GPU acceleration. Compute shaders for thousands of particles.
A physics engine is one of those projects with infinite refinement potential.
11. Module integration
| Module | What the physics engine deepens |
|---|---|
| Sem 1 Module 4 — Linear algebra | Direct application. Vectors, matrices, dot/cross products. |
| Sem 2 Module 1 — Algorithm analysis | Broad-phase data structures. |
| Sem 8 Module 4 — Performance | Frame budget. Cache-friendly layouts. |
| Game Engine tutorial | Natural pairing. |
| 3D Renderer tutorial | 3D physics requires 3D math; same machinery. |
12. Portfolio framing
What to publish:
- Source organized as
physics/{integrator, collide, resolve, broad}/. - A set of demos: bouncing balls, falling stack, vehicle, breakable structure.
- A README with:
- Video of demos.
- Performance numbers.
- Algorithm explanations (with references to Catto/Gaul).
- Known limitations (e.g., no continuous collision detection).
Reviewer entry points:
physics/integrator.rs— the time-stepping.physics/solver.rs— the sequential impulses solver.demos/stack.rs— the stacking demo.- README must include: stable stacking video, benchmark numbers, algorithm references.
A working physics engine is a striking portfolio piece because the results are immediately legible. Stable stacking is the single most compelling demo — it shows the engine handles the hard case.
Source
This tutorial draws from the BYO-X catalog "Physics Engine" section. Randy Gaul's series and Erin Catto's Box2D papers are the canonical primary sources.