Collision Chaining Part 2

In my last article, I talked about collision chaining as a collision system solution that is able to handle collision resolutions that cause additional collisions. In this article, I will explain some of the problems that collision chaining introduces and how I fixed them.

As I mentioned at the end of part 1, the first problem to address is that the chained collision should use the combined mass of all the entities already involved in the collision. If the player pushes a box to the right so that it collides with another box, for example, the chained collision of the first box to the second should treat the first box as having the combined mass of the player and the first box. To implement this, I added an additionalMass parameter to the collision resolution function. The first call to the function from the update loop passes in zero — no additional mass. When chaining collisions, we add that mass to entity A, then pass the combined mass to the recursive call. That way, as the collision is propagated down the chain, the mass increases.

The other parameter we need to add is a boolean value, forceStatic . This flag will be set to YES if a member of the collision chain is a static object. If the player tries to push a box into a static wall, the static flag will be set to true and the box will be treated as static in that chain. Once a single static entity has appeared in the chain, all further collisions will have that flag set to YES . It is important to make sure that only one static entity can be a part of a chain at a time. When we check for new collisions caused by the resolution, if the static flag is set, we only want to check dynamic objects. Otherwise, we will run into a problem with an infinite loop because two static entities are squishing another object. This is a special case we might want to handle later — perhaps if a moving platform smashes the player into the ceiling, for example, the player should die.

Here’s the updated section of the resolveCollisionsBetween method of the CollisionSystem .

double massA = (physicsA != nil ? [physicsA mass] : 0) + additionalMass; double massB = physicsB != nil ? [physicsB mass] : 0; double ratioA = massB / (massA + massB); double ratioB = ratioA - 1; // -1 * (1 - ratioA) CGPoint deltaA = [self scale:resolution by:ratioA]; CGPoint deltaB = [self scale:resolution by:ratioB]; // Resolve the collision [self updateTransform:transformA andCollider:colliderA andPhysics:physicsA with:deltaA]; [self updateTransform:transformB andCollider:colliderB andPhysics:physicsB with:deltaB]; // Collision chaining if(forceStatic || [colliderB type] == LGColliderTypeStatic) { NSArray *entities = [self entitiesThatCollideWith:a onlyDynamic:YES]; for(int i = 0; i < [entities count]; i++) { LGEntity *c = [entities objectAtIndex:i]; [self resolveCollisionsBetween:c and:a withAdditionalMass:0 forceStatic:YES]; } } else { BOOL chained = NO; NSArray *entities = [self entitiesThatCollideWith:a excluding:b]; for(int i = 0; i < [entities count]; i++) { LGEntity *c = [entities objectAtIndex:i]; CGPoint resolutionA = [self resolveCollisionsBetween:a and:c withAdditionalMass:massB forceStatic:NO]; if(!CGPointEqualToPoint(resolutionA, CGPointZero)) { // Resolution caused another collision -- move entity B back [transformB addToPosition:resolutionA]; deltaB = [self translate:deltaB by:resolutionA]; chained = YES; } } if(!chained) { NSArray *entitiesB = [self entitiesThatCollideWith:b excluding:a]; for(int i = 0; i < [entitiesB count]; i++) { LGEntity *c = [entities objectAtIndex:i]; CGPoint resolutionB = [self resolveCollisionsBetween:b and:c withAdditionalMass:massA forceStatic:NO]; if(!CGPointEqualToPoint(resolutionB, CGPointZero)) { // Resolution caused another collision -- move entity A back [transformA addToPosition:resolutionB]; deltaA = [self translate:deltaA by:resolutionB]; } } } } 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 58 59 double massA = ( physicsA != nil ? [ physicsA mass ] : 0 ) + additionalMass ; double massB = physicsB != nil ? [ physicsB mass ] : 0 ; double ratioA = massB / ( massA + massB ) ; double ratioB = ratioA - 1 ; // -1 * (1 - ratioA) CGPoint deltaA = [ self scale :resolution by :ratioA ] ; CGPoint deltaB = [ self scale :resolution by :ratioB ] ; // Resolve the collision [ self updateTransform :transformA andCollider :colliderA andPhysics :physicsA with :deltaA ] ; [ self updateTransform :transformB andCollider :colliderB andPhysics :physicsB with :deltaB ] ; // Collision chaining if ( forceStatic || [ colliderB type ] == LGColliderTypeStatic ) { NSArray *entities = [ self entitiesThatCollideWith :a onlyDynamic :YES ] ; for ( int i = 0 ; i < [ entities count ] ; i ++ ) { LGEntity *c = [ entities objectAtIndex :i ] ; [ self resolveCollisionsBetween :c and :a withAdditionalMass : 0 forceStatic :YES ] ; } } else { BOOL chained = NO ; NSArray *entities = [ self entitiesThatCollideWith :a excluding :b ] ; for ( int i = 0 ; i < [ entities count ] ; i ++ ) { LGEntity *c = [ entities objectAtIndex :i ] ; CGPoint resolutionA = [ self resolveCollisionsBetween :a and :c withAdditionalMass :massB forceStatic :NO ] ; if ( ! CGPointEqualToPoint ( resolutionA , CGPointZero ) ) { // Resolution caused another collision -- move entity B back [ transformB addToPosition :resolutionA ] ; deltaB = [ self translate :deltaB by :resolutionA ] ; chained = YES ; } } if ( ! chained ) { NSArray *entitiesB = [ self entitiesThatCollideWith :b excluding :a ] ; for ( int i = 0 ; i < [ entitiesB count ] ; i ++ ) { LGEntity *c = [ entities objectAtIndex :i ] ; CGPoint resolutionB = [ self resolveCollisionsBetween :b and :c withAdditionalMass :massA forceStatic :NO ] ; if ( ! CGPointEqualToPoint ( resolutionB , CGPointZero ) ) { // Resolution caused another collision -- move entity A back [ transformA addToPosition :resolutionB ] ; deltaA = [ self translate :deltaA by :resolutionB ] ; } } } }

