How to Create Custom Physics Bodies in SpriteKit - Swift

Swift - Sprite Kit Tutorial



If your sprite kit game works with physics or you are planning to add physics to your game maybe this tutorial will be useful for you so let's talk a little bit of physics bodies in sprite kit.

If you have worked before with physics bodies then you already know that create a physics body with the shape of a square, rectangle and circle is a really easy task, if you want a square body use SKPhysicsBody(rectangleOfSize: CGSize) and if you want a circular body use SKPhysicsBody(circleOfRadius: yoursprite.size.width/2) but what can you do if you want a physics body with the shape of a polygon, star, fruit, jar, spikes or a more complex custom shape?.

The purpose of this tutorial is to show you how to create a physics body with the custom shape that you wish with ease so i'll show you how to create a bottle that can be filled with physics bodies and maybe this project will give you a lot of ideas for your incoming games.

Until Apple lift the NDA this tutorial won't contain screenshots of X-Code 6

Shall We start?

First we need to create a new project in X-Code 6 Beta 5.

  • Product Name: Physics Bottle

  • Language: Swift

  • Game Technology: SpriteKit

  • Device: iPhone

  • Device Orientation: Portrait


Now open GameScene.swift add a Float extension to create a random value and clean the project like the following.




import SpriteKit

extension Float {
    static func range(min: CGFloat, max: CGFloat) -> CGFloat {
        return CGFloat(Float(arc4random()) / 0xFFFFFFFF) * (max - min) + min
    }
}

class GameScene: SKScene {
    override func didMoveToView(view: SKView) {

    }
}

Now download the images for this tutorial here, unzip the file and copy the directory to your project.

In order to create our custom shape we need to use the tool created by @dazchong available in his website, go to http://dazchong.com/spritekit, this tool is a small helper for easier path drawing.



This tool will let us to draw a path and then will return a code that will help us to draw that path into x-code, inside x-code open the directory called images and right click on the image named bottle.png and click the option open in finder.



Now drag and drop the image into the area with title Drop Sprite Image Here.



If you did it right you should see the following.



Take your time and read the Basic Instruction and the Some Rules / Known Issue and feel free to play with this tool before proceed, this will help you to understand how it works.

Let's proceed to create the shape of the bottle, you need to click on each point in the order specified in the image below



This shape will be our physics body but how can we use this shape in Sprite Kit?, as you noticed below the image is the code generated by this tool, believe it or not this code will draw the bottle shape into Sprite Kit.

But wait a second, this code is written in Objective-C and our project is written in Swift so what can we do now?

Copy the code go back to Xcode and paste it inside the didMoveToView function.




