Skip to main content

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:


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.
  • Box2D source code (github.com/erincatto/box2d) — the canonical 2D physics engine. Production C++. Readable.
  • Christer Ericson, Real-Time Collision Detectionthe 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 Tutorialtoptal.com 3-part series — excellent overview
  • C++: Game physics series by Allen Chouallenchou.net/category/game-physics/ — deep technical articles
  • C++: How to Create a Custom Physics EngineRandy Gaulrecommended primary
  • C++: 3D Physics Engine Tutorial [video] — 3D extension
  • JavaScript: How Physics Engines Workbuildnewgames.com
  • JavaScript: Broad Phase Collision Detection Using Spatial Partitioning
  • JavaScript: Build a simple 2D physics engine for JavaScript games

Randy Gaul, "How to Create a Custom Physics Engine" (6-part series).

Covers:

  1. Introduction and basic physics.
  2. AABB collisions.
  3. Circle vs circle / circle vs AABB.
  4. Convex polygon collisions (separating-axis theorem).
  5. Impulse resolution.
  6. 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:

  1. Separate (positional correction) — fix interpenetration.
  2. 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

TestHow
Free fallBall falls under gravity at expected acceleration
RestitutionPerfectly elastic bounce returns to original height
Static stacking10 boxes stable indefinitely
Dynamic load1,000 bodies at 60 FPS
DeterminismSame seed → identical simulation
Real gameBuild 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

ModuleWhat the physics engine deepens
Sem 1 Module 4 — Linear algebraDirect application. Vectors, matrices, dot/cross products.
Sem 2 Module 1 — Algorithm analysisBroad-phase data structures.
Sem 8 Module 4 — PerformanceFrame budget. Cache-friendly layouts.
Game Engine tutorialNatural pairing.
3D Renderer tutorial3D 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.