Table of Contents
- Introduction
- Overview
- Prerequisites
- What We’ll Build
- Understanding the Fundamentals
- Vector Mathematics
- Physics Concepts
- Basic Structure and Vector Operations
- Vector Implementation
- Essential Vector Operations
- Core Components
- Body Definition
- Shape Types
- World Configuration
1. Introduction
Overview
Building a physics engine is an exciting journey into the world of game development and simulation. This tutorial will guide you through creating a 2D physics engine from scratch, explaining every component in detail. We’ll cover everything from basic vector mathematics to complex collision resolution.
Prerequisites
To follow this tutorial, you should have:
- Intermediate knowledge of C++
- Basic understanding of vector mathematics
- Familiarity with fundamental physics concepts
- A C++ development environment set up
What We’ll Build
We’re going to create a complete 2D physics engine with the following features:
- Rigid body dynamics
- Circle and AABB (Axis-Aligned Bounding Box) collision detection
- Spatial partitioning for efficient collision detection
- Impulse-based collision resolution
- Gravity and force application
- Position and velocity integration
2. Understanding the Fundamentals
Vector Mathematics
At the heart of our physics engine lies vector mathematics. We’ll be working extensively with 2D vectors for:
- Position representation
- Velocity calculations
- Force applications
- Collision detection and response
Physics Concepts
Before diving into the code, let’s understand the key physics concepts we’ll be implementing:
- Newton’s laws of motion
- Linear momentum
- Impulse forces
- Coefficient of restitution
- Friction
- Conservation of energy
3. Basic Structure and Vector Operations
Vector Implementation
Let’s start with our fundamental building block - the 2D vector structure:
struct vec2 {
double x;
double y;
};
This simple structure represents a point or vector in 2D space. We’ll build several operations around this basic type.
Essential Vector Operations
Let’s implement the core vector operations we’ll need throughout our engine:
// Vector addition
vec2 vec2_add(const vec2 &a, const vec2 &b) {
return vec2{a.x + b.x, a.y + b.y};
}
// Vector subtraction
vec2 vec2_sub(const vec2 &a, const vec2 &b) {
return vec2{a.x - b.x, a.y - b.y};
}
// Scalar multiplication
vec2 vec2_mul(const vec2 &a, double scalar) {
return vec2{a.x * scalar, a.y * scalar};
}
// Dot product
double vec2_dot(const vec2 &a, const vec2 &b) {
return a.x * b.x + a.y * b.y;
}
// Cross product (2D version returns scalar)
double vec2_cross(const vec2 &a, const vec2 &b) {
return a.x * b.y - b.x * a.y;
}
Each of these operations serves a specific purpose:
vec2_add
: Used for combining vectors, such as adding velocity to positionvec2_sub
: Used for finding direction vectors between pointsvec2_mul
: Used for scaling vectors (e.g., applying force)vec2_dot
: Used for projection calculations and finding anglesvec2_cross
: Used for determining rotational direction and area calculations
Additional utility functions:
// Vector normalization
void vec2_normalize(vec2 &a) {
double length = sqrt(a.x * a.x + a.y * a.y);
if (length > 0) {
a.x /= length;
a.y /= length;
}
}
// Vector length calculation
double vec2_length(const vec2 &a) {
return sqrt(a.x * a.x + a.y * a.y);
}
// Squared length (optimization for comparisons)
double vec2_length_squared(const vec2 &a) {
return a.x * a.x + a.y * a.y;
}
// Vector rotation
void vec2_rotate(vec2 &a, const double &theta) {
double original_x = a.x;
a.x = original_x * cos(theta) - a.y * sin(theta);
a.y = original_x * sin(theta) + a.y * cos(theta);
}
These utility functions are crucial for:
- Normalizing vectors for direction calculations
- Finding distances between objects
- Optimizing calculations using squared lengths
- Rotating vectors for angular motion
4. Core Components
Body Definition
The core of our physics engine is the Body
structure, which represents any physical object in our simulation:
struct Body {
vec2 position, velocity, acceleration, forceAccumulator;
double mass, inverseMass, damping, angle;
bool isStatic, isActive;
enum ShapeType {
SHAPE_CIRCLE,
SHAPE_AABB,
} shapeType;
union {
Circle circle;
AABB aabb;
} shape;
};
Let’s break down each component:
- Position properties:
position
: Current position in 2D spaceangle
: Rotation angle (in radians)
- Motion properties:
velocity
: Linear velocityacceleration
: Current accelerationforceAccumulator
: Sum of all forces acting on the body
- Physical properties:
mass
: Body massinverseMass
: Precalculated 1/mass (0 for static bodies)damping
: Velocity damping factor
- State flags:
isStatic
: Whether the body can moveisActive
: Whether the body participates in simulation
- Shape information:
shapeType
: Enum indicating circle or AABBshape
: Union of possible shape types
Shape Types
We support two basic shape types:
struct AABB {
vec2 min, max;
};
struct Circle {
vec2 center;
double radius;
};
AABB (Axis-Aligned Bounding Box):
- Defined by minimum and maximum points
- Simple and efficient collision detection
- Limited to rectangular shapes aligned with axes
Circle:
- Defined by center point and radius
- Uniform collision detection in all directions
- Perfect for round objects
5. World Configuration and Management
World Structure
The physics world is our simulation container that manages all bodies and their interactions. Let’s examine its implementation:
struct WorldConfig {
vec2 gravity;
double maxDeltaTime;
AABB bounds;
WorldConfig() {
gravity.x = 0.0;
gravity.y = -9.81; // Default Earth gravity
maxDeltaTime = 0.016; // ~60 FPS
bounds.min = {-100.0, -100.0};
bounds.max = {100.0, 100.0};
}
};
struct World {
vector<Body> bodies;
WorldConfig config;
int activeBodyCount{0};
bool isPaused{false};
vector<int> queryResultIndices;
Grid grid; // Spatial partitioning grid
};
Key components:
WorldConfig
: Configuration parametersgravity
: Global gravity vectormaxDeltaTime
: Maximum time step (prevents instability)bounds
: World boundaries
World
: Main simulation containerbodies
: Vector of all physics bodiesactiveBodyCount
: Number of non-static bodiesgrid
: Spatial partitioning system
World Management Functions
void initWorld(World &world, const WorldConfig config = WorldConfig{}) {
world.bodies.clear();
world.config = config;
world.activeBodyCount = 0;
world.isPaused = false;
world.queryResultIndices.reserve(100);
initGrid(world.grid, config.bounds, 10.0);
}
void clearWorld(World &world) {
world.bodies.clear();
world.activeBodyCount = 0;
world.queryResultIndices.clear();
}
Adding and removing bodies:
int addBody(World &world, Body &body) {
world.bodies.push_back(body);
if (body.isActive)
world.activeBodyCount++;
return static_cast<int>(world.bodies.size() - 1);
}
void removeBody(World &world, int bodyIndex) {
if (bodyIndex < 0 || bodyIndex >= world.bodies.size())
return;
if (world.bodies[bodyIndex].isActive) {
world.activeBodyCount--;
}
if (bodyIndex < world.bodies.size() - 1) {
world.bodies[bodyIndex] = world.bodies.back();
}
world.bodies.pop_back();
}
Helper Functions for Body Creation
int addCircle(World &world, vec2 position, double radius, double mass) {
Body body{};
body.position = position;
body.velocity = {0, 0};
body.acceleration = {0, 0};
body.mass = mass;
body.inverseMass = mass > 0.0 ? 1.0 / mass : 0.0;
body.damping = 0.99;
body.isStatic = mass <= 0;
body.isActive = true;
body.angle = 0.0;
body.forceAccumulator = {0, 0};
body.shapeType = Body::SHAPE_CIRCLE;
body.shape.circle = {position, radius};
return addBody(world, body);
}
int addBox(World &world, vec2 position, vec2 size, double mass) {
Body body{};
body.position = position;
body.velocity = {0, 0};
body.acceleration = {0, 0};
body.mass = mass;
body.inverseMass = mass > 0 ? 1.0 / mass : 0;
body.damping = 0.99;
body.isStatic = mass <= 0;
body.isActive = true;
body.angle = 0;
body.shapeType = Body::SHAPE_AABB;
body.shape.aabb = {
{position.x - size.x / 2, position.y - size.y / 2},
{position.x + size.x / 2, position.y + size.y / 2}
};
return addBody(world, body);
}
6. Force Application and Integration
Force Management
Forces are fundamental to physics simulation. We implement a force accumulator pattern:
void applyForce(Body &body, const vec2 &force) {
if (body.isStatic || !body.isActive) {
return;
}
body.forceAccumulator = vec2_add(body.forceAccumulator, force);
}
void applyImpulse(Body &body, const vec2 &impulse) {
if (body.isStatic || !body.isActive) {
return;
}
vec2 deltaVelocity = vec2_mul(impulse, body.inverseMass);
body.velocity = vec2_add(body.velocity, deltaVelocity);
}
void clearForces(Body &body) {
body.forceAccumulator = {0.0, 0.0};
}
Gravity Application
const vec2 GRAVITY{0.0, -9.81};
void applyGravity(Body &body) {
if (body.isStatic || !body.isActive)
return;
vec2 gravityForce = vec2_mul(GRAVITY, body.mass);
applyForce(body, gravityForce);
}
Physics Integration
We use semi-implicit Euler integration for updating positions and velocities:
void integrateLinearMotion(Body &body, double dt) {
if (body.isStatic || !body.isActive)
return;
if (dt <= 0.0)
return;
// Calculate acceleration from accumulated forces
body.acceleration = vec2_mul(body.forceAccumulator, body.inverseMass);
// Update velocity
vec2 deltaVelocity = vec2_mul(body.acceleration, dt);
body.velocity = vec2_add(body.velocity, deltaVelocity);
// Apply damping
body.velocity = vec2_mul(body.velocity, pow(body.damping, dt));
// Update position
vec2 deltaP = vec2_add(
vec2_mul(body.velocity, dt),
vec2_mul(body.acceleration, 0.5 * dt * dt)
);
body.position = vec2_add(body.position, deltaP);
// Update shape positions
if (body.shapeType == Body::SHAPE_CIRCLE) {
body.shape.circle.center = body.position;
} else {
vec2 halfSize = vec2_mul(
vec2_sub(body.shape.aabb.max, body.shape.aabb.min),
0.5
);
body.shape.aabb.min = vec2_sub(body.position, halfSize);
body.shape.aabb.max = vec2_add(body.position, halfSize);
}
clearForces(body);
}
The integration process:
- Calculate acceleration from accumulated forces
- Update velocity using current acceleration
- Apply velocity damping to simulate air resistance
- Update position using velocity and acceleration
- Update shape-specific properties
- Clear accumulated forces for next frame
World Stepping
The main simulation step function:
void stepWorld(World &world, double dt) {
if (world.isPaused)
return;
// Clamp dt to prevent instability
dt = min(dt, world.config.maxDeltaTime);
// Update physics
updatePhysics(world, dt);
// Update spatial partitioning
updateBroadPhase(world.grid, world.bodies);
}
void updatePhysics(World &world, double dt) {
for (auto &body : world.bodies) {
if (body.isActive) {
applyGravity(body);
integrateLinearMotion(body, dt);
}
}
}
Utility Functions
double calculateArea(const Body &body) {
if (body.shapeType == Body::SHAPE_CIRCLE) {
return M_PI * body.shape.circle.radius * body.shape.circle.radius;
} else {
vec2 size = vec2_sub(body.shape.aabb.max, body.shape.aabb.min);
return size.x * size.y;
}
}
double calculateMOI(const Body &body) {
if (body.shapeType == Body::SHAPE_CIRCLE) {
return 0.5 * body.mass * body.shape.circle.radius *
body.shape.circle.radius;
} else {
vec2 size = vec2_sub(body.shape.aabb.max, body.shape.aabb.min);
return body.mass * (size.x * size.x + size.y * size.y) / 12;
}
}
AABB getShapeBoundingBox(const Body &body) {
if (body.shapeType == Body::SHAPE_CIRCLE) {
return {
{body.position.x - body.shape.circle.radius,
body.position.y - body.shape.circle.radius},
{body.position.x + body.shape.circle.radius,
body.position.y + body.shape.circle.radius}
};
}
return body.shape.aabb;
}
7. Spatial Partitioning System
Understanding Spatial Partitioning
Before diving into the code, let’s understand why spatial partitioning is crucial:
- In a physics simulation, checking collisions between every pair of objects (O(n²)) becomes extremely inefficient as the number of objects grows
- Spatial partitioning divides the world into a grid of cells
- Objects are mapped to grid cells based on their position
- Only objects in the same or neighboring cells need to be checked for collisions
- This reduces collision checks from O(n²) to O(n) in most cases
Grid Implementation
struct GridCell {
vector<int> bodyIndex; // Stores indices of bodies in this cell
};
struct Grid {
vector<GridCell> cells; // 1D array representing 2D grid
double cellSize; // Size of each cell
int numRows, numCols; // Grid dimensions
vec2 worldMin, worldMax; // World boundaries
};
Let’s examine each component:
GridCell
: Contains indices of bodies that overlap this cellGrid
: Main grid structurecells
: 1D vector representing 2D grid for cache efficiencycellSize
: Determines granularity of spatial partitioningnumRows/numCols
: Grid dimensionsworldMin/worldMax
: World boundaries for grid mapping
Grid Initialization and Management
void initGrid(Grid &grid, AABB &worldBound, double cellSize) {
// Set grid boundaries
grid.worldMin = worldBound.min;
grid.worldMax = worldBound.max;
grid.cellSize = cellSize;
// Calculate grid dimensions
vec2 worldSize = vec2_sub(worldBound.max, worldBound.min);
grid.numRows = static_cast<int>(worldSize.x / cellSize);
grid.numCols = static_cast<int>(worldSize.y / cellSize);
// Initialize cells
grid.cells.resize(grid.numRows * grid.numCols);
}
This function:
- Sets the grid boundaries based on world bounds
- Sets the cell size (important for performance tuning)
- Calculates how many rows and columns we need
- Resizes the cells vector to accommodate all cells
Grid Operations
Converting 2D grid coordinates to 1D array index:
int getCellIndex(const Grid &grid, int row, int col) {
return row * grid.numCols + col;
}
Converting world position to grid coordinates:
void getGridCell(const Grid &grid, const vec2 &position, int &row, int &col) {
// Calculate relative position from world minimum
vec2 relativePos = vec2_sub(position, grid.worldMin);
// Convert to grid coordinates
col = static_cast<int>(relativePos.x / grid.cellSize);
row = static_cast<int>(relativePos.y / grid.cellSize);
// Clamp to grid boundaries
col = max(0, min(col, grid.numCols - 1));
row = max(0, min(row, grid.numRows - 1));
}
This function:
- Calculates position relative to grid origin (worldMin)
- Divides by cell size to get grid coordinates
- Clamps values to ensure they’re within grid bounds
Inserting Bodies into the Grid
void insertBodyIntoGrid(Grid &grid, int bodyIndex, const Body &body) {
// Get body's bounding box
AABB bounds = getShapeBoundingBox(body);
// Calculate grid cells that contain the body
int startRow, startCol, endRow, endCol;
getGridCell(grid, bounds.min, startRow, startCol);
getGridCell(grid, bounds.max, endRow, endCol);
// Insert body index into all overlapping cells
for (int row = startRow; row <= endRow; ++row) {
for (int col = startCol; col <= endCol; ++col) {
int cellIndex = getCellIndex(grid, row, col);
grid.cells[cellIndex].bodyIndex.push_back(bodyIndex);
}
}
}
This function:
- Gets the body’s AABB (bounding box)
- Determines which grid cells the AABB overlaps
- Adds the body’s index to all overlapping cells
- A body can be in multiple cells if it spans cell boundaries
Updating the Broad Phase
void updateBroadPhase(Grid &grid, const vector<Body> &bodies) {
// Clear previous frame's data
clearGrid(grid);
// Insert active bodies into grid
for (size_t i = 0; i < bodies.size(); ++i) {
if (bodies[i].isActive) {
insertBodyIntoGrid(grid, i, bodies[i]);
}
}
}
8. Collision Detection System
Broad Phase Collision Detection
struct BroadPhaseCollisionPair {
int bodyA;
int bodyB;
};
vector<BroadPhaseCollisionPair> getPotentialCollisionPairs(const Grid &grid) {
vector<BroadPhaseCollisionPair> pairs;
// Helper function to create unique pair key
auto createPairKey = [](int id1, int id2) -> uint64_t {
if (id1 > id2)
swap(id1, id2);
return (static_cast<uint64_t>(id1) << 32) | static_cast<uint64_t>(id2);
};
// Track pairs we've already checked
unordered_set<uint64_t> addedPairs;
// Check each cell
for (int row = 0; row < grid.numRows; ++row) {
for (int col = 0; col < grid.numCols; ++col) {
const auto ¤tCell = grid.cells[getCellIndex(grid, row, col)];
// Check pairs within the same cell
for (size_t i = 0; i < currentCell.bodyIndex.size(); ++i) {
for (size_t j = i + 1; j < currentCell.bodyIndex.size(); ++j) {
int id1 = currentCell.bodyIndex[i];
int id2 = currentCell.bodyIndex[j];
uint64_t pairKey = createPairKey(id1, id2);
// Add only if we haven't seen this pair before
if (addedPairs.insert(pairKey).second) {
pairs.push_back({id1, id2});
}
}
}
// Direction vectors for neighboring cells
const int dx[] = {1, 1, 0, -1};
const int dy[] = {0, 1, 1, 1};
// Check neighboring cells
for (int dir = 0; dir < 4; ++dir) {
int newCol = col + dx[dir];
int newRow = row + dy[dir];
// Skip if neighbor is out of bounds
if (newCol >= 0 && newCol < grid.numCols &&
newRow >= 0 && newRow < grid.numRows) {
const auto &adjacentCell =
grid.cells[getCellIndex(grid, newRow, newCol)];
// Check pairs between current cell and neighbor
for (int id1 : currentCell.bodyIndex) {
for (int id2 : adjacentCell.bodyIndex) {
uint64_t pairKey = createPairKey(id1, id2);
if (addedPairs.insert(pairKey).second) {
pairs.push_back({id1, id2});
}
}
}
}
}
}
}
return pairs;
}
This broad phase collision detection:
- Examines each grid cell
- Checks for potential collisions between bodies in the same cell
- Checks neighboring cells for additional potential collisions
- Uses a hash set to prevent duplicate pairs
- Returns all potential collision pairs for detailed checking
Narrow Phase Collision Detection
First, let’s implement the basic collision checks for different shape combinations:
bool circleCircleCollision(const Circle &c1, const Circle &c2) {
// Calculate vector between centers
vec2 diff = vec2_sub(c1.center, c2.center);
// Compare squared distances (avoid square root)
double distSq = vec2_length_squared(diff);
double radiusSum = c1.radius + c2.radius;
return distSq <= radiusSum * radiusSum;
}
bool aabbAabbCollision(const AABB &aabb1, const AABB &aabb2) {
// Check for overlap on each axis
if (aabb1.max.x < aabb2.min.x || aabb1.min.x > aabb2.max.x)
return false;
if (aabb1.max.y < aabb2.min.y || aabb1.min.y > aabb2.max.y)
return false;
return true;
}
bool circleAabbCollision(const Circle &c, const AABB &aabb) {
// Find closest point on AABB to circle center
double closestX = max(aabb.min.x, min(c.center.x, aabb.max.x));
double closestY = max(aabb.min.y, min(c.center.y, aabb.max.y));
// Calculate squared distance to closest point
double distanceX = c.center.x - closestX;
double distanceY = c.center.y - closestY;
double distanceSquared = distanceX * distanceX + distanceY * distanceY;
return distanceSquared <= (c.radius * c.radius);
}
Then we combine these in a general collision check function:
bool checkCollision(const Body &bodyA, const Body &bodyB) {
if (bodyA.shapeType == Body::SHAPE_CIRCLE &&
bodyB.shapeType == Body::SHAPE_CIRCLE) {
return circleCircleCollision(bodyA.shape.circle, bodyB.shape.circle);
}
else if (bodyA.shapeType == Body::SHAPE_AABB &&
bodyB.shapeType == Body::SHAPE_AABB) {
return aabbAabbCollision(bodyA.shape.aabb, bodyB.shape.aabb);
}
else if (bodyA.shapeType == Body::SHAPE_CIRCLE &&
bodyB.shapeType == Body::SHAPE_AABB) {
return circleAabbCollision(bodyA.shape.circle, bodyB.shape.aabb);
}
else if (bodyA.shapeType == Body::SHAPE_AABB &&
bodyB.shapeType == Body::SHAPE_CIRCLE) {
return circleAabbCollision(bodyB.shape.circle, bodyA.shape.aabb);
}
return false;
}
Finally, we filter the potential collision pairs to find actual collisions:
vector<BroadPhaseCollisionPair> getActualCollisions(
const vector<BroadPhaseCollisionPair> &potentialPairs,
const vector<Body> &bodies) {
vector<BroadPhaseCollisionPair> actualCollisions;
for (const auto &pair : potentialPairs) {
const Body &bodyA = bodies[pair.bodyA];
const Body &bodyB = bodies[pair.bodyB];
if (checkCollision(bodyA, bodyB)) {
actualCollisions.push_back(pair);
}
}
return actualCollisions;
}
9. Collision Resolution System
Understanding Collision Resolution
Before diving into the code, let’s understand the key concepts of collision resolution:
-
Collision Response: When two bodies collide, we need to:
- Calculate impulse forces to change their velocities
- Prevent bodies from interpenetrating
- Apply friction and restitution effects
-
Impulse-Based Physics:
- Uses instantaneous changes in velocity
- Preserves momentum and energy (with restitution)
- More stable than force-based methods for collisions
-
Position Correction:
- Prevents objects from sinking into each other
- Maintains simulation stability
- Uses a percentage-based correction factor
Circle-Circle Collision Resolution
void resolveCircleCircleCollision(Body &a, Body &b) {
// Calculate collision vector
vec2 distance = vec2_sub(b.position, a.position);
vec2 normalVector = distance;
vec2_normalize(normalVector);
// Calculate collision tangent for friction
vec2 tangentVector = {-1.0 * normalVector.y, normalVector.x};
// Calculate relative velocity
vec2 relativeVelocity = vec2_sub(b.velocity, a.velocity);
double velocitySeperation = vec2_dot(relativeVelocity, normalVector);
// Skip if objects are moving apart
if (velocitySeperation > 0)
return;
// Calculate impulse scalar
double impulse = -1.0 * (1 + e) / (a.inverseMass + b.inverseMass)
* velocitySeperation;
// Apply impulse along normal vector
a.velocity = vec2_sub(a.velocity,
vec2_mul(normalVector, impulse * a.inverseMass));
b.velocity = vec2_add(b.velocity,
vec2_mul(normalVector, impulse * b.inverseMass));
// Position correction to prevent sinking
const double k = 0.4; // position correction factor
double totalRadius = a.shape.circle.radius + b.shape.circle.radius;
double penetrationDepth = totalRadius - vec2_length(distance);
if (penetrationDepth > 0) {
vec2 correction = vec2_mul(
normalVector,
(k * penetrationDepth) / (a.inverseMass + b.inverseMass)
);
// Only move if body isn't static
if (!a.isStatic) {
a.position = vec2_sub(a.position,
vec2_mul(correction, a.inverseMass));
}
if (!b.isStatic) {
b.position = vec2_add(b.position,
vec2_mul(correction, b.inverseMass));
}
}
}
Let’s break down the circle-circle collision resolution:
-
Normal Vector Calculation:
- Calculate vector between circle centers
- Normalize it to get collision normal
- Calculate tangent vector for friction calculations
-
Velocity Resolution:
- Calculate relative velocity between bodies
- Project velocity onto normal vector
- Check if objects are moving apart (early exit if true)
-
Impulse Calculation:
- Use coefficient of restitution (e)
- Consider inverse masses for proper impulse distribution
- Apply impulse to both bodies’ velocities
-
Position Correction:
- Calculate penetration depth
- Apply correction based on mass ratio
- Use correction factor (k) to prevent overshooting
AABB-AABB Collision Resolution
void resolveAABBCollision(Body &a, Body &b) {
// Calculate centers and half-sizes
vec2 centerA = a.position;
vec2 centerB = b.position;
vec2 halfSizeA = vec2_mul(
vec2_sub(a.shape.aabb.max, a.shape.aabb.min),
0.5
);
vec2 halfSizeB = vec2_mul(
vec2_sub(b.shape.aabb.max, b.shape.aabb.min),
0.5
);
// Calculate delta between centers
vec2 delta = vec2_sub(centerB, centerA);
// Calculate overlap on each axis
vec2 overlap = {
halfSizeA.x + halfSizeB.x - fabs(delta.x),
halfSizeA.y + halfSizeB.y - fabs(delta.y)
};
// Determine collision normal based on smallest overlap
vec2 normal;
double penetration;
if (overlap.x > 0 && overlap.y > 0) {
if (overlap.x < overlap.y) {
normal = (delta.x < 0) ?
(vec2){-1.0, 0.0} : (vec2){1.0, 0.0};
penetration = overlap.x;
} else {
normal = (delta.y < 0) ?
(vec2){0.0, -1.0} : (vec2){0.0, 1.0};
penetration = overlap.y;
}
// Calculate relative velocity
vec2 relativeVel = vec2_sub(b.velocity, a.velocity);
double normalVel = vec2_dot(relativeVel, normal);
// Early exit for objects moving apart
const double MIN_VELOCITY = 0.01;
if (normalVel > -MIN_VELOCITY) {
// Objects moving apart or too slow - just correct position
if (!a.isStatic) {
a.position = vec2_sub(a.position,
vec2_mul(normal, penetration * 0.5));
}
if (!b.isStatic) {
b.position = vec2_add(b.position,
vec2_mul(normal, penetration * 0.5));
}
return;
}
// Calculate impulse
const double RESTITUTION = 0.3;
double totalInverseMass = a.inverseMass + b.inverseMass;
if (totalInverseMass <= 0)
return;
// Apply restitution based on velocity
double restitution = vec2_length(relativeVel) < 2.0 ?
0.0 : RESTITUTION;
double j = -(1.0 + restitution) * normalVel / totalInverseMass;
// Apply impulse
vec2 impulse = vec2_mul(normal, j);
if (!a.isStatic) {
a.velocity = vec2_sub(a.velocity,
vec2_mul(impulse, a.inverseMass));
}
if (!b.isStatic) {
b.velocity = vec2_add(b.velocity,
vec2_mul(impulse, b.inverseMass));
}
// Apply friction
const double FRICTION = 0.4;
vec2 tangent = {
relativeVel.x - normalVel * normal.x,
relativeVel.y - normalVel * normal.y
};
double tangentLen = vec2_length(tangent);
if (tangentLen > MIN_VELOCITY) {
tangent = vec2_mul(tangent, 1.0 / tangentLen);
double jt = -vec2_dot(relativeVel, tangent) / totalInverseMass;
// Apply friction impulse
vec2 frictionImpulse = vec2_mul(tangent, jt * FRICTION);
if (!a.isStatic) {
a.velocity = vec2_sub(a.velocity,
vec2_mul(frictionImpulse, a.inverseMass));
}
if (!b.isStatic) {
b.velocity = vec2_add(b.velocity,
vec2_mul(frictionImpulse, b.inverseMass));
}
}
// Position correction
const double CORRECTION_PERCENT = 0.8;
vec2 correction = vec2_mul(
normal,
(penetration / totalInverseMass) * CORRECTION_PERCENT
);
if (!a.isStatic) {
a.position = vec2_sub(a.position,
vec2_mul(correction, a.inverseMass));
}
if (!b.isStatic) {
b.position = vec2_add(b.position,
vec2_mul(correction, b.inverseMass));
}
// Update AABB positions
if (!a.isStatic) {
vec2 halfSize = vec2_mul(
vec2_sub(a.shape.aabb.max, a.shape.aabb.min),
0.5
);
a.shape.aabb.min = vec2_sub(a.position, halfSize);
a.shape.aabb.max = vec2_add(a.position, halfSize);
}
if (!b.isStatic) {
vec2 halfSize = vec2_mul(
vec2_sub(b.shape.aabb.max, b.shape.aabb.min),
0.5
);
b.shape.aabb.min = vec2_sub(b.position, halfSize);
b.shape.aabb.max = vec2_add(b.position, halfSize);
}
}
}
The AABB collision resolution process:
-
Contact Point Determination:
- Calculate box centers and half-sizes
- Find overlap on each axis
- Choose collision normal based on minimum overlap
-
Collision Response:
- Calculate relative velocity
- Apply restitution based on velocity magnitude
- Calculate and apply impulse
- Handle friction using tangential impulse
-
Advanced Features:
- Velocity threshold for static friction
- Variable restitution based on impact velocity
- Proper handling of static objects
-
Position Updates:
- Correct positions to prevent sinking
- Update AABB boundaries
- Maintain object separation
10. Circle-AABB Collision Resolution
Understanding Circle-AABB Collisions
Circle-AABB collisions are more complex because:
- They involve different shape types
- Contact points need special calculation
- Normal calculation varies based on circle position
- Edge cases need careful handling
Implementation
void resolveCircleAABBCollision(Body &circleBody, Body &aabbBody,
bool circleIsA) {
// Validate shape types
if (circleBody.shapeType != Body::SHAPE_CIRCLE ||
aabbBody.shapeType != Body::SHAPE_AABB) {
return;
}
// 1. Find closest point on AABB to circle center
vec2 closestPoint = {
max(aabbBody.shape.aabb.min.x,
min(circleBody.position.x, aabbBody.shape.aabb.max.x)),
max(aabbBody.shape.aabb.min.y,
min(circleBody.position.y, aabbBody.shape.aabb.max.y))
};
// 2. Calculate distance vector from closest point to circle center
vec2 distance = vec2_sub(circleBody.position, closestPoint);
double distanceMag = vec2_length(distance);
// Skip if no collision
if (distanceMag > circleBody.shape.circle.radius) {
return;
}
// 3. Calculate normal vector
vec2 normal;
const double EPSILON = 0.0001;
if (distanceMag < EPSILON) {
// Circle center is inside AABB, use vector from AABB center to circle
vec2 aabbCenter = {
(aabbBody.shape.aabb.min.x + aabbBody.shape.aabb.max.x) * 0.5,
(aabbBody.shape.aabb.min.y + aabbBody.shape.aabb.max.y) * 0.5
};
distance = vec2_sub(circleBody.position, aabbCenter);
distanceMag = vec2_length(distance);
if (distanceMag < EPSILON) {
// Objects are at same position, use arbitrary normal
normal = {0.0, 1.0};
} else {
normal = vec2_mul(distance, 1.0 / distanceMag);
}
} else {
normal = vec2_mul(distance, 1.0 / distanceMag);
}
// 4. Calculate penetration depth
double penetrationDepth = circleBody.shape.circle.radius - distanceMag;
// 5. Calculate relative velocity
vec2 relativeVel = vec2_sub(circleBody.velocity, aabbBody.velocity);
double impactSpeed = vec2_dot(relativeVel, normal);
// Skip if objects are moving apart
const double MIN_VELOCITY = 0.01;
if (impactSpeed > -MIN_VELOCITY) {
// Objects moving apart or too slow - just correct position
const double POSITION_CORRECTION = 0.8;
double totalInverseMass = circleBody.inverseMass + aabbBody.inverseMass;
if (totalInverseMass > 0) {
vec2 correction = vec2_mul(normal,
(penetrationDepth / totalInverseMass) * POSITION_CORRECTION);
if (!circleBody.isStatic) {
circleBody.position = vec2_add(circleBody.position,
vec2_mul(correction, circleBody.inverseMass));
}
if (!aabbBody.isStatic) {
aabbBody.position = vec2_sub(aabbBody.position,
vec2_mul(correction, aabbBody.inverseMass));
}
}
return;
}
// 6. Calculate collision response
double totalInverseMass = circleBody.inverseMass + aabbBody.inverseMass;
if (totalInverseMass <= 0) return;
// Apply restitution based on impact speed
double restitution = vec2_length(relativeVel) < 2.0 ? 0.0 : e;
double j = -(1.0 + restitution) * impactSpeed / totalInverseMass;
// Apply impulse
vec2 impulse = vec2_mul(normal, j);
if (!circleBody.isStatic) {
circleBody.velocity = vec2_add(circleBody.velocity,
vec2_mul(impulse, circleBody.inverseMass));
}
if (!aabbBody.isStatic) {
aabbBody.velocity = vec2_sub(aabbBody.velocity,
vec2_mul(impulse, aabbBody.inverseMass));
}
// 7. Apply friction
const double FRICTION = 0.4;
vec2 tangent = {-normal.y, normal.x}; // Perpendicular to normal
double tangentSpeed = vec2_dot(relativeVel, tangent);
// Calculate and clamp friction impulse
double frictionImpulse = -tangentSpeed / totalInverseMass;
double maxFriction = FRICTION * fabs(j);
frictionImpulse = max(-maxFriction, min(frictionImpulse, maxFriction));
vec2 frictionVector = vec2_mul(tangent, frictionImpulse);
if (!circleBody.isStatic) {
circleBody.velocity = vec2_add(circleBody.velocity,
vec2_mul(frictionVector, circleBody.inverseMass));
}
if (!aabbBody.isStatic) {
aabbBody.velocity = vec2_sub(aabbBody.velocity,
vec2_mul(frictionVector, aabbBody.inverseMass));
}
// 8. Update positions
// Update circle position
if (!circleBody.isStatic) {
circleBody.shape.circle.center = circleBody.position;
}
// Update AABB position
if (!aabbBody.isStatic) {
vec2 halfSize = vec2_mul(
vec2_sub(aabbBody.shape.aabb.max, aabbBody.shape.aabb.min),
0.5
);
aabbBody.shape.aabb.min = vec2_sub(aabbBody.position, halfSize);
aabbBody.shape.aabb.max = vec2_add(aabbBody.position, halfSize);
}
}
Key aspects of circle-AABB collision resolution:
-
Contact Point Calculation:
- Find closest point on AABB to circle
- Handle special case when circle is inside AABB
- Calculate proper normal vector for collision response
-
Edge Cases:
- Circle center inside AABB
- Objects at exact same position
- Very small penetration depths
-
Physics Response:
- Variable restitution based on impact speed
- Friction calculation using tangential impulse
- Position correction to prevent sinking
11. Complete Collision System Integration
Collision Resolution Manager
void resolveCollisions(vector<BroadPhaseCollisionPair> &collisions,
vector<Body> &bodies) {
for (const auto &pair : collisions) {
Body &bodyA = bodies[pair.bodyA];
Body &bodyB = bodies[pair.bodyB];
// Choose appropriate resolution method based on shape types
if (bodyA.shapeType == Body::SHAPE_CIRCLE &&
bodyB.shapeType == Body::SHAPE_CIRCLE) {
resolveCircleCircleCollision(bodyA, bodyB);
}
else if (bodyA.shapeType == Body::SHAPE_AABB &&
bodyB.shapeType == Body::SHAPE_AABB) {
resolveAABBCollision(bodyA, bodyB);
}
else {
// Handle circle-AABB collisions
if (bodyA.shapeType == Body::SHAPE_CIRCLE) {
resolveCircleAABBCollision(bodyA, bodyB, true);
} else {
resolveCircleAABBCollision(bodyB, bodyA, false);
}
}
}
}
Complete Physics Update Cycle
Here’s how all the systems work together:
void stepWorld(World &world, double dt) {
if (world.isPaused)
return;
// 1. Limit time step to prevent instability
dt = min(dt, world.config.maxDeltaTime);
// 2. Apply forces and integrate motion
for (auto &body : world.bodies) {
if (body.isActive) {
// Apply gravity
applyGravity(body);
// Integrate forces
integrateLinearMotion(body, dt);
}
}
// 3. Update spatial partitioning
updateBroadPhase(world.grid, world.bodies);
// 4. Get potential collision pairs
auto potentialPairs = getPotentialCollisionPairs(world.grid);
// 5. Perform narrow phase collision detection
auto actualCollisions = getActualCollisions(potentialPairs, world.bodies);
// 6. Resolve collisions
resolveCollisions(actualCollisions, world.bodies);
}
12. Performance Optimization Tips
-
Spatial Partitioning Tuning:
// Adjust cell size based on average object size double optimalCellSize = averageObjectSize * 2.0; initGrid(world.grid, worldBound, optimalCellSize);
-
Avoiding Square Roots:
// Use squared distances when possible double distanceSquared = vec2_length_squared(diff); double radiusSquared = radius * radius; if (distanceSquared <= radiusSquared) { // Collision detected }
-
Reuse Vectors:
// Pre-allocate vectors to avoid reallocations world.queryResultIndices.reserve(100); grid.cells[cellIndex].bodyIndex.reserve(10);
-
Early Exit Conditions:
// Skip static-static collisions if (bodyA.isStatic && bodyB.isStatic) return; // Skip if moving apart if (relativeVelocity > 0) return;
13. Example Usage
Basic Setup and Initialization
int main() {
// Initialize the physics world
World world;
WorldConfig config;
// Configure world parameters
config.gravity = {0.0, -9.81}; // Standard Earth gravity
config.maxDeltaTime = 1.0/60.0; // 60 FPS simulation
config.bounds = {
{-50.0, -50.0}, // min bounds
{50.0, 50.0} // max bounds
};
initWorld(world, config);
// Example: Create ground
vec2 groundPos = {0.0, -45.0};
vec2 groundSize = {100.0, 10.0};
int groundId = addBox(world, groundPos, groundSize, 0.0); // mass = 0 makes it static
// Example: Add some dynamic bodies
int circle1 = addCircle(world, {0.0, 10.0}, 1.0, 1.0);
int box1 = addBox(world, {-5.0, 15.0}, {2.0, 2.0}, 1.0);
// Main simulation loop
double timeStep = 1.0/60.0;
while (/* your game loop condition */) {
stepWorld(world, timeStep);
// Render or process results
}
}
Common Scenarios
1. Creating a Stack of Boxes
void createBoxStack(World& world, int numBoxes) {
const double boxSize = 2.0;
const double boxMass = 1.0;
const double startX = 0.0;
const double startY = -40.0; // Above ground level
for (int i = 0; i < numBoxes; i++) {
vec2 position = {
startX,
startY + (i * boxSize * 1.1) // Small gap between boxes
};
vec2 size = {boxSize, boxSize};
addBox(world, position, size, boxMass);
}
}
2. Creating a Pendulum
void createPendulum(World& world) {
// Create anchor point (static box)
vec2 anchorPos = {0.0, 10.0};
int anchor = addBox(world, anchorPos, {1.0, 1.0}, 0.0); // Static
// Create pendulum bob (dynamic circle)
vec2 bobPos = {0.0, 0.0};
int bob = addCircle(world, bobPos, 1.0, 1.0);
// Note: In a real implementation, you'd need to add constraint/joint
// handling to connect the anchor and bob
}
3. Creating a Newton’s Cradle
void createNewtonsCradle(World& world, int numBalls) {
const double radius = 1.0;
const double mass = 1.0;
const double spacing = radius * 2.1;
const double startX = -(numBalls - 1) * spacing / 2;
const double height = 0.0;
for (int i = 0; i < numBalls; i++) {
vec2 position = {
startX + (i * spacing),
height
};
int ball = addCircle(world, position, radius, mass);
// Set initial velocity for first ball
if (i == 0) {
world.bodies[ball].velocity = {5.0, 0.0};
}
}
}
14. Testing and Debugging
Test Cases
struct PhysicsTest {
string name;
function<void(World&)> setup;
function<bool(const World&)> verify;
};
// Example test case: Conservation of momentum
PhysicsTest testConservationOfMomentum() {
return {
"Conservation of Momentum Test",
// Setup function
[](World& world) {
// Create two colliding bodies
int body1 = addCircle(world, {-5.0, 0.0}, 1.0, 1.0);
int body2 = addCircle(world, {5.0, 0.0}, 1.0, 1.0);
// Set initial velocities
world.bodies[body1].velocity = {1.0, 0.0};
world.bodies[body2].velocity = {-1.0, 0.0};
},
// Verification function
[](const World& world) {
vec2 totalMomentum = {0.0, 0.0};
for (const auto& body : world.bodies) {
vec2 momentum = vec2_mul(body.velocity, body.mass);
totalMomentum = vec2_add(totalMomentum, momentum);
}
double momentumMagnitude = vec2_length(totalMomentum);
return momentumMagnitude < 0.01; // Allow for small numerical errors
}
};
}
// Example test case: Energy conservation
PhysicsTest testEnergyConservation() {
return {
"Energy Conservation Test",
[](World& world) {
// Create a bouncing ball
int ball = addCircle(world, {0.0, 10.0}, 1.0, 1.0);
// Create ground
int ground = addBox(world, {0.0, -10.0}, {20.0, 1.0}, 0.0);
},
[](const World& world) {
double totalEnergy = 0.0;
for (const auto& body : world.bodies) {
if (body.isStatic) continue;
// Kinetic energy
double velocity_squared = vec2_length_squared(body.velocity);
double kinetic = 0.5 * body.mass * velocity_squared;
// Potential energy
double height = body.position.y + 10.0; // Relative to ground
double potential = body.mass * 9.81 * height;
totalEnergy += kinetic + potential;
}
static double initialEnergy = -1;
if (initialEnergy < 0) initialEnergy = totalEnergy;
// Allow for some energy loss due to restitution
return totalEnergy <= initialEnergy;
}
};
}
Debugging Helpers
struct PhysicsDebugInfo {
vector<string> messages;
vector<vec2> points;
vector<pair<vec2, vec2>> lines;
vector<Circle> circles;
vector<AABB> boxes;
};
class PhysicsDebugger {
public:
static void drawCollisionNormal(PhysicsDebugInfo& debug,
const vec2& point,
const vec2& normal) {
debug.points.push_back(point);
debug.lines.push_back({
point,
vec2_add(point, vec2_mul(normal, 2.0))
});
}
static void drawBody(PhysicsDebugInfo& debug, const Body& body) {
if (body.shapeType == Body::SHAPE_CIRCLE) {
debug.circles.push_back(body.shape.circle);
} else {
debug.boxes.push_back(body.shape.aabb);
}
}
static void logCollision(PhysicsDebugInfo& debug,
const Body& a,
const Body& b,
const vec2& point) {
string msg = "Collision between bodies at (" +
to_string(point.x) + ", " +
to_string(point.y) + ")";
debug.messages.push_back(msg);
}
};
// Usage in collision resolution:
void resolveCollisions(vector<BroadPhaseCollisionPair>& collisions,
vector<Body>& bodies,
PhysicsDebugInfo* debug = nullptr) {
for (const auto& pair : collisions) {
Body& bodyA = bodies[pair.bodyA];
Body& bodyB = bodies[pair.bodyB];
if (debug) {
PhysicsDebugger::drawBody(*debug, bodyA);
PhysicsDebugger::drawBody(*debug, bodyB);
}
// Regular collision resolution code...
if (debug) {
PhysicsDebugger::logCollision(*debug, bodyA, bodyB,
bodyA.position);
}
}
}
15. Common Problems and Solutions
1. Tunneling Problems
When objects move too fast and pass through each other:
// Solution: Implement Continuous Collision Detection (CCD)
bool checkContinuousCollision(const Body& a, const Body& b, double dt) {
// Calculate swept volumes
vec2 aMove = vec2_mul(a.velocity, dt);
vec2 bMove = vec2_mul(b.velocity, dt);
// Expand AABBs to include motion path
AABB aSwept = getShapeBoundingBox(a);
AABB bSwept = getShapeBoundingBox(b);
// Expand bounds based on movement
if (aMove.x > 0) aSwept.max.x += aMove.x;
else aSwept.min.x += aMove.x;
if (aMove.y > 0) aSwept.max.y += aMove.y;
else aSwept.min.y += aMove.y;
// Similar for b...
// Check if swept volumes intersect
return aabbAabbCollision(aSwept, bSwept);
}
2. Stacking Stability
Objects in stacks may jitter or collapse:
// Solution: Implement position-based dynamics correction
void stabilizeStack(Body& body, const vector<Body>& contacts) {
if (contacts.size() < 2) return;
// Calculate average contact normal
vec2 avgNormal = {0, 0};
for (const auto& contact : contacts) {
vec2 normal = vec2_sub(contact.position, body.position);
vec2_normalize(normal);
avgNormal = vec2_add(avgNormal, normal);
}
avgNormal = vec2_mul(avgNormal, 1.0/contacts.size());
// If mostly vertical stack, dampen horizontal velocity
if (fabs(avgNormal.y) > 0.8) {
body.velocity.x *= 0.8;
}
}
3. Energy Gain
Objects gaining energy due to numerical errors:
// Solution: Implement energy monitoring and correction
void monitorAndCorrectEnergy(Body& body, double& lastEnergy) {
double currentEnergy = 0.5 * body.mass *
vec2_length_squared(body.velocity);
if (currentEnergy > lastEnergy * 1.01) { // 1% tolerance
// Scale back velocity to match previous energy
double scale = sqrt(lastEnergy / currentEnergy);
body.velocity = vec2_mul(body.velocity, scale);
currentEnergy = lastEnergy;
}
lastEnergy = currentEnergy;
}
Key Takeaways
-
Core Principles
- Always separate broad and narrow phase collision detection
- Use spatial partitioning for performance optimization
- Implement proper integration methods for stability
- Handle edge cases carefully in collision resolution
- Consider numerical precision in calculations
-
Performance Considerations
- Optimize hot paths (collision detection and resolution)
- Use appropriate data structures (grid-based spatial partitioning)
- Implement early-out conditions where possible
- Avoid unnecessary square root calculations
- Pre-allocate memory for frequently used containers
-
Stability Tips
- Use consistent time steps
- Implement position correction
- Handle resting contacts properly
- Monitor and correct energy conservation
- Use appropriate coefficients for restitution and friction
Common Pitfalls to Avoid
-
Mathematical Issues
- Dividing by zero in normalizations
- Accumulated floating-point errors
- Missing edge cases in collision detection
- Incorrect handling of static objects
-
Performance Issues
- Checking every object against every other object
- Excessive memory allocations in tight loops
- Not using spatial partitioning
- Unnecessary precision in calculations
-
Stability Issues
- Too large time steps
- Insufficient position correction
- Improper handling of stacking
- Missing rest conditions
Happy physics programming!