Circle-Rectangle Collisions

The last type of collision we need to implement is the circle-to-rectangle collision. The math behind such a collision is split into two parts based on where the collision is on the rectangle. Let’s consider a rectangle for a moment.

The rectangle divides the space around it into eight regions, as shown in the figure. Regions II, IV, V, and VII are where potential overlap may occur. When the rectangle collides with another rectangle, it is in these regions that the overlap is considered and corrected.

Now, consider a circle about to collide with the rectangle. If we were to treat the circle the same way as a rectangle in this case (that is, by setting resolution to be the smallest overlap in regions II, IV, V, and VII), the circle would collide when it shouldn’t — some of the time.

When it comes down to it, there are only two cases for rectangle-circle collisions. Either the circle is colliding with one of the rectangle’s faces, or it is colliding with one of its corners. When it collides with one of the faces (that is, when the circle approaches from regions II, IV, V, or VII), we can treat it as a rectangle and use the same algorithm for rectangle-rectangle collisions. When it collides with one of the corners (that is, when the circle approaches from regions I, III, VI, or VIII), we will have to take a different approach. In the circle-corner case, we can treat the rectangle’s corner as a circle with radius zero — that is, we find the shortest distance between the center of the circle and the corner of the rectangle and compare with the circle’s radius to determine overlap and a possible resolution vector in the direction between the two colliding entities.

In our collision code, we have defined resolution as the vector that entity A needs to move to resolve the collision and impulse as the change in velocity of entity A if entity A received all of the kinetic energy. In other words, all our vectors point from entity B to entity A. For this new collision type, we will calculate these vectors pointing from the circle to the rectangle. To keep it consistent, we will simply check whether entity A was actually the rectangle, and, if not, we will invert the vectors.

Detecting a Circle-Rectangle Collision

The first thing we need to do is check whether a collision occurred at all, and, if so, which region the circle is in. All we have to do is check where the center point of the circle collider is and compare it to the rectangle’s coordinates.