override func didMoveToView(view: SKView) {
    SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@"bottle.png"];
        
    CGFloat offsetX = sprite.frame.size.width * sprite.anchorPoint.x;
    CGFloat offsetY = sprite.frame.size.height * sprite.anchorPoint.y;
        
    CGMutablePathRef path = CGPathCreateMutable();
        
    CGPathMoveToPoint(path, NULL, 29 - offsetX, 139 - offsetY);
    CGPathAddLineToPoint(path, NULL, 19 - offsetX, 139 - offsetY);
    CGPathAddLineToPoint(path, NULL, 19 - offsetX, 130 - offsetY);
    CGPathAddLineToPoint(path, NULL, 9 - offsetX, 130 - offsetY);
    CGPathAddLineToPoint(path, NULL, 9 - offsetX, 120 - offsetY);
    CGPathAddLineToPoint(path, NULL, 19 - offsetX, 120 - offsetY);
    CGPathAddLineToPoint(path, NULL, 19 - offsetX, 109 - offsetY);
    CGPathAddLineToPoint(path, NULL, 0 - offsetX, 90 - offsetY);
    CGPathAddLineToPoint(path, NULL, 0 - offsetX, 20 - offsetY);
    CGPathAddLineToPoint(path, NULL, 19 - offsetX, 0 - offsetY);
    CGPathAddLineToPoint(path, NULL, 79 - offsetX, 0 - offsetY);
    CGPathAddLineToPoint(path, NULL, 99 - offsetX, 20 - offsetY);
    CGPathAddLineToPoint(path, NULL, 99 - offsetX, 90 - offsetY);
    CGPathAddLineToPoint(path, NULL, 79 - offsetX, 110 - offsetY);
    CGPathAddLineToPoint(path, NULL, 80 - offsetX, 120 - offsetY);
    CGPathAddLineToPoint(path, NULL, 89 - offsetX, 120 - offsetY);
    CGPathAddLineToPoint(path, NULL, 89 - offsetX, 130 - offsetY);
    CGPathAddLineToPoint(path, NULL, 79 - offsetX, 131 - offsetY);
    CGPathAddLineToPoint(path, NULL, 79 - offsetX, 139 - offsetY);
    CGPathAddLineToPoint(path, NULL, 69 - offsetX, 139 - offsetY);
    CGPathAddLineToPoint(path, NULL, 69 - offsetX, 100 - offsetY);
    CGPathAddLineToPoint(path, NULL, 89 - offsetX, 79 - offsetY);
    CGPathAddLineToPoint(path, NULL, 89 - offsetX, 30 - offsetY);
    CGPathAddLineToPoint(path, NULL, 69 - offsetX, 10 - offsetY);
    CGPathAddLineToPoint(path, NULL, 30 - offsetX, 10 - offsetY);
    CGPathAddLineToPoint(path, NULL, 9 - offsetX, 31 - offsetY);
    CGPathAddLineToPoint(path, NULL, 9 - offsetX, 76 - offsetY);
    CGPathAddLineToPoint(path, NULL, 29 - offsetX, 100 - offsetY);
        
    CGPathCloseSubpath(path);
        
    sprite.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];
}

Now you may see a lot of errors but don't worry we are going to fix that so make the following changes.




override func didMoveToView(view: SKView) {
    //SKSpriteNode *sprite = [SKSpriteNode spriteNodeWithImageNamed:@"bottle.png"];
    //CGFloat offsetX = sprite.frame.size.width * sprite.anchorPoint.x;
     //CGFloat offsetY = sprite.frame.size.height * sprite.anchorPoint.y;
        
    let sprite: SKSpriteNode = SKSpriteNode(imageNamed: "bottle.png")
    // Position the sprite in the middle of the screen
    sprite.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
    let offsetX: CGFloat = sprite.frame.size.width * sprite.anchorPoint.x
    let offsetY: CGFloat = sprite.frame.size.height * sprite.anchorPoint.y
        
        
    //CGMutablePathRef path = CGPathCreateMutable();
        
    let path: CGMutablePathRef = CGPathCreateMutable()
        
    // Change all NULL values to nil
    CGPathMoveToPoint(path, nil, 29 - offsetX, 139 - offsetY)
    CGPathAddLineToPoint(path, nil, 19 - offsetX, 139 - offsetY)
    CGPathAddLineToPoint(path, nil, 19 - offsetX, 130 - offsetY)
    CGPathAddLineToPoint(path, nil, 9 - offsetX, 130 - offsetY)
    CGPathAddLineToPoint(path, nil, 9 - offsetX, 120 - offsetY)
    CGPathAddLineToPoint(path, nil, 19 - offsetX, 120 - offsetY)
    CGPathAddLineToPoint(path, nil, 19 - offsetX, 109 - offsetY)
    CGPathAddLineToPoint(path, nil, 0 - offsetX, 90 - offsetY)
    CGPathAddLineToPoint(path, nil, 0 - offsetX, 20 - offsetY)
    CGPathAddLineToPoint(path, nil, 19 - offsetX, 0 - offsetY)
    CGPathAddLineToPoint(path, nil, 79 - offsetX, 0 - offsetY)
    CGPathAddLineToPoint(path, nil, 99 - offsetX, 20 - offsetY)
    CGPathAddLineToPoint(path, nil, 99 - offsetX, 90 - offsetY)
    CGPathAddLineToPoint(path, nil, 79 - offsetX, 110 - offsetY)
    CGPathAddLineToPoint(path, nil, 80 - offsetX, 120 - offsetY)
    CGPathAddLineToPoint(path, nil, 89 - offsetX, 120 - offsetY)
    CGPathAddLineToPoint(path, nil, 89 - offsetX, 130 - offsetY)
    CGPathAddLineToPoint(path, nil, 79 - offsetX, 131 - offsetY)
    CGPathAddLineToPoint(path, nil, 79 - offsetX, 139 - offsetY)
    CGPathAddLineToPoint(path, nil, 69 - offsetX, 139 - offsetY)
    CGPathAddLineToPoint(path, nil, 69 - offsetX, 100 - offsetY)
    CGPathAddLineToPoint(path, nil, 89 - offsetX, 79 - offsetY)
    CGPathAddLineToPoint(path, nil, 89 - offsetX, 30 - offsetY)
    CGPathAddLineToPoint(path, nil, 69 - offsetX, 10 - offsetY)
    CGPathAddLineToPoint(path, nil, 30 - offsetX, 10 - offsetY)
    CGPathAddLineToPoint(path, nil, 9 - offsetX, 31 - offsetY)
    CGPathAddLineToPoint(path, nil, 9 - offsetX, 76 - offsetY)
    CGPathAddLineToPoint(path, nil, 29 - offsetX, 100 - offsetY)
        
    CGPathCloseSubpath(path)
        
    //sprite.physicsBody = [SKPhysicsBody bodyWithPolygonFromPath:path];
    sprite.physicsBody = SKPhysicsBody(polygonFromPath: path)
    // Set Dynamic to false
    sprite.physicsBody?.dynamic = false
    // Add the sprite to the scene
    self.addChild(sprite)
}

