| name | physics-simulation-patterns |
| description | Rigid body dynamics, soft bodies, cloth, fluids, deterministic physics |
Physics Simulation Patterns
Description
Master rigid body dynamics, soft body simulation, cloth, fluids, and vehicle physics for real-time game engines. Apply fixed timestep integration, continuous collision detection, and deterministic simulation patterns to avoid physics explosions, tunneling, and multiplayer desyncs.
When to Use This Skill
Use this skill when implementing or debugging:
- Vehicle physics (cars, boats, aircraft)
- Character physics (ragdolls, dynamic movement)
- Destructible environments (debris, particles)
- Cloth and soft body simulation
- Fluid dynamics for games
- Multiplayer physics synchronization
- Any real-time physics simulation requiring stability and determinism
Do NOT use this skill for:
- Basic kinematic movement (simple position/velocity updates)
- Pure animation systems without physics
- UI animations or tweens
- Turn-based games without real-time physics
Quick Start (Time-Constrained Implementation)
If you need working physics quickly (< 4 hours), follow this priority order:
CRITICAL (Never Skip):
- Fixed timestep: Use engine's fixed update (Unity:
FixedUpdate(), Unreal: auto-handled) - Use engine built-ins: Unity
WheelCollider, UnrealWheeledVehicleMovementComponent - Enable CCD: For objects faster than 20 m/s (prevents tunneling through walls)
- Semi-implicit integration: Engine default (don't change it)
IMPORTANT (Strongly Recommended): 5. Lower center of mass for vehicles (improves stability) 6. Test at different frame rates (30 FPS and 144 FPS should behave identically) 7. Add velocity clamping for safety (prevent physics explosions)
CAN DEFER (Optimize Later):
- Custom tire friction models (use engine defaults first)
- Advanced aerodynamics and downforce
- Detailed damage and deformation systems
- Performance optimizations (if meeting target frame rate)
Example - Unity Vehicle in 30 Minutes:
// 1. Add WheelColliders to wheel positions
// 2. Configure in FixedUpdate():
void FixedUpdate() { // ← Fixed timestep automatically
wheelFL.motorTorque = Input.GetAxis("Vertical") * 1500f;
wheelFR.motorTorque = Input.GetAxis("Vertical") * 1500f;
wheelFL.steerAngle = Input.GetAxis("Horizontal") * 30f;
wheelFR.steerAngle = Input.GetAxis("Horizontal") * 30f;
}
// 3. Enable CCD on Rigidbody:
GetComponent<Rigidbody>().collisionDetectionMode = CollisionDetectionMode.Continuous;
// 4. Lower center of mass:
GetComponent<Rigidbody>().centerOfMass = new Vector3(0, -0.5f, 0);
This gives you functional vehicle physics. Refine later based on feel and performance.
Core Concepts
1. Physics Integration Methods
Physics integration updates object positions based on forces. The method choice determines stability, accuracy, and performance.
Euler Integration (Explicit/Forward Euler):
# Simple but UNSTABLE for most game physics
velocity += acceleration * dt
position += velocity * dt
# Problem: Energy accumulation
# At high speeds or large dt, objects gain energy and "explode"
Semi-Implicit Euler (Symplectic Euler):
# Better stability - use for most game physics
velocity += acceleration * dt
position += velocity * dt # Uses UPDATED velocity
# Advantage: Conserves energy better, stable for games
# This is what Unity/Unreal use internally
Verlet Integration:
# Position-based, good for constraints
new_position = 2 * position - previous_position + acceleration * dt * dt
previous_position = position
position = new_position
# Advantage: Stable, easy constraints, no explicit velocity
# Used in: Cloth simulation, rope physics, soft bodies
Runge-Kutta (RK4):
# High accuracy but expensive
# 4 function evaluations per timestep
k1 = f(t, y)
k2 = f(t + dt/2, y + dt/2 * k1)
k3 = f(t + dt/2, y + dt/2 * k2)
k4 = f(t + dt, y + dt * k3)
y_next = y + dt/6 * (k1 + 2*k2 + 2*k3 + k4)
# Use ONLY when: Accuracy critical, performance not (orbital mechanics, space sims)
# Most games: Semi-implicit Euler is better trade-off
Decision Framework:
- Rigid bodies (vehicles, debris): Semi-Implicit Euler + Fixed Timestep
- Cloth/rope/chains: Verlet Integration + Position-based constraints
- Orbital mechanics: RK4 or higher-order methods
- Never use: Explicit Euler (unstable)
2. Fixed Timestep vs Variable Timestep
The Problem: Variable timestep (using raw frame delta time) causes:
- Frame-rate dependent physics
- Instability at low frame rates
- Non-determinism (different results on different machines)
- Multiplayer desyncs
The Solution: Fixed Timestep with Accumulator:
# ALWAYS use this pattern for game physics
FIXED_TIMESTEP = 1.0 / 60.0 # 60 Hz physics (16.67ms)
accumulator = 0.0
def game_loop():
global accumulator
frame_time = get_delta_time()
# Clamp to prevent spiral of death
if frame_time > 0.25:
frame_time = 0.25 # Max 250ms (4 FPS minimum)
accumulator += frame_time
# Run physics in fixed steps
while accumulator >= FIXED_TIMESTEP:
physics_update(FIXED_TIMESTEP) # Always same dt
accumulator -= FIXED_TIMESTEP
# Interpolate rendering between physics states
alpha = accumulator / FIXED_TIMESTEP
render_interpolated(alpha)
def render_interpolated(alpha):
# Smooth visuals between physics steps
interpolated_pos = previous_pos + (current_pos - previous_pos) * alpha
draw_at_position(interpolated_pos)
Why This Works:
- Physics always runs at 60 Hz (consistent)
- Fast machines: Multiple render frames per physics step
- Slow machines: Multiple physics steps per render frame
- Deterministic: Same inputs produce same outputs
- Interpolation: Smooth visuals even at low frame rates
Common Mistake - Variable Timestep Scaling:
# ❌ WRONG - Still frame-rate dependent
dt = get_delta_time()
velocity += acceleration * dt * 60 # "Scale to 60 FPS"
# Problem: Integration errors still vary with dt
# Physics behaves differently at different frame rates
Unity Example:
// Unity provides this via FixedUpdate()
void FixedUpdate() {
// Always runs at Time.fixedDeltaTime (default 0.02 = 50 Hz)
// Use for all physics operations
rb.AddForce(force);
}
void Update() {
// Variable timestep - use for input, rendering
// NEVER use for physics calculations
}
3. Continuous Collision Detection (CCD)
The Problem: Tunneling:
Frame 1: [Bullet] |Wall|
Frame 2: |Wall| [Bullet]
^ Bullet passed through wall between frames!
At high velocities, discrete collision checks miss collisions. For a 200 mph car (89 m/s) at 60 FPS, the car moves 1.48 meters per frame - can easily phase through walls.
Solution 1: Conservative Advancement:
def conservative_advancement(start_pos, end_pos, obstacles):
"""Move in small steps until collision"""
current_pos = start_pos
direction = (end_pos - start_pos).normalized()
remaining_distance = (end_pos - start_pos).length()
while remaining_distance > 0:
# Find distance to nearest obstacle
safe_distance = min_distance_to_obstacles(current_pos, obstacles)
# Move by safe distance (slightly less for safety margin)
step = min(safe_distance * 0.9, remaining_distance)
current_pos += direction * step
remaining_distance -= step
if safe_distance < EPSILON:
return current_pos, True # Collision detected
return current_pos, False
Solution 2: Swept Collision Detection:
def swept_sphere_vs_plane(sphere_center, sphere_radius, velocity, plane):
"""Check collision over movement path"""
# Ray from sphere center along velocity
ray_start = sphere_center - plane.normal * sphere_radius
ray_direction = velocity.normalized()
ray_length = velocity.length()
# Intersect ray with plane
t = ray_plane_intersection(ray_start, ray_direction, plane)
if 0 <= t <= ray_length:
# Collision occurs during this frame
collision_time = t / ray_length # 0 to 1
collision_point = ray_start + ray_direction * t
return True, collision_time, collision_point
return False, None, None
Solution 3: Speculative Contacts (Modern Engines):
def speculative_contacts(body, dt):
"""Predict and prevent tunneling before it happens"""
# Expand collision shape based on velocity
velocity_magnitude = body.velocity.length()
expansion = velocity_magnitude * dt
# Use expanded AABB for collision detection
expanded_bounds = body.bounds.expand(expansion)
# Check collisions with expanded bounds
contacts = check_collisions(expanded_bounds)
# Apply contact constraints to prevent penetration
for contact in contacts:
apply_contact_constraint(body, contact, dt)
When to Use CCD:
- Always: Bullets, projectiles, fast vehicles (>50 m/s)
- Usually: Player characters (falling at terminal velocity)
- Sometimes: Debris, particles (if important)
- Never: Static objects, slow-moving props
Unity Example:
Rigidbody rb = GetComponent<Rigidbody>();
// For very fast objects (bullets)
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
// For fast objects that hit static geometry (vehicles)
rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
// Default (fast but can tunnel)
rb.collisionDetectionMode = CollisionDetectionMode.Discrete;
4. Deterministic Physics
Why Determinism Matters:
- Multiplayer: Clients must compute same physics results
- Replays: Must reproduce exact gameplay
- Rollback netcode: Must rewind and re-simulate
- Testing: Must be reproducible for bug fixing
Sources of Non-Determinism:
- Floating-Point Non-Determinism:
# ❌ Can produce different results on different CPUs
a = 0.1 + 0.2 # Might be 0.30000000000000004 or slightly different
# ✅ Use fixed-point math for critical calculations
FIXED_POINT_SCALE = 1000
a = (1 * FIXED_POINT_SCALE + 2 * FIXED_POINT_SCALE) // 10 # Exact
- Iteration Order Non-Determinism:
# ❌ Dictionary/set iteration order undefined
for obj in physics_objects: # If physics_objects is a dict/set
obj.update()
# ✅ Sort objects by unique ID for consistent order
sorted_objects = sorted(physics_objects, key=lambda obj: obj.id)
for obj in sorted_objects:
obj.update()
- Multi-Threading Non-Determinism:
# ❌ Parallel physics updates can happen in any order
parallel_for(objects, lambda obj: obj.update())
# ✅ Either: Single-threaded physics updates
for obj in objects:
obj.update()
# ✅ Or: Deterministic parallel scheduling (island-based)
islands = partition_into_islands(objects) # No dependencies between islands
for island in islands:
parallel_for(island, lambda obj: obj.update()) # Order within island fixed
- Random Number Non-Determinism:
# ❌ Different seed every run
debris_velocity = random.uniform(-10, 10)
# ✅ Seeded RNG, advance deterministically
rng = Random(seed=12345)
debris_velocity = rng.uniform(-10, 10)
Deterministic Physics Checklist:
- Fixed timestep (never variable)
- Sorted iteration order (by ID or consistent key)
- Single-threaded physics or deterministic parallel
- Seeded random number generators
- Avoid floating-point math in critical paths (or use fixed-point)
- Consistent compiler flags (same floating-point mode)
- No OS/hardware dependencies (timer resolution, etc.)
Unreal Engine Example:
// Enable deterministic physics in Unreal
// Project Settings > Physics > Simulation
bEnableEnhancedDeterminism = true;
// Use fixed timestep
FixedDeltaTime = 0.0333f; // 30 Hz for determinism
// Disable async physics
bTickPhysicsAsync = false;
Decision Frameworks
Framework 1: Full Physics vs Kinematic Control
Use Full Physics Simulation When:
- Realistic force-based interactions required (collisions, explosions)
- Unpredictable outcomes are desirable (debris, ragdolls)
- Complex constraint solving needed (vehicles, joints)
- Object interactions with environment are core gameplay
Use Kinematic Control When:
- Precise, predictable movement required (elevators, cutscenes)
- Performance is critical (hundreds of objects)
- Simple animations are sufficient
- No force-based interactions needed
Hybrid Approach (Common):
class Vehicle:
def __init__(self):
self.mode = "kinematic" # Start kinematic
self.rigidbody = None
def transition_to_physics(self):
"""Switch to physics when player takes control"""
self.mode = "physics"
self.rigidbody = create_rigidbody(self)
self.rigidbody.velocity = self.kinematic_velocity
def transition_to_kinematic(self):
"""Switch to kinematic for cutscenes"""
self.kinematic_velocity = self.rigidbody.velocity
destroy_rigidbody(self.rigidbody)
self.mode = "kinematic"
def update(self, dt):
if self.mode == "physics":
# Full physics simulation
self.apply_forces(dt)
else:
# Direct position control
self.position += self.kinematic_velocity * dt
Examples:
- Racing game car: Physics (forces, suspension, tire grip)
- Racing game camera: Kinematic (smooth following, no physics)
- Destructible building: Physics after destruction trigger
- Elevator: Kinematic (predictable timing)
- Ragdoll: Kinematic (alive) → Physics (dead)
Framework 2: Sub-Stepping Decision
Sub-stepping: Running multiple small physics steps per frame for stability.
def physics_update(dt):
SUB_STEPS = 4
sub_dt = dt / SUB_STEPS
for _ in range(SUB_STEPS):
# Smaller timesteps = more stable
integrate_forces(sub_dt)
solve_constraints(sub_dt)
integrate_velocities(sub_dt)
Use Sub-Stepping When:
- Complex constraints (vehicle suspension, rope physics)
- Stiff springs (high spring constants)
- High-speed collisions requiring accuracy
- Soft body or cloth simulation
Skip Sub-Stepping When:
- Simple rigid bodies with no constraints
- Performance is critical
- Objects are slow-moving
- Using very small base timestep already (>120 Hz)
Practical Guidelines:
- Most rigid bodies: 1 step (60 Hz fixed timestep sufficient)
- Vehicles with suspension: 2-4 steps
- Cloth simulation: 4-10 steps
- Rope/chain physics: 8-16 steps
Unity Example:
// Project Settings > Time > Fixed Timestep
Time.fixedDeltaTime = 0.0166f; // 60 Hz base
// Physics Settings > Solver Iteration Count
Physics.defaultSolverIterations = 6; // Velocity
Physics.defaultSolverVelocityIterations = 1; // Position
// For complex vehicle physics, increase iterations
Rigidbody rb = GetComponent<Rigidbody>();
rb.solverIterations = 12;
rb.solverVelocityIterations = 2;
Framework 3: Real Physics vs Faked Physics
Real Physics (Forces, constraints, integration):
# Particle with gravity
particle.force += Vector3(0, -9.8 * particle.mass, 0)
particle.velocity += (particle.force / particle.mass) * dt
particle.position += particle.velocity * dt
Faked Physics (Direct manipulation):
# Fake gravity for particles (much faster)
particle.position.y -= 9.8 * dt * dt / 2
particle.velocity.y -= 9.8 * dt
# Simple ballistic trajectory (no integration needed)
t = elapsed_time
particle.position = start_position + velocity * t + 0.5 * gravity * t * t
Use Real Physics When:
- Interactions with other physics objects (collisions, joints)
- Forces are complex or change dynamically
- Constraints must be solved (rope, springs)
- Accuracy is critical (gameplay-affecting)
Use Faked Physics When:
- Visual effects only (sparks, dust, blood splatter)
- No interactions with other objects
- Performance is critical (thousands of particles)
- Simple, predictable motion (arcs, bounces)
Performance Comparison:
- Real physics: ~100-1000 objects at 60 FPS
- Faked physics: ~10,000-100,000 particles at 60 FPS
Example - Explosion Debris:
# GOOD: Nearby debris (10-20 pieces) - real physics
for debris in nearby_debris:
debris.rigidbody.add_force(explosion_force)
debris.rigidbody.add_torque(random_spin)
# GOOD: Distant particles (1000s) - faked physics
for particle in distant_particles:
particle.velocity = (particle.position - explosion_center).normalized() * speed
particle.lifetime = 2.0
# Simple ballistic arc, no collision detection
Framework 4: When to Use Specific Integration Methods
| Scenario | Integration Method | Reason |
|---|---|---|
| Rigid body vehicles | Semi-Implicit Euler | Stable, fast, energy-conserving |
| Cloth/fabric | Verlet + PBD | Position-based constraints, stable |
| Rope/chains | Verlet + PBD | Easy to constrain, no velocity needed |
| Space/orbital sim | RK4 or higher | High accuracy for long-term stability |
| Soft bodies | Position-Based Dynamics | Unconditionally stable |
| Ragdolls | Semi-Implicit Euler | Fast, good enough for visual quality |
| Particle effects | Explicit Euler | Fast, accuracy doesn't matter |
| Fluids (SPH) | Leapfrog or Verlet | Symplectic, energy-conserving |
Framework 5: Multiplayer Physics Architecture
Choose your multiplayer architecture based on whether physics is deterministic:
Deterministic Physics + Low Latency Required → Rollback Netcode:
- Examples: Fighting games (Street Fighter, Guilty Gear), competitive platformers
- Re-simulates past frames when late inputs arrive
- Requires: Bit-perfect determinism, fast physics (must re-run multiple frames)
- Advantages: Feels instant (no input delay), handles variable latency well
- Disadvantages: Complex to implement, requires deterministic physics
Deterministic Physics + Turn-Based or Slow Pace → Lock-Step:
- Examples: RTS games (StarCraft), turn-based strategy
- All clients wait for all inputs before advancing
- Requires: Determinism, same simulation order on all clients
- Advantages: Simple, guaranteed sync, minimal bandwidth
- Disadvantages: Input lag = highest player latency
Non-Deterministic Physics → Server-Authoritative:
- Examples: Most MMOs, open-world games, battle royale
- Server runs authoritative physics, sends snapshots to clients
- Clients predict locally, reconcile with server updates
- Advantages: No determinism required, easier to implement, cheat-resistant
- Disadvantages: Bandwidth intensive, visible corrections/rubber-banding
Decision Table:
| Requirement | Architecture | Implementation Time |
|---|---|---|
| Physics already deterministic + competitive | Rollback | 2-4 weeks |
| Physics already deterministic + slow-paced | Lock-step | 1-2 weeks |
| Physics NOT deterministic | Server-authoritative | 1-3 weeks |
| Need it working in < 1 week | Server-authoritative | Fastest |
| Converting non-deterministic to deterministic | Refactor first | 1-2 weeks + architecture time |
Time-Constrained Multiplayer (< 1 week):
# Server-authoritative is fastest to implement
class Server:
def update(self):
# Server runs authoritative physics
self.physics_update(FIXED_TIMESTEP)
# Send snapshots to clients (10-30 Hz)
if time.time() - self.last_snapshot > SNAPSHOT_INTERVAL:
self.broadcast_snapshot(self.get_physics_state())
class Client:
def update(self):
# Client predicts locally
self.physics_update(FIXED_TIMESTEP)
# When server snapshot arrives, reconcile
if snapshot_received:
# Hard snap or interpolate to server state
self.reconcile_with_server(snapshot)
Converting to Deterministic Physics (for rollback/lock-step later):
- Implement fixed timestep (if not already)
- Sort all object iteration by ID
- Seed all RNG deterministically
- Use fixed-point math for critical calculations (or ensure same FP mode)
- Make physics single-threaded (or island-based deterministic parallel)
- Test on different machines for bit-perfect results
Critical: Don't attempt rollback netcode with non-deterministic physics. Either refactor for determinism OR use server-authoritative.
Implementation Patterns
Pattern 1: Fixed Timestep Game Loop
Complete implementation with interpolation:
class GameEngine:
def __init__(self):
self.PHYSICS_TIMESTEP = 1.0 / 60.0 # 16.67ms
self.MAX_FRAME_TIME = 0.25 # Don't spiral if below 4 FPS
self.accumulator = 0.0
self.current_state = PhysicsState()
self.previous_state = PhysicsState()
def run(self):
last_time = time.time()
while self.running:
current_time = time.time()
frame_time = current_time - last_time
last_time = current_time
# Cap frame time to prevent spiral of death
if frame_time > self.MAX_FRAME_TIME:
frame_time = self.MAX_FRAME_TIME
self.accumulator += frame_time
# Physics updates (fixed timestep)
while self.accumulator >= self.PHYSICS_TIMESTEP:
# Store previous state for interpolation
self.previous_state.copy_from(self.current_state)
# Physics update
self.physics_update(self.PHYSICS_TIMESTEP)
self.accumulator -= self.PHYSICS_TIMESTEP
# Interpolation factor
alpha = self.accumulator / self.PHYSICS_TIMESTEP
# Render with interpolation
self.render(alpha)
def physics_update(self, dt):
"""Fixed timestep physics"""
# Apply forces
for obj in self.physics_objects:
obj.apply_forces(dt)
# Integrate
for obj in self.physics_objects:
obj.velocity += (obj.force / obj.mass) * dt
obj.position += obj.velocity * dt
obj.force = Vector3.ZERO
# Collision detection and response
self.resolve_collisions()
def render(self, alpha):
"""Interpolate between physics states for smooth rendering"""
for obj in self.physics_objects:
# Interpolate position
interpolated_pos = lerp(
obj.previous_position,
obj.current_position,
alpha
)
# Interpolate rotation (use slerp for quaternions)
interpolated_rot = slerp(
obj.previous_rotation,
obj.current_rotation,
alpha
)
obj.render_at(interpolated_pos, interpolated_rot)
Key Points:
- Physics always runs at fixed interval (deterministic)
- Rendering interpolates between states (smooth)
- Handles both fast and slow frame rates gracefully
- Prevents spiral of death with max frame time cap
Pattern 2: Vehicle Physics with Suspension
Realistic vehicle simulation using ray-cast suspension:
class VehicleController:
def __init__(self):
self.mass = 1500 # kg
self.wheel_positions = [
Vector3(-1, 0, 1.5), # Front-left
Vector3(1, 0, 1.5), # Front-right
Vector3(-1, 0, -1.5), # Rear-left
Vector3(1, 0, -1.5), # Rear-right
]
# Suspension parameters
self.suspension_length = 0.4 # meters
self.suspension_stiffness = 50000 # N/m
self.suspension_damping = 4500 # N·s/m
# Tire parameters
self.tire_grip = 2.0
self.tire_friction_curve = TireFrictionCurve()
def physics_update(self, dt):
"""Vehicle physics with proper suspension"""
total_suspension_force = Vector3.ZERO
# 1. Ray-cast suspension for each wheel
for i, wheel_pos in enumerate(self.wheel_positions):
world_wheel_pos = self.transform.transform_point(wheel_pos)
ray_direction = -self.transform.up
hit, hit_distance, hit_normal = raycast(
world_wheel_pos,
ray_direction,
self.suspension_length
)
if hit:
# Calculate suspension compression
compression = self.suspension_length - hit_distance
# Spring force: F = -kx
spring_force = self.suspension_stiffness * compression
# Damper force: F = -cv (relative to ground)
wheel_velocity = self.get_point_velocity(wheel_pos)
suspension_velocity = Vector3.dot(wheel_velocity, hit_normal)
damper_force = self.suspension_damping * suspension_velocity
# Total suspension force
total_force = spring_force - damper_force
suspension_force = hit_normal * total_force
# Apply force at wheel position
self.add_force_at_position(suspension_force, world_wheel_pos)
# 2. Tire forces (grip and friction)
self.apply_tire_forces(i, world_wheel_pos, hit_normal, dt)
# 3. Aerodynamic drag
drag_force = -self.velocity * self.velocity.length() * 0.4
self.add_force(drag_force)
# 4. Engine force
if self.throttle > 0:
engine_force = self.transform.forward * self.engine_power * self.throttle
self.add_force(engine_force)
def apply_tire_forces(self, wheel_index, wheel_position, ground_normal, dt):
"""Calculate longitudinal and lateral tire forces"""
# Get wheel velocity
wheel_velocity = self.get_point_velocity(wheel_position)
# Project velocity onto ground plane
ground_velocity = wheel_velocity - ground_normal * Vector3.dot(wheel_velocity, ground_normal)
# Forward and lateral directions
forward = self.transform.forward
lateral = self.transform.right
# Slip calculations
forward_speed = Vector3.dot(ground_velocity, forward)
lateral_speed = Vector3.dot(ground_velocity, lateral)
# Tire friction curve (slip ratio → force)
forward_force = self.tire_friction_curve.evaluate(forward_speed) * self.tire_grip
lateral_force = self.tire_friction_curve.evaluate(lateral_speed) * self.tire_grip
# Apply tire forces
tire_force = forward * forward_force + lateral * lateral_force
self.add_force_at_position(tire_force, wheel_position)
def get_point_velocity(self, local_point):
"""Get velocity of a point on the vehicle (includes rotation)"""
world_point = self.transform.transform_point(local_point)
offset = world_point - self.center_of_mass
return self.velocity + Vector3.cross(self.angular_velocity, offset)
class TireFrictionCurve:
"""Pacejka tire model (simplified)"""
def evaluate(self, slip):
# Peak grip at ~15% slip
# https://en.wikipedia.org/wiki/Hans_B._Pacejka
B = 10 # Stiffness factor
C = 1.9 # Shape factor
D = 1.0 # Peak value
E = 0.97 # Curvature factor
return D * math.sin(C * math.atan(B * slip - E * (B * slip - math.atan(B * slip))))
Why This Works:
- Ray-cast suspension: Handles varying terrain
- Spring-damper model: Realistic suspension behavior
- Tire friction curve: Realistic grip/slip behavior
- Force application at wheel positions: Proper torque for steering
Pattern 3: Continuous Collision Detection for Projectiles
Swept sphere collision for bullets and fast projectiles:
class Projectile:
def __init__(self, position, velocity, radius):
self.position = position
self.velocity = velocity
self.radius = radius
self.active = True
def update_with_ccd(self, dt, world):
"""Update with continuous collision detection"""
if not self.active:
return
start_position = self.position
end_position = self.position + self.velocity * dt
# Swept collision detection
collision, hit_time, hit_point, hit_normal = self.swept_sphere_cast(
start_position,
end_position,
self.radius,
world
)
if collision:
# Move to collision point
self.position = start_position + self.velocity * (dt * hit_time)
# Handle collision (bounce, damage, destroy, etc.)
self.on_collision(hit_point, hit_normal)
else:
# No collision, move full distance
self.position = end_position
def swept_sphere_cast(self, start, end, radius, world):
"""Sweep sphere along path, detect first collision"""
direction = (end - start).normalized()
distance = (end - start).length()
# Ray-cast with radius offset
# Check multiple rays (center + offset in perpendicular directions)
earliest_hit = None
earliest_time = float('inf')
# Center ray
hit, t, point, normal = world.raycast(start, direction, distance)
if hit and t < earliest_time:
earliest_time = t
earliest_hit = (point, normal)
# Offset rays (perpendicular to velocity)
perp1, perp2 = get_perpendicular_vectors(direction)
for angle in [0, 90, 180, 270]:
rad = math.radians(angle)
offset = (perp1 * math.cos(rad) + perp2 * math.sin(rad)) * radius
hit, t, point, normal = world.raycast(
start + offset,
direction,
distance
)
if hit and t < earliest_time:
earliest_time = t
earliest_hit = (point, normal)
if earliest_hit:
point, normal = earliest_hit
return True, earliest_time / distance, point, normal
return False, None, None, None
def on_collision(self, hit_point, hit_normal):
"""Handle collision response"""
# Example: Destroy projectile and spawn impact effect
self.active = False
spawn_impact_effect(hit_point, hit_normal)
# Or bounce:
# self.velocity = reflect(self.velocity, hit_normal) * 0.8
Key Features:
- Swept collision: Never tunnels through thin objects
- Multiple ray-casts: Handles sphere shape accurately
- Early-out: Stops at first collision
- Configurable response: Destroy, bounce, penetrate, etc.
Pattern 4: Deterministic Physics for Multiplayer
Lock-step multiplayer with deterministic physics:
class DeterministicPhysicsEngine:
def __init__(self, seed):
# Fixed-point math for determinism
self.FIXED_POINT_SCALE = 1000
# Seeded RNG
self.rng = Random(seed)
# Sorted objects for consistent iteration
self.physics_objects = [] # Sorted by ID
# Fixed timestep
self.TIMESTEP = 1.0 / 60.0
def add_object(self, obj):
"""Add object and maintain sorted order"""
self.physics_objects.append(obj)
self.physics_objects.sort(key=lambda o: o.id)
def physics_step(self, dt):
"""Deterministic physics update"""
assert dt == self.TIMESTEP, "Must use fixed timestep!"
# Phase 1: Apply forces (sorted order)
for obj in self.physics_objects: # Consistent order
obj.apply_forces(dt)
# Phase 2: Integrate (sorted order)
for obj in self.physics_objects:
self.integrate_object(obj, dt)
# Phase 3: Collision detection (sorted pairs)
self.detect_and_resolve_collisions()
def integrate_object(self, obj, dt):
"""Fixed-point integration for determinism"""
# Convert to fixed-point
vel_x = int(obj.velocity.x * self.FIXED_POINT_SCALE)
vel_y = int(obj.velocity.y * self.FIXED_POINT_SCALE)
vel_z = int(obj.velocity.z * self.FIXED_POINT_SCALE)
acc_x = int(obj.acceleration.x * self.FIXED_POINT_SCALE)
acc_y = int(obj.acceleration.y * self.FIXED_POINT_SCALE)
acc_z = int(obj.acceleration.z * self.FIXED_POINT_SCALE)
dt_fixed = int(dt * self.FIXED_POINT_SCALE)
# Semi-implicit Euler (fixed-point)
vel_x += (acc_x * dt_fixed) // self.FIXED_POINT_SCALE
vel_y += (acc_y * dt_fixed) // self.FIXED_POINT_SCALE
vel_z += (acc_z * dt_fixed) // self.FIXED_POINT_SCALE
pos_x = int(obj.position.x * self.FIXED_POINT_SCALE)
pos_y = int(obj.position.y * self.FIXED_POINT_SCALE)
pos_z = int(obj.position.z * self.FIXED_POINT_SCALE)
pos_x += (vel_x * dt_fixed) // self.FIXED_POINT_SCALE
pos_y += (vel_y * dt_fixed) // self.FIXED_POINT_SCALE
pos_z += (vel_z * dt_fixed) // self.FIXED_POINT_SCALE
# Convert back to float
obj.velocity.x = vel_x / self.FIXED_POINT_SCALE
obj.velocity.y = vel_y / self.FIXED_POINT_SCALE
obj.velocity.z = vel_z / self.FIXED_POINT_SCALE
obj.position.x = pos_x / self.FIXED_POINT_SCALE
obj.position.y = pos_y / self.FIXED_POINT_SCALE
obj.position.z = pos_z / self.FIXED_POINT_SCALE
def detect_and_resolve_collisions(self):
"""Deterministic collision detection"""
# Sort pairs for consistent order
pairs = []
for i in range(len(self.physics_objects)):
for j in range(i + 1, len(self.physics_objects)):
obj_a = self.physics_objects[i]
obj_b = self.physics_objects[j]
# Always put lower ID first
if obj_a.id < obj_b.id:
pairs.append((obj_a, obj_b))
else:
pairs.append((obj_b, obj_a))
# Process collisions in sorted order
for obj_a, obj_b in pairs:
if self.check_collision(obj_a, obj_b):
self.resolve_collision(obj_a, obj_b)
def get_random_value(self):
"""Deterministic random numbers"""
return self.rng.random()
# Usage in multiplayer game:
def multiplayer_game_loop():
# All clients use same seed
engine = DeterministicPhysicsEngine(seed=game_session_id)
while running:
# Receive inputs from all players
player_inputs = receive_inputs_from_all_clients()
# Sort inputs by player ID for determinism
player_inputs.sort(key=lambda inp: inp.player_id)
# Apply inputs in order
for input in player_inputs:
apply_player_input(input)
# Run physics (identical on all clients)
engine.physics_step(engine.TIMESTEP)
# Render (can differ per client)
render()
Determinism Guarantees:
- Fixed-point math: No floating-point non-determinism
- Sorted iteration: Consistent operation order
- Seeded RNG: Reproducible randomness
- Fixed timestep: Same dt every frame
- Single-threaded: No parallel non-determinism
Pattern 5: Position-Based Dynamics for Cloth
Unconditionally stable cloth simulation:
class ClothSimulation:
def __init__(self, width, height, particle_spacing):
self.particles = []
self.constraints = []
# Create particle grid
for y in range(height):
for x in range(width):
pos = Vector3(x * particle_spacing, y * particle_spacing, 0)
particle = ClothParticle(pos, mass=0.1)
self.particles.append(particle)
# Create distance constraints (structural)
for y in range(height):
for x in range(width):
idx = y * width + x
# Horizontal constraint
if x < width - 1:
self.constraints.append(
DistanceConstraint(
self.particles[idx],
self.particles[idx + 1],
particle_spacing
)
)
# Vertical constraint
if y < height - 1:
self.constraints.append(
DistanceConstraint(
self.particles[idx],
self.particles[idx + width],
particle_spacing
)
)
# Add shear constraints (diagonals) for stiffness
for y in range(height - 1):
for x in range(width - 1):
idx = y * width + x
# Diagonal constraints
self.constraints.append(
DistanceConstraint(
self.particles[idx],
self.particles[idx + width + 1],
particle_spacing * math.sqrt(2)
)
)
self.constraints.append(
DistanceConstraint(
self.particles[idx + 1],
self.particles[idx + width],
particle_spacing * math.sqrt(2)
)
)
def simulate(self, dt, iterations=10):
"""Position-Based Dynamics simulation"""
# 1. Apply external forces (gravity, wind)
for particle in self.particles:
if not particle.fixed:
particle.velocity += Vector3(0, -9.8, 0) * dt # Gravity
particle.velocity += self.wind_force * dt
# 2. Predict positions (Verlet integration)
for particle in self.particles:
if not particle.fixed:
particle.predicted_position = particle.position + particle.velocity * dt
# 3. Solve constraints (multiple iterations for stability)
for _ in range(iterations):
for constraint in self.constraints:
constraint.solve()
# Collision constraints
for particle in self.particles:
if not particle.fixed:
self.resolve_collisions(particle)
# 4. Update velocities and positions
for particle in self.particles:
if not particle.fixed:
particle.velocity = (particle.predicted_position - particle.position) / dt
particle.position = particle.predicted_position
# Velocity damping (air resistance)
particle.velocity *= 0.99
def resolve_collisions(self, particle):
"""Keep particles above ground plane"""
if particle.predicted_position.y < 0:
particle.predicted_position.y = 0
# Friction
particle.velocity *= 0.8
class ClothParticle:
def __init__(self, position, mass):
self.position = position
self.predicted_position = position
self.velocity = Vector3.ZERO
self.mass = mass
self.fixed = False # Pinned particles don't move
class DistanceConstraint:
def __init__(self, particle_a, particle_b, rest_length):
self.particle_a = particle_a
self.particle_b = particle_b
self.rest_length = rest_length
def solve(self):
"""Enforce distance constraint"""
if self.particle_a.fixed and self.particle_b.fixed:
return
delta = self.particle_b.predicted_position - self.particle_a.predicted_position
current_length = delta.length()
if current_length == 0:
return
# Correction to restore rest length
correction = delta * (1.0 - self.rest_length / current_length) * 0.5
# Apply correction (weighted by mass)
if not self.particle_a.fixed:
self.particle_a.predicted_position += correction
if not self.particle_b.fixed:
self.particle_b.predicted_position -= correction
Why Position-Based Dynamics:
- Unconditionally stable: Cannot explode, even with large timesteps
- Fast: Simple position corrections, no matrix solves
- Intuitive: Easy to add constraints (distance, bending, collision)
- Controllable: Iterations directly control stiffness
Used in: Unity's cloth simulation, many game engines
Common Pitfalls
Pitfall 1: Variable Timestep Physics (The Cardinal Sin)
The Mistake:
# ❌ NEVER DO THIS
def update(self, dt):
self.velocity += self.acceleration * dt
self.position += self.velocity * dt
Why It Fails:
- Physics behaves differently at different frame rates
- Integration errors accumulate differently
- Non-deterministic (same inputs ≠ same outputs)
- Multiplayer desyncs guaranteed
Real-World Example: Game ships, works fine on dev machines (high FPS). Players with low-end hardware (30 FPS) report:
- Cars are "floaty" and hard to control
- Objects fall through floors
- Multiplayer is "laggy" (desyncs)
The Fix:
# ✅ ALWAYS use fixed timestep
FIXED_TIMESTEP = 1.0 / 60.0
accumulator = 0.0
def game_loop():
global accumulator
dt = get_frame_time()
accumulator += dt
while accumulator >= FIXED_TIMESTEP:
physics_update(FIXED_TIMESTEP) # Fixed dt
accumulator -= FIXED_TIMESTEP
Detection:
- If
dtappears in physics calculations, you're at risk - Test at different frame rates (30 FPS vs 144 FPS) - should behave identically
Pitfall 2: Physics Explosions (Energy Accumulation)
The Mistake:
# ❌ Explicit Euler - unstable!
velocity += acceleration * dt
position += velocity * dt # Uses OLD velocity
Why It Fails: At high speeds or large dt, explicit Euler adds energy to the system. Objects accelerate indefinitely, leading to "physics explosions".
Symptoms:
- Objects suddenly fly off at high speed
- Ragdolls "explode" into the sky
- Vehicles flip and spin uncontrollably
- Happens more at low frame rates
Real-World Example: Racing game vehicle hits wall at 150 mph. Instead of stopping, it bounces off at 300 mph and flies into orbit.
The Fix:
# ✅ Semi-implicit Euler - stable
velocity += acceleration * dt
position += velocity * dt # Uses NEW velocity
# Energy is conserved (or slightly dissipated)
Why This Works: Semi-implicit Euler is symplectic - it conserves energy rather than adding it. Objects slow down naturally instead of speeding up.
Additional Safety:
# Velocity clamping for safety
MAX_VELOCITY = 100.0
if velocity.length() > MAX_VELOCITY:
velocity = velocity.normalized() * MAX_VELOCITY
Pitfall 3: Tunneling (Missing CCD)
The Mistake:
# ❌ Discrete collision detection only
def check_collision(bullet):
if bullet.position inside wall:
bullet.on_collision()
Why It Fails: Fast objects move multiple body-lengths per frame, skipping over thin obstacles.
The Math:
- Bullet speed: 1000 m/s
- Frame rate: 60 FPS
- Distance per frame: 16.67 meters
- Wall thickness: 0.2 meters
- Result: Bullet is on one side of wall in frame N, other side in frame N+1, never "inside"
Real-World Example: FPS game, players shoot through walls, hitting players on the other side. Especially bad with high ping or low frame rates.
The Fix:
# ✅ Swept collision detection
def update_with_ccd(bullet, dt):
start_pos = bullet.position
end_pos = bullet.position + bullet.velocity * dt
hit, hit_point, hit_time = swept_raycast(start_pos, end_pos, world)
if hit:
bullet.position = lerp(start_pos, end_pos, hit_time)
bullet.on_collision(hit_point)
else:
bullet.position = end_pos
When CCD is Critical:
- Projectiles (bullets, arrows)
- Fast vehicles (>50 m/s)
- Falling objects (terminal velocity ~53 m/s)
- Player characters (lunging attacks, dashes)
Unity Setting:
rigidbody.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
Pitfall 4: Non-Deterministic Multiplayer Physics
The Mistake:
# ❌ Many sources of non-determinism
def update_physics():
for obj in physics_objects: # Undefined iteration order
obj.update(Time.deltaTime) # Variable dt
if random.random() < 0.1: # Non-seeded RNG
obj.apply_impulse()
Why It Fails:
- Dictionary/set iteration order is non-deterministic
- Variable timestep differs on different machines
- Random numbers differ without seeding
- Floating-point math can differ across CPUs
Symptoms:
- Multiplayer clients "desync" (see different physics states)
- Replays don't match original gameplay
- Rollback netcode fails (re-simulation produces different results)
- Heisenbugs (disappear when you try to debug them)
Real-World Example: Fighting game with rollback netcode. Players see different positions for characters, hits don't register, game is "laggy" despite good ping. Problem: physics is non-deterministic.
The Fix:
# ✅ Deterministic physics
class DeterministicEngine:
def __init__(self, seed):
self.FIXED_TIMESTEP = 1.0 / 60.0 # Fixed dt
self.rng = Random(seed) # Seeded RNG
self.objects = [] # List (ordered), not dict/set
def update_physics(self):
# Sort for consistent order
sorted_objects = sorted(self.objects, key=lambda o: o.id)
for obj in sorted_objects:
obj.update(self.FIXED_TIMESTEP) # Fixed dt
if self.rng.random() < 0.1: # Seeded RNG
obj.apply_impulse()
Determinism Checklist:
- Fixed timestep (never variable)
- Sorted iteration order
- Seeded RNG
- Single-threaded physics (or deterministic parallel)
- Consistent floating-point mode
- Same code/compiler on all clients
Pitfall 5: Missing Sub-Stepping for Constraints
The Mistake:
# ❌ Single physics step with stiff constraints
def update(self, dt):
self.solve_constraints() # Only once
self.integrate(dt)
Why It Fails: Stiff constraints (vehicle suspension, rope physics) need multiple iterations to converge. Single iteration causes jittering and instability.
Symptoms:
- Vehicle suspension jitters and bounces
- Ropes stretch unrealistically
- Joints don't stay connected
- Soft bodies "explode" or collapse
Real-World Example: Racing game with suspension springs. At 60 FPS with single iteration, cars bounce violently. Suspension never settles.
The Fix:
# ✅ Sub-stepping for constraint stability
def update(self, dt):
SUB_STEPS = 4
sub_dt = dt / SUB_STEPS
for _ in range(SUB_STEPS):
self.solve_constraints()
self.integrate(sub_dt)
Guidelines:
- Simple rigid bodies: 1 step (no sub-stepping needed)
- Vehicles with suspension: 2-4 steps
- Rope/chain physics: 8-16 steps
- Cloth simulation: 4-10 steps
Unity Example:
// Increase solver iterations for complex constraints
Rigidbody rb = GetComponent<Rigidbody>();
rb.solverIterations = 12; // Default is 6
rb.solverVelocityIterations = 2; // Default is 1
Pitfall 6: Ignoring Center of Mass
The Mistake:
# ❌ Apply force at object origin
def apply_force(self, force):
self.acceleration = force / self.mass
# No torque calculation!
Why It Fails: Forces not applied at center of mass create torque. Ignoring this makes objects rotate incorrectly or not at all.
Real-World Example: Car accelerates, but nose doesn't dip down. Car turns, but body doesn't lean. Feels unrealistic.
The Fix:
# ✅ Calculate torque from force application point
def add_force_at_position(self, force, world_position):
# Linear force
self.acceleration += force / self.mass
# Angular force (torque)
offset = world_position - self.center_of_mass
torque = Vector3.cross(offset, force)
self.angular_acceleration += torque / self.moment_of_inertia
Correct Center of Mass:
# Calculate center of mass for compound object
def calculate_center_of_mass(self):
total_mass = 0
weighted_position = Vector3.ZERO
for part in self.parts:
total_mass += part.mass
weighted_position += part.position * part.mass
self.center_of_mass = weighted_position / total_mass
Unity Tip:
// Set center of mass for vehicles
Rigidbody rb = GetComponent<Rigidbody>();
rb.centerOfMass = new Vector3(0, -0.5f, 0); // Lower = more stable
Pitfall 7: Wrong Collision Response
The Mistake:
# ❌ Incorrect collision impulse
def resolve_collision(a, b):
a.velocity = -a.velocity # Just reverse direction
b.velocity = -b.velocity
Why It Fails:
- Doesn't conserve momentum
- Doesn't account for relative masses
- Ignores coefficient of restitution (bounciness)
- No friction
Real-World Example: Small box hits large truck. Box bounces off at same speed, truck also bounces. Physics looks wrong (momentum not conserved).
The Fix:
# ✅ Physically correct impulse-based collision
def resolve_collision(a, b, collision_normal, restitution=0.5):
# Relative velocity
relative_velocity = a.velocity - b.velocity
velocity_along_normal = Vector3.dot(relative_velocity, collision_normal)
# Don't resolve if separating
if velocity_along_normal > 0:
return
# Calculate impulse magnitude
# Formula: j = -(1 + e) * v_rel · n / (1/m_a + 1/m_b)
impulse_magnitude = -(1 + restitution) * velocity_along_normal
impulse_magnitude /= (1 / a.mass + 1 / b.mass)
# Apply impulse
impulse = collision_normal * impulse_magnitude
a.velocity += impulse / a.mass
b.velocity -= impulse / b.mass
With Friction:
def resolve_collision_with_friction(a, b, collision_normal, restitution=0.5, friction=0.3):
# Normal impulse (from above)
# ... normal impulse calculation ...
# Tangential (friction) impulse
relative_velocity = a.velocity - b.velocity
tangent = relative_velocity - collision_normal * Vector3.dot(relative_velocity, collision_normal)
if tangent.length() > 0:
tangent = tangent.normalized()
# Friction impulse (capped by friction coefficient)
friction_magnitude = -Vector3.dot(relative_velocity, tangent)
friction_magnitude /= (1 / a.mass + 1 / b.mass)
friction_magnitude = clamp(friction_magnitude, -friction * impulse_magnitude, friction * impulse_magnitude)
friction_impulse = tangent * friction_magnitude
a.velocity += friction_impulse / a.mass
b.velocity -= friction_impulse / b.mass
Real-World Examples
Example 1: Unity Vehicle Physics
Unity's WheelCollider (simplified conceptual implementation):
public class UnityVehiclePhysics : MonoBehaviour
{
public WheelCollider frontLeft, frontRight, rearLeft, rearRight;
public float motorTorque = 1500f;
public float brakeTorque = 3000f;
public float maxSteerAngle = 30f;
private Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
// Lower center of mass for stability
rb.centerOfMass = new Vector3(0, -0.5f, 0);
// Continuous collision for high-speed stability
rb.collisionDetectionMode = CollisionDetectionMode.Continuous;
}
void FixedUpdate() // ← Fixed timestep (50 Hz default)
{
// Input
float motor = Input.GetAxis("Vertical") * motorTorque;
float steering = Input.GetAxis("Horizontal") * maxSteerAngle;
float brake = Input.GetKey(KeyCode.Space) ? brakeTorque : 0;
// Apply to wheels
frontLeft.steerAngle = steering;
frontRight.steerAngle = steering;
rearLeft.motorTorque = motor;
rearRight.motorTorque = motor;
frontLeft.brakeTorque = brake;
frontRight.brakeTorque = brake;
rearLeft.brakeTorque = brake;
rearRight.brakeTorque = brake;
}
}
// WheelCollider does internally:
// - Ray-cast suspension (spring-damper model)
// - Tire friction curve (Pacejka model)
// - Sub-stepping for stability
// - Force application at contact point
Key Patterns:
- Fixed timestep via
FixedUpdate()- deterministic physics - Continuous collision detection - prevents tunneling at high speeds
- Lowered center of mass - improves vehicle stability
- WheelCollider handles complex tire physics automatically
Example 2: Unreal Engine Chaos Physics
Vehicle physics in Unreal (Blueprint/C++):
// Unreal's vehicle physics (simplified concepts)
class AVehiclePawn : public APawn
{
public:
void SetupVehicle()
{
// Create physics vehicle
VehicleMovement = CreateDefaultSubobject<UWheeledVehicleMovementComponent>(TEXT("VehicleMovement"));
// Configure wheels
VehicleMovement->WheelSetups.SetNum(4);
// Front wheels (steering)
VehicleMovement->WheelSetups[0].WheelClass = UFrontWheel::StaticClass();
VehicleMovement->WheelSetups[1].WheelClass = UFrontWheel::StaticClass();
// Rear wheels (drive)
VehicleMovement->WheelSetups[2].WheelClass = URearWheel::StaticClass();
VehicleMovement->WheelSetups[3].WheelClass = URearWheel::StaticClass();
// Engine setup
VehicleMovement->MaxEngineRPM = 6000.0f;
VehicleMovement->EngineSetup.TorqueCurve.GetRichCurve()->AddKey(0.0f, 400.0f);
VehicleMovement->EngineSetup.TorqueCurve.GetRichCurve()->AddKey(3000.0f, 500.0f);
VehicleMovement->EngineSetup.TorqueCurve.GetRichCurve()->AddKey(6000.0f, 400.0f);
// Transmission
VehicleMovement->TransmissionSetup.bUseGearAutoBox = true;
VehicleMovement->TransmissionSetup.GearSwitchTime = 0.5f;
}
void Tick(float DeltaTime) override
{
// Physics runs in substepped fixed timestep
// Unreal handles this automatically via Chaos physics
}
};
// Enable deterministic physics in Unreal
// Edit > Project Settings > Physics
// - Substepping: Enabled
// - Max Substep Delta Time: 0.0166 (60 Hz)
// - Max Substeps: 6
Key Patterns:
- Sub-stepping for constraint stability (suspension, gears)
- Torque curves for realistic engine behavior
- Automatic gear shifting (state machine)
- Wheel classes with tire friction models
Example 3: Rocket League Physics
Vehicle physics with aerodynamics (conceptual):
class RocketLeagueVehicle:
def __init__(self):
self.rigidbody = Rigidbody(mass=180) # kg
self.boost_force = 30000 # Newtons
self.jump_impulse = 5000
# Aerial control
self.air_control_torque = 400
self.air_damping = 0.3
# Ground physics
self.wheel_friction = 3.0
self.drift_friction = 1.5
def fixed_update(self, dt): # Fixed 120 Hz
"""Physics update at 120 Hz for responsiveness"""
if self.is_grounded():
self.apply_ground_physics(dt)
else:
self.apply_aerial_physics(dt)
# Boost
if self.boost_active and self.boost_fuel > 0:
boost_direction = self.transform.forward
self.rigidbody.add_force(boost_direction * self.boost_force)
self.boost_fuel -= 30 * dt # 30 boost per second
def apply_ground_physics(self, dt):
"""Wheel-based ground control"""
# Steering input
steer_angle = self.input.steering * 30 # degrees
# Apply wheel forces
for wheel in self.wheels:
# Forward force from throttle
forward_force = self.transform.forward * self.input.throttle * 1500
# Lateral friction (with drift reduction)
friction_multiplier = self.drift_friction if self.input.drift else self.wheel_friction
lateral_force = self.calculate_tire_force(wheel, friction_multiplier)
self.rigidbody.add_force_at_position(
forward_force + lateral_force,
wheel.world_position
)
def apply_aerial_physics(self, dt):
"""Aerial control via orientation torque"""
# Air roll, pitch, yaw
torque = Vector3(
self.input.pitch * self.air_control_torque,
self.input.yaw * self.air_control_torque,
self.input.roll * self.air_control_torque
)
self.rigidbody.add_torque(torque)
# Air damping (reduces rotation speed in air)
self.rigidbody.angular_velocity *= (1.0 - self.air_damping * dt)
# Gravity
self.rigidbody.add_force(Vector3(0, -9.8 * self.rigidbody.mass, 0))
def jump(self):
"""Dodge/jump mechanics"""
if self.can_jump:
self.rigidbody.add_impulse(Vector3.UP * self.jump_impulse)
self.can_jump = False
# Dodge: Add impulse + angular momentum
if self.input.direction.length() > 0:
dodge_direction = self.input.direction.normalized()
self.rigidbody.add_impulse(dodge_direction * self.jump_impulse * 0.8)
# Flip car
axis = Vector3.cross(Vector3.UP, dodge_direction)
self.rigidbody.add_angular_impulse(axis * 10)
# Fixed timestep game loop
PHYSICS_RATE = 120 # Hz (higher for competitive gameplay)
FIXED_DT = 1.0 / PHYSICS_RATE
def game_loop():
accumulator = 0.0
while running:
frame_time = get_delta_time()
accumulator += frame_time
while accumulator >= FIXED_DT:
vehicle.fixed_update(FIXED_DT) # 120 Hz physics
accumulator -= FIXED_DT
render(accumulator / FIXED_DT) # Interpolate
Key Patterns:
- High-frequency fixed timestep (120 Hz) for competitive responsiveness
- State-based physics (grounded vs aerial)
- Drift mechanics via friction modulation
- Aerial control via direct torque application
- Boost as continuous force (not impulse)
Example 4: Half-Life 2 Gravity Gun
Constraint-based object manipulation:
// Valve's physics manipulation (conceptual)
class CGravityGun
{
public:
void HoldObject(CPhysicsObject* pObject)
{
// Create spring constraint to hold object
m_pHeldObject = pObject;
// Target position: In front of player
Vector targetPos = GetPlayer()->GetEyePosition() + GetPlayer()->GetForward() * m_flHoldDistance;
// Create spring-damper constraint
m_pConstraint = CreateSpringConstraint(
pObject,
targetPos,
stiffness: 1000.0f, // Strong spring
damping: 50.0f // Moderate damping
);
// Reduce object's angular velocity for stability
pObject->SetAngularDamping(0.8f);
}
void UpdateHeldObject(float dt)
{
if (!m_pHeldObject) return;
// Update target position
Vector targetPos = GetPlayer()->GetEyePosition() + GetPlayer()->GetForward() * m_flHoldDistance;
// Calculate spring force
Vector offset = targetPos - m_pHeldObject->GetPosition();
Vector force = offset * m_flStiffness;
// Calculate damping force
Vector velocity = m_pHeldObject->GetVelocity();
Vector dampingForce = -velocity * m_flDamping;
// Apply forces
m_pHeldObject->AddForce(force + dampingForce);
// Rotate object to face player (optional)
Quaternion targetRotation = LookRotation(GetPlayer()->GetForward());
Quaternion currentRotation = m_pHeldObject->GetRotation();
Quaternion deltaRotation = ShortestRotation(currentRotation, targetRotation);
Vector torque = deltaRotation.ToTorque() * 100.0f;
m_pHeldObject->AddTorque(torque);
}
void LaunchObject()
{
if (!m_pHeldObject) return;
// Punt object forward
Vector launchVelocity = GetPlayer()->GetForward() * m_flLaunchSpeed;
m_pHeldObject->SetVelocity(launchVelocity);
// Restore normal damping
m_pHeldObject->SetAngularDamping(0.05f);
// Destroy constraint
DestroyConstraint(m_pConstraint);
m_pHeldObject = nullptr;
}
};
Key Patterns:
- Spring-damper constraint for smooth following
- Angular damping to reduce oscillation
- Constraint-based (not direct position setting)
- Smooth transition from constraint to free physics
Example 5: Angry Birds Destruction
Large-scale destructible physics:
// Unity-based destruction (Angry Birds style)
public class DestructibleStructure : MonoBehaviour
{
public float health = 100f;
public GameObject[] debrisPrefabs;
private Rigidbody rb;
private bool isDestroyed = false;
void Start()
{
rb = GetComponent<Rigidbody>();
// Static until hit (optimization)
rb.isKinematic = true;
}
void OnCollisionEnter(Collision collision)
{
// Calculate damage from impact
float impactForce = collision.impulse.magnitude;
float damage = impactForce / rb.mass;
health -= damage;
if (health <= 0 && !isDestroyed)
{
Destroy();
}
else if (rb.isKinematic)
{
// Transition to physics when hit
rb.isKinematic = false;
}
}
void Destroy()
{
isDestroyed = true;
// Spawn debris pieces
for (int i = 0; i < debrisPrefabs.Length; i++)
{
Vector3 spawnPos = transform.position + Random.insideUnitSphere * 0.5f;
GameObject debris = Instantiate(debrisPrefabs[i], spawnPos, Random.rotation);
Rigidbody debrisRb = debris.GetComponent<Rigidbody>();
// Inherit velocity
debrisRb.velocity = rb.velocity;
// Add explosion impulse
Vector3 explosionDir = (spawnPos - transform.position).normalized;
debrisRb.AddForce(explosionDir * 500f, ForceMode.Impulse);
// Random spin
debrisRb.AddTorque(Random.insideUnitSphere * 10f, ForceMode.Impulse);
// Auto-despawn after 5 seconds
Destroy(debris, 5f);
}
// Destroy original object
Destroy(gameObject);
}
}
// Bird projectile
public class Bird : MonoBehaviour
{
private Rigidbody rb;
void Start()
{
rb = GetComponent<Rigidbody>();
// Continuous collision (fast-moving)
rb.collisionDetectionMode = CollisionDetectionMode.ContinuousDynamic;
// High mass for destruction
rb.mass = 10f;
}
void FixedUpdate()
{
// Apply drag for realistic arc
rb.velocity *= 0.99f;
// Rotate to face direction of travel
if (rb.velocity.magnitude > 0.1f)
{
transform.rotation = Quaternion.LookRotation(rb.velocity);
}
}
}
Key Patterns:
- Kinematic → Dynamic transition (optimization)
- Impulse-based damage calculation
- Debris spawning with inherited velocity
- CCD for fast-moving projectiles
- Auto-despawn for performance
Cross-References
Use This Skill WITH:
- performance-optimization-patterns: Physics is expensive; profile and optimize
- netcode-patterns: Deterministic physics for multiplayer synchronization
- state-machines: Managing physics states (grounded, aerial, ragdoll)
- pooling-patterns: Reusing physics objects (debris, particles)
Use This Skill AFTER:
- game-architecture-fundamentals: Understand fixed update loops
- 3d-math-essentials: Vector math, quaternions, coordinate spaces
- collision-detection-patterns: Broad-phase, narrow-phase algorithms
Related Skills:
- character-controller-patterns: Kinematic character control vs physics-based
- animation-blending: Transitioning between animation and ragdoll
- vfx-patterns: Faking physics for visual effects
Performance Optimization
When physics becomes a bottleneck (< 60 FPS with target object count), apply these optimizations:
1. Spatial Partitioning (Broad-Phase Collision)
Problem: Checking every object pair is O(n²) - with 100 objects, that's 4,950 checks per frame.
Solution - Uniform Grid:
class SpatialGrid:
def __init__(self, cell_size):
self.cell_size = cell_size
self.grid = {} # Dict[Tuple[int, int], List[Object]]
def insert(self, obj):
"""Insert object into grid cells it overlaps"""
cells = self.get_cells_for_bounds(obj.bounds)
for cell in cells:
if cell not in self.grid:
self.grid[cell] = []
self.grid[cell].append(obj)
def get_potential_collisions(self, obj):
"""Get only nearby objects (same or adjacent cells)"""
cells = self.get_cells_for_bounds(obj.bounds)
nearby = set()
for cell in cells:
if cell in self.grid:
nearby.update(self.grid[cell])
return nearby
def clear(self):
"""Clear grid each frame before re-inserting"""
self.grid.clear()
# Usage:
def physics_update(dt):
grid = SpatialGrid(cell_size=10.0)
# Insert all objects
for obj in physics_objects:
grid.insert(obj)
# Collision detection (only check nearby)
for obj in physics_objects:
nearby = grid.get_potential_collisions(obj)
for other in nearby:
if obj.id < other.id: # Avoid duplicate checks
check_collision(obj, other)
Performance: O(n) instead of O(n²). With 100 objects, ~400 checks vs 4,950.
2. Physics Islands and Sleeping Objects
Concept: Objects at rest don't need simulation until disturbed.
class SleepingObject:
def __init__(self):
self.is_sleeping = False
self.sleep_timer = 0.0
self.sleep_threshold_time = 0.5 # seconds
self.sleep_velocity_threshold = 0.01 # m/s
def update(self, dt):
if self.is_sleeping:
return # Skip physics simulation
# Check if object should sleep
if self.velocity.length() < self.sleep_velocity_threshold:
self.sleep_timer += dt
if self.sleep_timer > self.sleep_threshold_time:
self.is_sleeping = True
else:
self.sleep_timer = 0.0
# Normal physics update
self.integrate(dt)
def wake_up(self):
"""Called when object is hit or disturbed"""
self.is_sleeping = False
self.sleep_timer = 0.0
# Propagate wake-up through connected objects
def on_collision(obj_a, obj_b):
if obj_a.is_sleeping:
obj_a.wake_up()
if obj_b.is_sleeping:
obj_b.wake_up()
Performance: Large static environments (buildings, props) don't consume CPU when untouched.
3. Level of Detail (LOD) for Physics
Concept: Simplify physics for distant objects.
class PhysicsLOD:
def __init__(self, camera):
self.camera = camera
# Distance thresholds
self.FULL_PHYSICS_DISTANCE = 50.0 # < 50m: Full physics
self.SIMPLIFIED_PHYSICS_DISTANCE = 200.0 # 50-200m: Reduced fidelity
# > 200m: Kinematic or disabled
def get_lod_level(self, obj):
distance = (obj.position - self.camera.position).length()
if distance < self.FULL_PHYSICS_DISTANCE:
return "FULL"
elif distance < self.SIMPLIFIED_PHYSICS_DISTANCE:
return "SIMPLIFIED"
else:
return "KINEMATIC"
def update_physics(self, obj, dt):
lod = self.get_lod_level(obj)
if lod == "FULL":
# Full physics: All features, high solver iterations
obj.solver_iterations = 12
obj.enable_ccd = True
obj.update_full_physics(dt)
elif lod == "SIMPLIFIED":
# Simplified: Reduced iterations, no CCD
obj.solver_iterations = 4
obj.enable_ccd = False
obj.update_full_physics(dt)
else: # KINEMATIC
# No physics simulation, just update position
obj.update_kinematic(dt)
Performance: Can handle 3-10x more objects by reducing fidelity for distant objects.
4. Solver Iteration Tuning
Trade-off: More iterations = more stable constraints, but slower.
// Unity example - tune per object
Rigidbody rb = GetComponent<Rigidbody>();
// Simple objects (debris, props)
rb.solverIterations = 4; // Velocity constraints
rb.solverVelocityIterations = 1; // Position constraints
// Complex objects (vehicles with suspension)
rb.solverIterations = 12;
rb.solverVelocityIterations = 2;
// Critical objects (player character)
rb.solverIterations = 20;
rb.solverVelocityIterations = 4;
Guideline: Start low (4/1), increase only if jittering or instability observed.
5. Collision Layer Matrix
Concept: Many object pairs never need collision checks.
// Unity: Edit > Project Settings > Physics > Layer Collision Matrix
// Example layer setup:
// - Layer 8: PlayerBullets (collide with Enemies, Environment)
// - Layer 9: EnemyBullets (collide with Player, Environment)
// - Layer 10: Environment (collide with everything)
// - Layer 11: Debris (collide only with Environment)
// Bullets don't collide with each other (huge savings)
Physics.IgnoreLayerCollision(8, 8); // PlayerBullets vs PlayerBullets
Physics.IgnoreLayerCollision(9, 9); // EnemyBullets vs EnemyBullets
Physics.IgnoreLayerCollision(8, 9); // PlayerBullets vs EnemyBullets
// Debris doesn't collide with bullets (performance)
Physics.IgnoreLayerCollision(11, 8);
Physics.IgnoreLayerCollision(11, 9);
Performance: Can reduce collision checks by 50-90% depending on game.
Performance Optimization Checklist
- Spatial partitioning implemented (grid, octree, or sort-and-sweep)
- Sleeping/waking system for static objects
- Physics LOD based on distance to camera
- Solver iterations tuned per object type (not all need 12)
- Collision layers configured (ignore unnecessary pairs)
- Profiled to identify actual bottleneck (don't guess)
Debugging Guide
Debugging Physics Explosions
Symptoms: Objects suddenly fly off at extreme speeds, spin wildly, or "explode" apart.
Diagnosis Checklist:
Check integration method:
# ❌ BAD: Explicit Euler velocity += acceleration * dt position += velocity * dt # Uses OLD velocity # ✅ GOOD: Semi-implicit Euler velocity += acceleration * dt position += velocity * dt # Uses NEW velocityAdd velocity clamping:
MAX_VELOCITY = 100.0 # m/s (adjust for your game) if velocity.length() > MAX_VELOCITY: velocity = velocity.normalized() * MAX_VELOCITYCheck for NaN/Infinity:
def safe_normalize(vector): length = vector.length() if length < 0.0001 or math.isnan(length) or math.isinf(length): return Vector3(0, 1, 0) # Default direction return vector / lengthVerify timestep size:
assert dt <= 0.033, f"Timestep too large: {dt}s (should be ≤ 0.033)"
Debugging Tunneling
Symptoms: Fast objects pass through walls, bullets hit players behind walls.
Diagnosis Checklist:
Calculate required CCD threshold:
# Rule: Enable CCD if object moves > half its size per frame velocity_per_frame = velocity * dt object_size = collider.radius * 2 if velocity_per_frame > object_size * 0.5: print(f"⚠️ CCD required! Moving {velocity_per_frame}m, size {object_size}m") enable_ccd()Verify CCD is enabled:
// Unity Debug.Log($"CCD Mode: {rigidbody.collisionDetectionMode}"); // Should be Continuous or ContinuousDynamicCheck collider thickness:
# Walls should be at least 2x the distance fast objects travel per frame min_wall_thickness = max_object_speed * FIXED_TIMESTEP * 2 print(f"Minimum wall thickness: {min_wall_thickness}m")
Debugging Multiplayer Desyncs
Symptoms: Clients see different physics states, objects in different positions.
Diagnosis Process:
Enable determinism logging:
def log_physics_state(frame): # Log complete physics state each frame state_hash = hash_physics_state(physics_objects) print(f"Frame {frame}: Hash {state_hash}") # Log first object details for debugging obj = physics_objects[0] print(f" Obj[0]: pos={obj.position}, vel={obj.velocity}")Compare logs from both clients:
# If hashes differ, find first divergence frame diff client1.log client2.logCheck iteration order:
# ❌ BAD: Undefined order for obj in physics_objects: # If dict/set obj.update() # ✅ GOOD: Sorted order for obj in sorted(physics_objects, key=lambda o: o.id): obj.update()Verify RNG synchronization:
# Both clients must use same seed rng = Random(seed=game_session_id) # Log RNG calls value = rng.random() print(f"RNG: {value}") # Should match on both clientsTest on different machines:
# Different CPUs can produce different floating-point results # Use fixed-point math if needed: def to_fixed(value, scale=1000): return int(value * scale) def from_fixed(fixed_value, scale=1000): return fixed_value / scale
Debugging Jittery Constraints
Symptoms: Vehicle suspension bounces, ropes vibrate, joints don't settle.
Diagnosis:
Increase sub-steps:
# Current: 1 step physics_update(dt) # Fix: 4 sub-steps for _ in range(4): physics_update(dt / 4)Increase solver iterations (Unity):
rigidbody.solverIterations = 12; // Try 8, 12, 16Check spring stiffness (might be too high):
# Too stiff: stiffness = 100000 # Better: stiffness = 50000 (or lower) suspension_stiffness = 50000
Common Debugging Commands
# Enable physics visualization
debug_draw_colliders = True
debug_draw_forces = True
debug_draw_velocities = True
# Frame-by-frame physics
pause_physics = True
step_one_frame = False
if step_one_frame or not pause_physics:
physics_update(dt)
step_one_frame = False
# Slow motion
time_scale = 0.1 # 10x slower
physics_update(dt * time_scale)
Testing Checklist
Fixed Timestep Verification
- Physics runs at consistent rate regardless of frame rate
- Test at 30 FPS, 60 FPS, 144 FPS - identical behavior
- Rendering interpolates smoothly between physics states
- No spiral of death at low frame rates (frame time clamped)
Stability Testing
- No physics explosions at high speeds
- Objects settle into rest (don't vibrate forever)
- Stacked objects are stable (no jittering)
- Constraints converge (suspension, ropes, chains)
Collision Detection
- Fast objects don't tunnel through thin walls
- CCD enabled for projectiles and fast vehicles
- Collision response conserves momentum
- Friction behaves realistically
Determinism Testing (Multiplayer)
- Same inputs produce same outputs (bit-identical)
- Fixed timestep (never variable)
- Sorted iteration order (by ID or consistent key)
- Seeded random number generator
- Single-threaded physics or deterministic parallel
- Tested on different machines (same results)
Performance Testing
- Meets target frame rate with max object count
- Spatial partitioning for collision broad-phase
- Physics islands for sleeping objects
- LOD for distant physics objects
Edge Cases
- Handles zero/infinite/NaN values gracefully
- Extreme velocities clamped or handled
- Extreme forces don't cause explosions
- Division by zero checks (mass, distance, etc.)
Integration Testing
- Physics integrates with game state (damage, score, etc.)
- Transitions between physics states work (kinematic ↔ dynamic)
- Save/load preserves physics state
- Replay system reproduces physics accurately
Summary
Physics simulation for games requires balancing realism, performance, and stability. The core principles are:
- Always use fixed timestep - Determinism and stability
- Choose integration method carefully - Semi-implicit Euler for most cases
- Use CCD for fast objects - Prevent tunneling
- Design for determinism - Critical for multiplayer
- Sub-step complex constraints - Vehicles, cloth, ropes
- Fake physics when possible - Visual effects don't need real physics
- Test under pressure - High speeds, low frame rates, edge cases
Master these patterns and avoid the common pitfalls, and your physics systems will be stable, performant, and feel great to play.