Circle Collisions, and Refactoring the Collision System

While implementing the circle colliders, I decided to do some refactoring of the collision system. Most notably, I’ve added two vectors, resolution and impulse, to keep track of the minimum displacement needed to resolve the collision and the impulse that needs to be applied to the colliding entities. I will also no longer overcorrect the collision to avoid getting stuck in the cracks, as I have decided to split out tile collisions as a separate system from the entity-entity collision system.

The collision algorithm is now as follows.

Loop through each pair of collidable entities. Use a basic bounding box test to eliminate pairs that are clearly not colliding. Use a type-specific test. If a collision is detected, calculate the resolution and impulse vectors.

If no collision is detected, go to the next pair. Adjust entity positions and velocities based on mass, modeling static colliders as having infinite mass.

So far, there is one type-specific test — the rectangle-to-rectgangle collision. In this article, we will implement circle-to-circle collision types as well.

The Resolution and Impulse Vectors

I’ve introduced two new vectors to the collision equation. First, resolution. The resolution vector is the shortest path that entity A has to move to resolve the collision. For rectangle collisions, this is equal to MIN(overlapX, overlapY) in the direction pointing from entity B to entity A.

The second vector is impulse. Impulse is the change in an entity’s momentum, where momentum is mass times velocity. In this instance, the impulse vector will be how much the velocity of entity A would change if all of the kinetic energy of the two colliding bodies were transferred to it. The direction of the impulse vector will be identical to the direction of the resolution vector.

Once the impulse and resolution vectors have been calculated for a collision, we have to decide which entity is moved. Rather than randomly choose one entity, or only moving back the one that caused the collision, now we will split these vectors up based on the entity’s mass. Recall that mass is an object’s resistance to a change in motion — so the less massive entity in the collision will receive more of the resolution and impulse vectors. If the two entities have the same mass, then each of them will receive half. Note that the portion of the resolution vector that is applied to entity B is negated so that it moves in the opposite direction.

Finally, if entity B cannot move or doesn’t have physical properties like mass, then simply apply the entire vector to A as if B had infinite mass.