if([colliderA isMemberOfClass:[colliderB class]] && [colliderA isMemberOfClass:[LGRectangleCollider class]]) { // Case 1: Rectangle to Rectangle Collision // ... } else if([colliderA isMemberOfClass:[colliderB class]] && [colliderA isMemberOfClass:[LGCircleCollider class]]) { // Case 2: Circle to Circle Collision // ... } else { // Case 3: Rectangle to Circle LGRectangleCollider *rect = nil; LGCircleCollider *circle = nil; CGPoint rectPos, circlePos; BOOL circleIsA = NO; // Determine which collider is the circle and which is the rectangle if([colliderA isMemberOfClass:[LGRectangleCollider class]] && [colliderB isMemberOfClass:[LGCircleCollider class]]) { rect = (LGRectangleCollider *)colliderA; circle = (LGCircleCollider *)colliderB; rectPos = colliderPositionA; circlePos = colliderPositionB; } else if([colliderA isMemberOfClass:[LGCircleCollider class]] && [colliderB isMemberOfClass:[LGRectangleCollider class]]) { circle = (LGCircleCollider *)colliderA; rect = (LGRectangleCollider *)colliderB; circlePos = colliderPositionA; rectPos = colliderPositionB; // Flag -- the rectangle is not entity A, so we will need to invert resolution and impulse vectors circleIsA = YES; } BOOL outsideX = circlePos.x + [circle radius] < rectPos.x || circlePos.x + [circle radius] > rectPos.x + [rect size].width; BOOL outsideY = circlePos.y + [circle radius] < rectPos.y || circlePos.y + [circle radius] > rectPos.y + [rect size].height; if(outsideX && outsideY) { // Treat the rectangle as a single point (a circle with radius 0); find the closest corner // Circle is in region I, III, VI, or VIII } else { // Treat the circle as a rectangle // Circle is in region II, IV, V, or VII } } 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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 if ( [ colliderA isMemberOfClass : [ colliderB class ] ] && [ colliderA isMemberOfClass : [ LGRectangleCollider class ] ] ) { // Case 1: Rectangle to Rectangle Collision // ... } else if ( [ colliderA isMemberOfClass : [ colliderB class ] ] && [ colliderA isMemberOfClass : [ LGCircleCollider class ] ] ) { // Case 2: Circle to Circle Collision // ... } else { // Case 3: Rectangle to Circle LGRectangleCollider *rect = nil ; LGCircleCollider *circle = nil ; CGPoint rectPos , circlePos ; BOOL circleIsA = NO ; // Determine which collider is the circle and which is the rectangle if ( [ colliderA isMemberOfClass : [ LGRectangleCollider class ] ] && [ colliderB isMemberOfClass : [ LGCircleCollider class ] ] ) { rect = ( LGRectangleCollider * ) colliderA ; circle = ( LGCircleCollider * ) colliderB ; rectPos = colliderPositionA ; circlePos = colliderPositionB ; } else if ( [ colliderA isMemberOfClass : [ LGCircleCollider class ] ] && [ colliderB isMemberOfClass : [ LGRectangleCollider class ] ] ) { circle = ( LGCircleCollider * ) colliderA ; rect = ( LGRectangleCollider * ) colliderB ; circlePos = colliderPositionA ; rectPos = colliderPositionB ; // Flag -- the rectangle is not entity A, so we will need to invert resolution and impulse vectors circleIsA = YES ; } BOOL outsideX = circlePos . x + [ circle radius ] < rectPos . x || circlePos . x + [ circle radius ] > rectPos . x + [ rect size ] . width ; BOOL outsideY = circlePos . y + [ circle radius ] < rectPos . y || circlePos . y + [ circle radius ] > rectPos . y + [ rect size ] . height ; if ( outsideX && outsideY ) { // Treat the rectangle as a single point (a circle with radius 0); find the closest corner // Circle is in region I, III, VI, or VIII } else { // Treat the circle as a rectangle // Circle is in region II, IV, V, or VII } }

At this point, we’ve detected which case of collision has occurred. All that’s left is to copy some of the collision algorithm from the other collision types, making modifications as needed (i.e. eliminating the radius of the “second circle,” or using the circle’s diameter as the size of its bounding box). Here is the final collision code.

// Circle is in region I, III, VI, or VIII CGPoint dist = CGPointZero; dist.x = circlePos.x + [circle radius] < rectPos.x ? rectPos.x - circlePos.x - [circle radius] : rectPos.x + [rect size].width - circlePos.x - [circle radius]; dist.y = circlePos.y + [circle radius] < rectPos.y ? rectPos.y - circlePos.y - [circle radius] : rectPos.y + [rect size].height - circlePos.y - [circle radius]; double squareDist = [self squareMagnitude:dist]; if(squareDist < [circle radius] * [circle radius]) { double magnitude = sqrt(squareDist); resolution = dist; resolution.x *= ([circle radius] / magnitude - 1) * (circleIsA ? -1 : 1); resolution.y *= ([circle radius] / magnitude - 1) * (circleIsA ? -1 : 1); if(physicsA != nil) { double impulseMagnitude; if(physicsB != nil) { impulseMagnitude = sqrt( ([physicsB velocity].x - [physicsA velocity].x) * ([physicsB velocity].x - [physicsA velocity].x) + ([physicsB velocity].y - [physicsA velocity].y) * ([physicsB velocity].y - [physicsA velocity].y) ); } else { impulseMagnitude = sqrt( [physicsA velocity].x * [physicsA velocity].x + [physicsA velocity].y * [physicsA velocity].y ); } 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 28 29 30 31 32 33 34 // Circle is in region I, III, VI, or VIII CGPoint dist = CGPointZero ; dist . x = circlePos . x + [ circle radius ] < rectPos . x ? rectPos . x - circlePos . x - [ circle radius ] : rectPos . x + [ rect size ] . width - circlePos . x - [ circle radius ] ; dist . y = circlePos . y + [ circle radius ] < rectPos . y ? rectPos . y - circlePos . y - [ circle radius ] : rectPos . y + [ rect size ] . height - circlePos . y - [ circle radius ] ; double squareDist = [ self squareMagnitude :dist ] ; if ( squareDist < [ circle radius ] * [ circle radius ] ) { double magnitude = sqrt ( squareDist ) ; resolution = dist ; resolution . x *= ( [ circle radius ] / magnitude - 1 ) * ( circleIsA ? - 1 : 1 ) ; resolution . y *= ( [ circle radius ] / magnitude - 1 ) * ( circleIsA ? - 1 : 1 ) ; if ( physicsA != nil ) { double impulseMagnitude ; if ( physicsB != nil ) { impulseMagnitude = sqrt ( ( [ physicsB velocity ] . x - [ physicsA velocity ] . x ) * ( [ physicsB velocity ] . x - [ physicsA velocity ] . x ) + ( [ physicsB velocity ] . y - [ physicsA velocity ] . y ) * ( [ physicsB velocity ] . y - [ physicsA velocity ] . y ) ) ; } else { impulseMagnitude = sqrt ( [ physicsA velocity ] . x * [ physicsA velocity ] . x + [ physicsA velocity ] . y * [ physicsA velocity ] . y ) ; } impulse = [ self normalize :resolution ] ; impulse . x *= impulseMagnitude ; impulse . y *= impulseMagnitude ; } }

// Circle is in region II, IV, V, or VII CGPoint dist = CGPointZero; dist.x = circlePos.x > rectPos.x ? circlePos.x - rectPos.x - [rect size].width : circlePos.x + [circle radius] * 2 - rectPos.x; dist.y = circlePos.y > rectPos.y ? circlePos.y - rectPos.y - [rect size].height : circlePos.y + [circle radius] * 2 - rectPos.y; if(fabs(dist.y) < fabs(dist.x)) { resolution.y += dist.y * (circleIsA ? -1 : 1); if(physicsA != nil) { if(physicsB != nil) { impulse.y = [physicsB velocity].y - [physicsA velocity].y; } else { impulse.y = -[physicsA velocity].y; } } } else { resolution.x += dist.x * (circleIsA ? -1 : 1); if(physicsA != nil) { if(physicsB != nil) { impulse.x = [physicsB velocity].x - [physicsA velocity].x; } else { impulse.x = -[physicsA velocity].x; } } } 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 // Circle is in region II, IV, V, or VII CGPoint dist = CGPointZero ; dist . x = circlePos . x > rectPos . x ? circlePos . x - rectPos . x - [ rect size ] . width : circlePos . x + [ circle radius ] * 2 - rectPos . x ; dist . y = circlePos . y > rectPos . y ? circlePos . y - rectPos . y - [ rect size ] . height : circlePos . y + [ circle radius ] * 2 - rectPos . y ; if ( fabs ( dist . y ) < fabs ( dist . x ) ) { resolution . y += dist . y * ( circleIsA ? - 1 : 1 ) ; if ( physicsA != nil ) { if ( physicsB != nil ) { impulse . y = [ physicsB velocity ] . y - [ physicsA velocity ] . y ; } else { impulse . y = - [ physicsA velocity ] . y ; } } } else { resolution . x += dist . x * ( circleIsA ? - 1 : 1 ) ; if ( physicsA != nil ) { if ( physicsB != nil ) { impulse . x = [ physicsB velocity ] . x - [ physicsA velocity ] . x ; } else { impulse . x = - [ physicsA velocity ] . x ; } } }

When we compile and run it, it works like a charm!

For now, this concludes my posts about the collision system. As I mentioned before, this will be strictly for entity-entity collisions — entity-tile collisions will be handled by the tile system. The reasons for this will be explained in a future post about the tile system, which is the next piece of the engine I intend to implement.