Let's make physics visible by modifying the viewDidLoad function in GameViewController.swift




override func viewDidLoad() {
    super.viewDidLoad()

    let scene = GameScene(fileNamed:"GameScene")
    let skView = self.view as SKView
    skView.showsFPS = true
    skView.showsNodeCount = true
    skView.showsPhysics = true
    skView.ignoresSiblingOrder = true
        
    scene.scaleMode = .AspectFill
        
    skView.presentScene(scene)
}

Build and Run to see the results.



As you noticed is working but our physics shape is not the same size as we draw earlier and that happens because in X-Code 6 all our content is scaled.




// viewDidLoad()
scene.scaleMode = .AspectFill

To correct our shape let's add 3 new functions that will helps us to correct our physics body.




func offset(node: SKSpriteNode, isX: Bool)->CGFloat {
    return isX ? node.frame.size.width * node.anchorPoint.x : node.frame.size.height * node.anchorPoint.y
}
    
func AddLineToPoint(path: CGMutablePath!, x: CGFloat, y: CGFloat, node: SKSpriteNode) {
    CGPathAddLineToPoint(path, nil, (x * 2) - offset(node, isX: true), (y * 2) - offset(node, isX: false))
}
    
func MoveToPoint(path: CGMutablePath!, x: CGFloat, y: CGFloat, node: SKSpriteNode) {
    CGPathMoveToPoint(path, nil, (x * 2) - offset(node, isX: true), (y * 2) - offset(node, isX: false))
}

Basically we just need to multiply the point value per 2 to obtain the correct size so let's modify the didMoveToView function like the following.