// Adjust the transforms if(physicsA != nil && physicsB != nil && canMoveB) { double aRatio = [physicsB mass] / ([physicsA mass] + [physicsB mass]); double bRatio = [physicsA mass] / ([physicsA mass] + [physicsB mass]) * -1; colliderPositionA = [self translate:colliderPositionA by:[self scale:resolution by:aRatio]]; colliderPositionB = [self translate:colliderPositionB by:[self scale:resolution by:bRatio]]; [transformA setPosition:[self untranslate:colliderPositionA by:[colliderA offset]]]; [transformB setPosition:[self untranslate:colliderPositionB by:[colliderB offset]]]; } else { colliderPositionA = [self translate:colliderPositionA by:resolution]; [transformA setPosition:[self untranslate:colliderPositionA by:[colliderA offset]]]; } // Adjust the velocities if(!CGPointEqualToPoint(impulse, CGPointZero)) { if(physicsA != nil && physicsB != nil && canMoveB) { double elasticity = MAX([physicsA elasticity], [physicsB elasticity]); double newVelocityAx = (elasticity * [physicsB mass] * impulse.x + [physicsA mass] * [physicsA velocity].x + [physicsB mass] * [physicsB velocity].x) / ([physicsA mass] + [physicsB mass]); double newVelocityBx = (elasticity * [physicsA mass] * -impulse.x + [physicsA mass] * [physicsA velocity].x + [physicsB mass] * [physicsB velocity].x) / ([physicsA mass] + [physicsB mass]); double newVelocityAy = (elasticity * [physicsB mass] * impulse.y + [physicsA mass] * [physicsA velocity].y + [physicsB mass] * [physicsB velocity].y) / ([physicsA mass] + [physicsB mass]); double newVelocityBy = (elasticity * [physicsA mass] * -impulse.y + [physicsA mass] * [physicsA velocity].y + [physicsB mass] * [physicsB velocity].y) / ([physicsA mass] + [physicsB mass]); [physicsA setVelocity:CGPointMake(newVelocityAx, newVelocityAy)]; [physicsB setVelocity:CGPointMake(newVelocityBx, newVelocityBy)]; } else { // Model entity B as if it has infinite mass; apply the entire impulse to entity A double elasticity = physicsB != nil ? MAX([physicsA elasticity], [physicsB elasticity]) : [physicsA elasticity]; [physicsA setVelocity:CGPointMake(elasticity * impulse.x, elasticity * impulse.y)]; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 // Adjust the transforms if ( physicsA != nil && physicsB != nil && canMoveB ) { double aRatio = [ physicsB mass ] / ( [ physicsA mass ] + [ physicsB mass ] ) ; double bRatio = [ physicsA mass ] / ( [ physicsA mass ] + [ physicsB mass ] ) * - 1 ; colliderPositionA = [ self translate :colliderPositionA by : [ self scale :resolution by :aRatio ] ] ; colliderPositionB = [ self translate :colliderPositionB by : [ self scale :resolution by :bRatio ] ] ; [ transformA setPosition : [ self untranslate :colliderPositionA by : [ colliderA offset ] ] ] ; [ transformB setPosition : [ self untranslate :colliderPositionB by : [ colliderB offset ] ] ] ; } else { colliderPositionA = [ self translate :colliderPositionA by :resolution ] ; [ transformA setPosition : [ self untranslate :colliderPositionA by : [ colliderA offset ] ] ] ; } // Adjust the velocities if ( ! CGPointEqualToPoint ( impulse , CGPointZero ) ) { if ( physicsA != nil && physicsB != nil && canMoveB ) { double elasticity = MAX ( [ physicsA elasticity ] , [ physicsB elasticity ] ) ; double newVelocityAx = ( elasticity * [ physicsB mass ] * impulse . x + [ physicsA mass ] * [ physicsA velocity ] . x + [ physicsB mass ] * [ physicsB velocity ] . x ) / ( [ physicsA mass ] + [ physicsB mass ] ) ; double newVelocityBx = ( elasticity * [ physicsA mass ] * - impulse . x + [ physicsA mass ] * [ physicsA velocity ] . x + [ physicsB mass ] * [ physicsB velocity ] . x ) / ( [ physicsA mass ] + [ physicsB mass ] ) ; double newVelocityAy = ( elasticity * [ physicsB mass ] * impulse . y + [ physicsA mass ] * [ physicsA velocity ] . y + [ physicsB mass ] * [ physicsB velocity ] . y ) / ( [ physicsA mass ] + [ physicsB mass ] ) ; double newVelocityBy = ( elasticity * [ physicsA mass ] * - impulse . y + [ physicsA mass ] * [ physicsA velocity ] . y + [ physicsB mass ] * [ physicsB velocity ] . y ) / ( [ physicsA mass ] + [ physicsB mass ] ) ; [ physicsA setVelocity :CGPointMake ( newVelocityAx , newVelocityAy ) ] ; [ physicsB setVelocity :CGPointMake ( newVelocityBx , newVelocityBy ) ] ; } else { // Model entity B as if it has infinite mass; apply the entire impulse to entity A double elasticity = physicsB != nil ? MAX ( [ physicsA elasticity ] , [ physicsB elasticity ] ) : [ physicsA elasticity ] ; [ physicsA setVelocity :CGPointMake ( elasticity * impulse . x , elasticity * impulse . y ) ] ; } }

Circle Collisions

Now we can create a new kind of Collider component, the CircleCollider . This type of collider will subclass the basic Collider and add a radius component. The circle collider will be used by setting the origin of the circle to be the entity’s position, plus the circle’s offset, plus the circle’s radius. In other words, the circle will be positioned in the center of the bounding box with sides equal to its diameter. Incidentally, for the quick collision detection in step 2, circle colliders will use its bounding box, calculated as a square the size of its diameter.

circleOrigin.x = [transform position].x + [circle offset].x + [circle radius]; circleOrigin.y = [transform position].y + [circle offset].y + [circle radius]; 1 2 circleOrigin . x = [ transform position ] . x + [ circle offset ] . x + [ circle radius ] ; circleOrigin . y = [ transform position ] . y + [ circle offset ] . y + [ circle radius ] ;

Now we need to add a section to the type-specific test. We will start with a circle-to-circle collision test. In order to detect a collision, we look at the shortest path between the two circles’ origins (labelled d in the figure). If a collision has occurred, d will be less than the sum of the radii, r 1 + r 2 , and in fact, the difference will be exactly the magnitude of the resolution vector.

