You will need SKUtils by Ray Wenderlich. The library just makes life easier when working with SpriteKit
Creating soft body physics or jelly physics is nothing new. I based my approach on the awesome demo posted at Chipmunk2d. Chipmunk2d in case you don’t know is an excellent 2d physics engine, but since we are in SpriteKit land we will go with the default physics engine (Box2d).
The process of creating the illusion of soft body simulation is pretty simple. All you need is a few rigid bodies and some joints to connect them.
Lets go over the steps in a little more detail:
- Create the main circle (a physics body)
- Create the outer circle (also a physics body)
- Create a Spring Joint to connect the outer circle to the main circle
- Repeat Step 2 and 3 a few times to get the general shape of the soft body (you need a minimum of 4 and probably no more than 20)
- Create a Spring Joint between each outer circle
Once you have the underlying physics body created then all that is needed is to draw a circle on top of it and have it conform to the position of each outer circle. Since SpriteKit doesn’t allow us low level access to the rendering pipeline, we fake it by using SKShapeNode and creating a path that passes through each outer circle.
Sounds complicated but it isn’t. This is the code for it all:
The header file first:
#import <SpriteKit/SpriteKit.h> @interface MyScene : SKScene @end |
and the implemention
#import "MyScene.h" float m_SphereRadius = 0.25f; float m_Radius = 30.0f; // The mass of the entire sprite (each of the n reference point is 1/n times this value) float m_Mass = 2.0f; float m_Mass_Center = 4.0f; // Circle-configuration only - how many rigid bodies are placed around the circle radius int m_RadiusPoints = 22; // how fast does the body recover from a bounce float m_DampingRatio = 2.0f; // How stiff the soft body physics springs are float m_Stiffness = 9.0F; @implementation MyScene { SKSpriteNode *mainCircle; NSMutableArray *bodies; SKShapeNode *ballLine; CGMutablePathRef pathToDraw; SKColor *ballOutlineColor; SKColor *ballFillColor; float ballGlowWidth; } -(id)initWithSize:(CGSize)size { if (self = [super initWithSize:size]) { /* Setup your scene here */ self.backgroundColor = [SKColor colorWithRed:0.15 green:0.15 blue:0.3 alpha:1.0]; } return self; } - (void)didMoveToView:(SKView *)view { self.physicsWorld.gravity = CGVectorMake(0, -10); self.physicsBody = [SKPhysicsBody bodyWithEdgeLoopFromRect:self.frame]; ballOutlineColor = [SKColor lightGrayColor]; ballFillColor = [SKColor magentaColor]; ballGlowWidth = 3.0f; [self createBall]; } -(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event { /* Called when a touch begins */ for (UITouch *touch in touches) { [mainCircle.physicsBody applyImpulse:CGVectorMake(0, 1000)]; } } -(void)update:(CFTimeInterval)currentTime { /* Called before each frame is rendered */ [self drawBall]; } -(void) drawBall { //remove the old ball from the scene [ballLine removeFromParent]; // draw the new ball ballLine = [SKShapeNode node]; SKSpriteNode *point = [bodies objectAtIndex:0]; // create a path between the outer circles pathToDraw = CGPathCreateMutable(); CGPathMoveToPoint(pathToDraw, NULL, point.position.x, point.position.y); for(int loop = 1; loop < [bodies count]; loop++) { point = [bodies objectAtIndex:loop]; CGPathAddLineToPoint(pathToDraw, NULL, point.position.x, point.position.y); } // close the final gap between the last point and first point point = [bodies objectAtIndex:0]; CGPathAddLineToPoint(pathToDraw, NULL, point.position.x, point.position.y); //draw the line ballLine.path = pathToDraw; [ballLine setStrokeColor:ballOutlineColor]; [ballLine setGlowWidth:ballGlowWidth]; [ballLine setFillColor:ballFillColor]; [self addChild:ballLine]; CGPathRelease(pathToDraw); } -(void) createBall { int numPoints = m_RadiusPoints; float radius = m_Radius; float bigCircle_radius = m_Radius/2; float smallCircle_radius = m_Radius/6; //to hold the outer circles bodies = [[NSMutableArray alloc] initWithCapacity:numPoints]; // create the main circle mainCircle = [SKSpriteNode spriteNodeWithColor:[SKColor clearColor] size:CGSizeMake(10, 10)]; mainCircle.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:bigCircle_radius]; mainCircle.physicsBody.dynamic = YES; mainCircle.physicsBody.affectedByGravity = NO; mainCircle.physicsBody.mass = m_Mass_Center; mainCircle.position = CGPointMake(200 , 280); mainCircle.name = @"player"; [self addChild:mainCircle]; // create the outer circles for(int loop = 0; loop < numPoints; loop++) { // Work out the correct offset to place each outer circle float angle = ((M_PI * 2)/numPoints) * loop; float x_offset = cosf(angle); float y_offset = sinf(angle); x_offset *= radius; y_offset *= radius; SKSpriteNode *point = [SKSpriteNode spriteNodeWithColor:[SKColor clearColor] size:CGSizeMake(10, 10)]; point.physicsBody = [SKPhysicsBody bodyWithCircleOfRadius:smallCircle_radius]; point.physicsBody.dynamic = YES; point.physicsBody.affectedByGravity = YES; point.physicsBody.mass = m_Mass/numPoints; point.position = CGPointAdd(mainCircle.position,CGPointMake(x_offset, y_offset)); point.name = @"smallcircle"; [self addChild:point]; // attach each out circle to the main circle by using a joint [self AttachPoint:mainCircle secondPoint:point]; [bodies addObject:point]; } // create a joint between each outer circile for(int loop = 1; loop < [bodies count]; loop++) { [self AttachPoint:[bodies objectAtIndex:loop] secondPoint:[bodies objectAtIndex:(loop-1)]] ; } // close the outer circle by creating a joint between the last outer circle and the first out circle [self AttachPoint:[bodies objectAtIndex:([bodies count]-1)] secondPoint:[bodies objectAtIndex:1]]; [self drawBall]; } -(void)AttachPoint:(SKSpriteNode *)point1 secondPoint:(SKSpriteNode *)point2 { // create a joint between two bodies SKPhysicsJointSpring *joint = [SKPhysicsJointSpring jointWithBodyA:point1.physicsBody bodyB:point2.physicsBody anchorA:point1.position anchorB:point2.position ]; joint.damping = m_DampingRatio; joint.frequency = m_Stiffness; [self.physicsWorld addJoint:joint]; } @end |