override func didMoveToView(view: SKView) {
    let sprite: SKSpriteNode = SKSpriteNode(imageNamed: "bottle.png")
    sprite.position = CGPointMake(CGRectGetMidX(self.frame), CGRectGetMidY(self.frame))
        
    let path: CGMutablePathRef = CGPathCreateMutable()

    MoveToPoint(path,    x: 29, y: 139, node: sprite)
    AddLineToPoint(path, x: 19, y: 139, node: sprite)
    AddLineToPoint(path, x: 19, y: 130, node: sprite)
    AddLineToPoint(path, x: 9,  y: 130, node: sprite)
    AddLineToPoint(path, x: 9,  y: 120, node: sprite)
    AddLineToPoint(path, x: 19, y: 120, node: sprite)
    AddLineToPoint(path, x: 19, y: 109, node: sprite)
    AddLineToPoint(path, x: 0,  y: 90,  node: sprite)
    AddLineToPoint(path, x: 0,  y: 20,  node: sprite)
    AddLineToPoint(path, x: 19, y: 0,   node: sprite)
    AddLineToPoint(path, x: 79, y: 0,   node: sprite)
    AddLineToPoint(path, x: 99, y: 20,  node: sprite)
    AddLineToPoint(path, x: 99, y: 90,  node: sprite)
    AddLineToPoint(path, x: 79, y: 110, node: sprite)
    AddLineToPoint(path, x: 80, y: 120, node: sprite)
    AddLineToPoint(path, x: 89, y: 120, node: sprite)
    AddLineToPoint(path, x: 89, y: 130, node: sprite)
    AddLineToPoint(path, x: 79, y: 131, node: sprite)
    AddLineToPoint(path, x: 79, y: 139, node: sprite)
    AddLineToPoint(path, x: 69, y: 139, node: sprite)
    AddLineToPoint(path, x: 69, y: 100, node: sprite)
    AddLineToPoint(path, x: 89, y: 79,  node: sprite)
    AddLineToPoint(path, x: 89, y: 30,  node: sprite)
    AddLineToPoint(path, x: 69, y: 10,  node: sprite)
    AddLineToPoint(path, x: 30, y: 10,  node: sprite)
    AddLineToPoint(path, x: 9,  y: 31,  node: sprite)
    AddLineToPoint(path, x: 9,  y: 76,  node: sprite)
    AddLineToPoint(path, x: 29, y: 100, node: sprite)
 
    CGPathCloseSubpath(path);
        
    sprite.physicsBody = SKPhysicsBody(polygonFromPath: path)
    sprite.physicsBody?.dynamic = false
    self.addChild(sprite) 
}

Build and Run to see the results



Good! the physics body size is now as expected and now we need to test it so add the following function.




func generateFluid() {
    let sprite: SKSpriteNode = SKSpriteNode(imageNamed: "ball")
    sprite.position = CGPointMake(Float.range(CGRectGetMidX(self.frame) - 40, max: CGRectGetMidX(self.frame) + 40), CGRectGetMidY(self.frame) + 350)
    sprite.physicsBody = SKPhysicsBody(circleOfRadius: sprite.size.width/2)
    self.addChild(sprite)
        
    sprite.runAction(SKAction.sequence([SKAction.waitForDuration(20), SKAction.removeFromParent()]))
    }

Change the zPosition of the sprite and run the following actions at the bottom of the didMoveToView function.




sprite.physicsBody = SKPhysicsBody(polygonFromPath: path)
sprite.physicsBody?.dynamic = false
sprite.zPosition = 10
self.addChild(sprite)

self.runAction(SKAction.repeatActionForever(SKAction.sequence([SKAction.waitForDuration(0.05), SKAction.runBlock(self.generateFluid)])))
sprite.runAction(SKAction.repeatActionForever(SKAction.sequence([SKAction.waitForDuration(10), SKAction.rotateByAngle(6.28318531, duration: 5.0)])))

Inside GameViewController.swift set the showsPhysics value to false.




skView.showsPhysics = false

Build and Run to see the results.



it's working!!


Seems that this is the end of the tutorial, now is your turn to create tons of custom shapes and add them to your games.

what's next?

In this tutorial you've learned how to create custom Physics Bodies but why did i choose to create the custom shape of a bottle?

Only for one important reason, if you apply the correct particle system configuration to the blue balls then you can achieve fluid in Sprite Kit so then the bottle will be useful.

Here you can download the final project Final Project

we have covered a lot of topics through this tutorial, i hope you found it helpful and i hope you have enjoyed it, feel free to leave a comment if you have any questions or if you just want say hi.


Go Back

  Posted on August 10, 2014 at 03:37 AM by Julio Montoya