Physics in 2D Games: Collisions, Friction, and Jumps Explained

Team 5 min read

#game-physics

#2d-games

#collision-detection

Introduction

2D games often rely on simplified, but believable, physics to feel responsive without heavy computation. In this post we’ll break down three core ideas: collisions, friction, and jumps. You’ll learn approachable math, the intuition behind common techniques, and practical tips you can apply in engines or vanilla canvas-based games.

Collisions in 2D

Collisions are about detecting when two bodies touch and then adjusting their positions and velocities to reflect that contact.

  • Collision detection basics

    • Use a broadphase to quickly rule out distant pairs (e.g., spatial hashing, uniform grid, or simple AABB checks).
    • Use a narrow phase for precise contact normals. Simple shapes work well in practice: circles for round objects, AABBs for boxes, or a circle-approximation of a box.
  • Impulse-based collision resolution (the basics)

    • When two bodies A and B collide, compute:
      • Normal n: the unit vector from A to B at contact
      • Relative velocity vr = vB − vA
      • Velocity along the normal: velAlongNormal = dot(vr, n)
      • Restitution e: how bouncy the collision is (0 = perfectly inelastic, 1 = perfectly elastic)
    • If velAlongNormal > 0, they are separating; no impulse needed.
    • Normal impulse magnitude:
      • j = -(1 + e) * velAlongNormal / (1/mA + 1/mB)
    • Apply the impulse to velocities:
      • vA’ = vA − (j / mA) * n
      • vB’ = vB + (j / mB) * n
  • Penetration resolution (keeping objects from sticking together)

    • If bodies overlap by penetration p along n, separate them by moving each body along n by a proportion of the penetration, typically weighted by inverse masses:
      • A moves by p * (mB / (mA + mB)) along −n
      • B moves by p * (mA / (mA + mB)) along +n
  • A quick example (pseudo-code)

    • The idea is to calculate a normal impulse and then apply it to both bodies, followed by a small positional correction to resolve overlap.

Code (pseudo):

function resolveCollision(A, B, restitution):
  n = normalize(B.pos - A.pos)
  rel = B.vel - A.vel
  velAlongNormal = dot(rel, n)
  if velAlongNormal > 0: return

  invMassA = 1 / A.mass
  invMassB = 1 / B.mass
  j = -(1 + restitution) * velAlongNormal / (invMassA + invMassB)
  impulse = j * n

  A.vel -= impulse * invMassA
  B.vel += impulse * invMassB

Friction and Contact

Friction makes sliding feel more natural and helps characters stand on surfaces without slipping endlessly.

  • Decomposing velocity at contact

    • Normal direction n is as above; tangential direction t is perpendicular to n (for 2D, t can be [-n.y, n.x]).
  • Friction impulse (Coulomb model)

    • Relative velocity along tangent: vT = dot(vr, t)
    • Tangential impulse before friction: jt = -vT / (1/mA + 1/mB)
    • Friction used depends on whether contact sticks or slips:
      • Static friction: if |jt| <= mu_s * j (where mu_s is static friction and j is the normal impulse), clamp jt to that value and set tangential velocity to zero.
      • Kinetic friction: if |jt| > mu_k * j (mu_k is kinetic friction), clamp jt to ±mu_k * j.
  • Applying friction

    • impulseT = jt * t
    • A.vel -= impulseT * invMassA
    • B.vel += impulseT * invMassB
  • Quick note

    • In practice you’ll often combine this with the normal impulse in the same update, ensuring the tangential impulse acts only at contacts that exist.

Jumps and Vertical Dynamics

Jumping is a staple of platformers and action games. The math is simple, but the feel comes from how you apply it in time.

  • Gravity and integration

    • Each frame: v.y += gravity * dt
    • Then: pos.y += v.y * dt
    • When grounded, vertical velocity is typically clamped or reset to zero to avoid sticking to the ground.
  • Jump impulse

    • When the jump button is pressed while on the ground (or within a small grace period, sometimes called “coyote time”), give the character an upward velocity:
      • v.y = jumpVelocity
    • The jumpVelocity is chosen from the desired jump height h:
      • jumpVelocity = sqrt(2 * gravity * h)
  • Variable jump height and hold-to-jump

    • If the player holds the jump button, you can reduce gravity temporarily to allow a higher jump. If they release early, gravity returns to normal sooner:
      • if jumpButtonHeld and v.y > 0: gravityScale = 0.5
      • else: gravityScale = 1.0
    • Apply gravity as: v.y += gravity * gravityScale * dt
  • Grounding and extra jumps

    • Track onGround and a short “grace period” after leaving the ground to permit a jump (coyote time).
    • Optional: allow double-jumps or wall-jumps with additional checks on contact normals and surfaces.

Implementation tips

  • Use a fixed time step for stability
    • A common pattern is to run physics with a fixed dt (e.g., 1/60s) and accumulate any leftover time to substeps.
  • Broadphase and narrow-phase
    • Start with simple broadphase (grid, spatial hashing) and pair candidates for narrow-phase collision checks.
  • Continuous collision detection
    • To avoid tunneling (fast objects passing through thin walls), consider raycasting or Swept AABB checks for fast-moving bodies.
  • Handling slopes and friction
    • When objects are on inclined surfaces, align the contact normal with the slope. Friction should resist motion along the slope, not perpendicular to it.
  • Sleep and wake
    • If objects become stationary for a while, you can “sleep” them to save on updates, waking when a force or collision occurs.
  • Shape choices
    • Circles are cheap for collisions; AABBs are simple for many boxes. If you need more accuracy, consider capsule shapes or rotated boxes with SAT, but be mindful of complexity.
  • Debugging
    • Visualize contact normals, impulses, and penetration depths to diagnose non-intuitive behavior.

Conclusion

Mastering collisions, friction, and jumps in 2D games comes down to combining simple physics equations with stable integration and sensible heuristics. Start with impulse-based collision response, layer in friction, and add jumping mechanics with gravity and timing. As your game evolves, you can adjust coefficients and implement features like coyote time, variable jump height, and slope handling to strike the right balance between realism and responsive gameplay.