As we did with rectangle collisions, we will calculate the resolution vector in the direction that entity A needs to move to resolve the collision. Part of the resolution vector will be applied to entity A, and part of it will be applied to B (except opposite), dividing up the movement based on mass. Finally, the impulse vector will be calculated based on the incident velocities and the elasticities of the entities, and again divided up based on mass.

So, first things first, here’s how we detect a collision between circles. First, we calculate the vector dist , then we get the magnitude of that vector ( d in the figure). The square root operation is slow, and since the magnitude is sqrt(dist.x * dist.x + dist.y * dist.y) , we can leave the magnitude squared and check that d2 < radii2 instead of d < radii to speed up calculations.

LGCircleCollider *circleA = (LGCircleCollider *)colliderA; LGCircleCollider *circleB = (LGCircleCollider *)colliderB; CGPoint dist = CGPointZero; dist.x = colliderPositionA.x + [circleA radius] - colliderPositionB.x - [circleB radius]; dist.y = colliderPositionA.y + [circleA radius] - colliderPositionB.y - [circleB radius]; double radii = [circleA radius] + [circleB radius]; double squareDist = [self squareMagnitude:dist]; double squareRadii = radii * radii; if(squareDist < squareRadii) { // Collision detected! } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 LGCircleCollider *circleA = ( LGCircleCollider * ) colliderA ; LGCircleCollider *circleB = ( LGCircleCollider * ) colliderB ; CGPoint dist = CGPointZero ; dist . x = colliderPositionA . x + [ circleA radius ] - colliderPositionB . x - [ circleB radius ] ; dist . y = colliderPositionA . y + [ circleA radius ] - colliderPositionB . y - [ circleB radius ] ; double radii = [ circleA radius ] + [ circleB radius ] ; double squareDist = [ self squareMagnitude :dist ] ; double squareRadii = radii * radii ; if ( squareDist < squareRadii ) { // Collision detected! }

Now that we've detected the collision, we need to determine the resolution vector, which is has a magnitude of |dist| - radii and a direction pointing from circle B to circle A. The impulse magnitude can be determined using physics, and the direction will be the same direction as the resolution vector. We will calculate the impulse magnitude, then multiply it with a normalized vector pointing in the direction of the resolution vector.

double magnitude = sqrt(squareDist); resolution = dist; resolution.x *= radii / magnitude - 1; resolution.y *= radii / magnitude - 1; if(physicsA != nil) { double impulseMagnitude; if(physicsB != nil) { double dx = ([physicsB velocity].x - [physicsA velocity].x); double dy = ([physicsB velocity].y - [physicsA velocity].y); impulseMagnitude = sqrt( dx * dx + dy * dy ); } else { double dx = [physicsA velocity].x; double dy = [physicsA velocity].y; impulseMagnitude = sqrt( dx * dx + dy * dy); } impulse = [self normalize:resolution]; impulse.x *= impulseMagnitude; impulse.y *= impulseMagnitude; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 double magnitude = sqrt ( squareDist ) ; resolution = dist ; resolution . x *= radii / magnitude - 1 ; resolution . y *= radii / magnitude - 1 ; if ( physicsA != nil ) { double impulseMagnitude ; if ( physicsB != nil ) { double dx = ( [ physicsB velocity ] . x - [ physicsA velocity ] . x ) ; double dy = ( [ physicsB velocity ] . y - [ physicsA velocity ] . y ) ; impulseMagnitude = sqrt ( dx * dx + dy * dy ) ; } else { double dx = [ physicsA velocity ] . x ; double dy = [ physicsA velocity ] . y ; impulseMagnitude = sqrt ( dx * dx + dy * dy ) ; } impulse = [ self normalize :resolution ] ; impulse . x *= impulseMagnitude ; impulse . y *= impulseMagnitude ; }

When we test it out, we get exactly what we would expect! Here's what happens when we throw two circles at each other with elasticity.

Something else that's pretty cool is what happens when one of the circles doesn't have a physics component.

The next thing we need to do is implement a type-specific test for when a circle collides with a rectangle. Rectangles usually only collide on one axis, but when a circle hits the corner of a rectangle, it will be colliding on two axes just like a circle. On the other hand, when a circle hits the face of a rectangle, it behaves like a single-axis collision instead. We will explore the implementation of circle-rectangle collisions in the next post.