Note that I’m using the prefix LG on my engine classes now. Also note that I only try to chain entity B if entity A didn’t chain. If the first entity chained, the second one shouldn’t need to, so the extra calculations are unnecessary.

With these two additional parameters, the collision system works… mostly. There is one more problem that causes some glitches, and that is that the resolution method doesn’t currently differentiate between collisions that were caused by the resolution and collisions that were there as a result of motion before the collision system got a turn to update. This is a problem because the static collision from the ground an entity will initiate a chain that treats the first object as static. It doesn’t completely break everything, but it does cause mass to be completely ignored.

I came up with three possible solutions to this problem. First, I could implement a queue that processed collisions in the order in which they were detected. The downside is that the collisions would no longer be chained, because this method would not allow recursion. Second, I could keep track of every pair of entities that was colliding before the chain started, and make sure the chain didn’t include any of those pairs. This would be an expensive way to go, both for memory and CPU. The third possibility was to limit chaining to a single axis. Unfortunately, this precludes non-rectangle colliders. Because I’ve already removed non-rectangle colliders for now, the third option was the best.

To implement it, I added yet another parameter to the collision resolution method: chainingAxis . If the chaining axis is set to the x or y axis, the resolution vector’s opposite axis will be forced to zero. In the figure above, the chained collision from the first circle to the second circle would be set to only chain on the y-axis, because the initial collision resolved along the y-axis. The x component of the resolution vector between the circles would be forced to zero, so no chaining would be caused, leaving the collision on the x-axis to be resolved by the main update loop, where the masses will be used correctly to distribute the resolution vector.

I used an enum to create values for the x-axis, the y-axis, and a third value that allows collisions on any axis. A chained collision must always have a collision axis, so I set the chaining axis to the axis of the resolution vector. Immediately after calculating the resolution vector, I check the chaining axis and force the opposite axis to zero. Finally, just before the recursive call, I determine the resolution vector’s axis and pass that in as the chaining axis.

With that, the collision system appears to function as designed! Thank you for reading this article. I’m curious to know if any of you have come up with a better method to avoid chaining existing collisions — or do you use a different method altogether for collisions that cause other collisions?

I’ve already implemented a simple input system, camera system, and tile system, and will describe them in the next few articles. When I’ve finally reached that point, I’ll be ready to open source the project for anyone who would care to take a look at the code in its entirety. I’ll be curious to see how other developers use the engine and ECS framework.