Brief introduction
This can definitely be done, and there are no complex calculations necessary. SpriteKit is perfectly capable of handling this collision. I think you're misunderstanding the physics in play here. There is one simple rule to keep in mind in physics that you are breaking. Speaking in real-life terms here:
No two objects can occupy the same point in space. This goes for parts of objects as well. No parts of any two objects can occupy the same point in space. With this in mind, we can say that no matter how close two objects are, there is always some space between them.
In other words, the amount of space between two objects next to each other must be greater than zero. No matter how infinitely small that space is, it still has to be there.
The Problem
Your underlying problem is that you've left zero space between the blue circles. Let's assume the circles have a radius of 30 and the top circle has a y-position of 400.
Your math for positioning the middle circle is likely topCircle.position.y - (topCircle.height + middleCircle.height) / 2
. Since your circles are all the same radius (30 in our example), you probably simplified that to topCircle.y - CIRCLE_DIAMETER
. If you positioned the circles absolutely, this is probably the calculation you performed in your head.
With either equation, when you plug in the numbers, the y-position of the second ball comes out to 340.
First equation: 400 - (60 + 60) / 2 = 340
Second equation: 400 - 60 = 340
(Note: first equation is more flexible and will allow you to work with any size circles instead of uniform-size)
Herein lies the problem. It should be clear by now that the bottom of the top circle is occupying the same point in space as the top of the top of the middle circle.
Top circle: 400 - 30 = 370
Middle circle: 340 + 30 = 370
Space between: |370 - 370| = 0
The two physics bodies are attempting to occupy the same point in space, and thus have merged into one body, giving you the behavior you're seeing.
The solution
With all that being said, if you simply add a point (or less) of space between the circles, you will get the behavior you desire.
(Note: also make sure to set the restitution of each circle's physics body to 1)
This works with any number of circles. Here's a simple example scene with 1 circle contacting 4 others that exhibits this behavior. I ran this on an iPhone 6:
#import "GameScene.h"
@interface GameScene()
@property (strong, nonatomic) NSArray *nodes;
@end
@implementation GameScene
#pragma mark - View Lifecycle
-(void)didMoveToView:(SKView *)view
{
self.backgroundColor = [SKColor blackColor];
[self createNodes];
[self positionNodes];
[self addNodes];
}
#pragma mark - Setup
- (void)createNodes
{
// You could use SKShapeNode as well
self.nodes = @[[SKSpriteNode spriteNodeWithImageNamed:@"circle"],
[SKSpriteNode spriteNodeWithImageNamed:@"circle"],
[SKSpriteNode spriteNodeWithImageNamed:@"circle"],
[SKSpriteNode spriteNodeWithImageNamed:@"circle"],
[SKSpriteNode spriteNodeWithImageNamed:@"circle"]];
// Use this if you don't have a 30-radius circle image at hand
// self.nodes = @[[SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:CGSizeMake(60, 60)],
// [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:CGSizeMake(60, 60)],
// [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:CGSizeMake(60, 60)],
// [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:CGSizeMake(60, 60)],
// [SKSpriteNode spriteNodeWithColor:[UIColor blueColor] size:CGSizeMake(60, 60)]];
for(SKSpriteNode *node in self.nodes)
{
node.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:30];
node.physicsBody.affectedByGravity = NO;
node.physicsBody.restitution = 1;
}
}
- (void)positionNodes
{
SKSpriteNode *node = self.nodes.firstObject;
node.position = CGPointMake(300, 400);
for(NSInteger i = 1; i < self.nodes.count - 1; i++)
{
SKSpriteNode *prevNode = node;
node = self.nodes[i];
node.position = CGPointMake(300, prevNode.position.y - (prevNode.frame.size.height + node.frame.size.height) / 2 - 1);
}
node = self.nodes.lastObject;
node.position = CGPointMake(300, 100);
// Above created nodes at these positions:
// (300, 400);
// (300, 339);
// (300, 278);
// (300, 217);
// (300, 100);
}
- (void)addNodes
{
for(SKSpriteNode *node in self.nodes)
{
[self addChild:node];
}
}
#pragma mark - Touch Events
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
SKSpriteNode *node = self.nodes.lastObject;
[node.physicsBody applyImpulse:CGVectorMake(0, 20)];
}
@end