Intro

It’s been a while since the original Angry Birds game released for iOS. Ever since, it inspired hundreds of developers around the world. It was a mixture of a very good concept along with a solid 2d physics engine - Box2d

For few years the easiest way to use Box2d was to download cocos2d for iPhone, setup the basics and you had all you need to create your own 2d physics game.

With the release of iOS 7 back in 2013, Apple has introduced SpriteKit and along with it, a full intergrated Box2d based physics engine.

In this tutorial, i’ll show you how to create an Angry Birds-like, slingshot game using SpriteKit and Swift 2.1. The most recent XCode version as the time of writing, is 7.2.

By the end of this tutorial, we will have a slingshot like the one below:

Setting up the project

Fire up XCode and create a new game. Call it Slingshot:

XCode will create a GameScene.sks file by default. Delete that, we dont need it. Also, delete the default asset catalog Assets.xcassets and replace it with this one

Make sure you disable the Upside Down and the Portrait orientation

Replace the GameViewController class with the one below:

class GameViewController: UIViewController {
    override func viewDidLoad() {
        super.viewDidLoad()
        let skView = view as! SKView
        //skView.showsPhysics = true
        let scene = GameScene(size:view.bounds.size)
        scene.scaleMode = .AspectFill
        skView.presentScene(scene)
    }

    override func shouldAutorotate() -> Bool {
        return true
    }

    override func supportedInterfaceOrientations() -> UIInterfaceOrientationMask {
        return [.LandscapeRight, .LandscapeLeft]
    }

    override func prefersStatusBarHidden() -> Bool {
        return true
    }
}

Tip: You can uncomment the skView.showPhysics if you want to see outlines in your physics bodies.

The code we replaced was loading the scene from the bundle file that we deleted. The code above will load it programatically. Now move on to the GameScene.swift file and replace the GameScene class with the one below:

class GameScene: SKScene {
    override func didMoveToView(view: SKView) {
        backgroundColor = UIColor.blackColor()
    }
}

Go ahead and run the project, you should see an empty, black screen.

Creating sprite classes

Create a new Swift file called Nodes.swift and put the code below:

import SpriteKit

class Projectile: SKShapeNode {
    convenience init(path: UIBezierPath, color: UIColor, borderColor:UIColor) {
        self.init()
        self.path = path.CGPath
        self.fillColor = color
        self.strokeColor = borderColor
    }
}

class Box: SKSpriteNode {
    var integrity: Int = 2 {
        didSet {
            if integrity > 2 {
                integrity = 2
            }
            if integrity < 0 {
                removeFromParent()
            }
            texture = SKTexture(imageNamed: "box_\(integrity)")
        }
    }
}

Projectile will be our… projectile. It’s a subclass of SKShapeNode for our convenience. Not many graphics involved in this tutorial.

Box is an SKSpriteNode. Lots of them are going to be used as the victims of the projectile. The integrity property is changing the graphic of the box until the box has no integrity at all and removes itself from the scene.

Creating the slingshot

The functionality we want is described below:

  • User touches the screen, in or around the projectile.
  • User drags the projectile around to determine the velocity and the angle of the projectile.
  • User releases the finger and the projectile launches.

We also need to make sure that while the user is dragging, the projectile will remain fixed within a certain radius. The image below will help you understand the concept.

So lets go ahead and create couple of helper functions. Add the code below in your GameScene class:

func fingerDistanceFromProjectileRestPosition(projectileRestPosition: CGPoint, fingerPosition: CGPoint) -> CGFloat {
    return sqrt(pow(projectileRestPosition.x - fingerPosition.x,2) + pow(projectileRestPosition.y - fingerPosition.y,2))
}

The function above will return the Eucledian distance (A→B in the figure above) between the projectile’s rest position and the finger.

Now add this function too:

func projectilePositionForFingerPosition(fingerPosition: CGPoint, projectileRestPosition:CGPoint, rLimit:CGFloat) -> CGPoint {
    let θ = atan2(fingerPosition.x - projectileRestPosition.x, fingerPosition.y - projectileRestPosition.y)
    let cX = sin(θ) * rLimit
    let cY = cos(θ) * rLimit
    return CGPoint(x: cX + projectileRestPosition.x, y: cY + projectileRestPosition.y)
}

The above function will give us the position the projectile has to be (even if our finger is outside rLimit.
First, we calulating the angle θ by using the arctangent function atan2(). Then, we calculating the points that the projectile has to be.

The slingshot itself, will not participate in the physics emulation at all. Will be just a picture of a slingshot. We will do this later.

Putting things together

We are going to create a struct that will keep our settings:

struct Settings {
    struct Metrics {
        static let projectileRadius = CGFloat(10)
        static let projectileRestPosition = CGPoint(x: 100, y: 100)
        static let projectileTouchThreshold = CGFloat(10)
        static let projectileSnapLimit = CGFloat(10)
        static let forceMultiplier = CGFloat(0.5)
        static let rLimit = CGFloat(50)
    }
    struct Game {
        static let gravity = CGVector(dx: 0,dy: -9.8)
    }
}

Swift magic here. You can now access the individual settings by using dot notation. ex: Settings.Metrics.rLimit
The constants above are:

  • projectileRadius : The radius of our projectile.
  • projectileRestPosition : The initial position of the projectile on the scene.
  • projectileTouchThreshold : The threshold outside the projectile radius in which the dragging process will start.
  • projectileSnapLimit : If the user lifts the finger and the projectile is within this radius, it will snap back to initial position.
  • forceMultiplier : The force multiplier for the original vector that slingshot is using to fire the projectile.
  • rLimit : The radius of the virtual circle surrounding the slingshot.
  • gravity: The gravity of the physics emulation. -9.8 is the default value but you can play with it to get the desire results.

Put the code above in new file, called Settings.swift

Add a property projectile in the GameScene class. It will be our reference to the projectile:

class GameScene: SKScene {
    var projectile: Projectile! // <--- this one
    
    override func didMoveToView(view: SKView) {
        backgroundColor = UIColor.blackColor()
    }
}

Now add two functions on the GameScene Class:

func setupSlingshot() {
    let slingshot_1 = SKSpriteNode(imageNamed: "slingshot_1")
    slingshot_1.position = CGPoint(x: 100, y: 50)
    addChild(slingshot_1)
    
    let projectilePath = UIBezierPath(
        arcCenter: CGPoint.zero,
        radius: Settings.Metrics.projectileRadius,
        startAngle: 0,
        endAngle: CGFloat(M_PI * 2),
        clockwise: true
    )
    projectile = Projectile(path: projectilePath, color: UIColor.redColor(), borderColor: UIColor.whiteColor())
    projectile.position = Settings.Metrics.projectileRestPosition
    addChild(projectile)
    
    let slingshot_2 = SKSpriteNode(imageNamed: "slingshot_2")
    slingshot_2.position = CGPoint(x: 100, y: 50)
    addChild(slingshot_2)
}
    
func setupBoxes() {
    for i in 1...2 {
        for j in 1...8 {
            let box = Box(imageNamed: "box_2")
            box.integrity = 2
            box.position = CGPoint(x: 400 + (i * 20 + 5 * i), y:  j * 20 + j)
            box.physicsBody = SKPhysicsBody(rectangleOfSize: box.size)
            addChild(box)
        }
    }
}

The first function will setup the slingshot and put it on the scene. As we mentioned before, it wont participate in any physics. To give the user the impression that the ball is moving between the slingshot’s brances while the user is dragging, we need to add the children with the correct order. Which is : slingshot_1projectileslingshot_2

The second function will add two stacks of boxes on the right hand side of the screen.

Now edit the didMoveToView function and make it look like this:

override func didMoveToView(view: SKView) {
    backgroundColor = UIColor.blackColor()
    
    physicsBody = SKPhysicsBody(edgeLoopFromRect: frame)
    physicsWorld.gravity = Settings.Game.gravity
    physicsWorld.speed = 0.5
    
    setupSlingshot()
    setupBoxes()
}

Run the project. You should see this:

Touching the screen wont have any effect, since we havent implement the touches methods yet. This is exactly what we are going to do next.

Implementing touches

First of all we need some properties to keep tracking on the touches. Add them in your GameScene class:

class GameScene: SKScene {
    var projectile : Projectile!
    //Touch dragging vars
    var projectileIsDragged = false
    var touchCurrentPoint: CGPoint!
    var touchStartingPoint: CGPoint!
    ....

As we mentioned before, when the user touches in, or few pixels around the ball, the dragging process has to start. Add the touchesBegun in your GameScene class:

override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
    
    func shouldStartDragging(touchLocation:CGPoint, threshold: CGFloat) -> Bool {
        let distance = fingerDistanceFromProjectileRestPosition(
            Settings.Metrics.projectileRestPosition,
            fingerPosition: touchLocation
        )
        return distance < Settings.Metrics.projectileRadius + threshold
    }
    
    if let touch = touches.first {
        let touchLocation = touch.locationInNode(self)
        
        if !projectileIsDragged && shouldStartDragging(touchLocation, threshold: Settings.Metrics.projectileTouchThreshold)  {
            touchStartingPoint = touchLocation
            touchCurrentPoint = touchLocation
            projectileIsDragged = true
        }
    }
}

When the user is touching the screen, we need to find out whether we must start the dragging process or not. The function shouldStartDragging will give us this information. If the process has to start (and we haven’t already started it), we set our tracking properties to current touch location and we turn on the flag projectileIsDragged.

Now copy and paste the touchesMoved function in your GameScene class:

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
    if projectileIsDragged {
        if let touch = touches.first {
            let touchLocation = touch.locationInNode(self)
            let distance = fingerDistanceFromProjectileRestPosition(touchLocation, fingerPosition: touchStartingPoint)
            if distance < Settings.Metrics.rLimit  {
                touchCurrentPoint = touchLocation
            } else {
                touchCurrentPoint = projectilePositionForFingerPosition(
                    touchLocation,
                    projectileRestPosition: touchStartingPoint,
                    circleRadius: Settings.Metrics.rLimit
                )
            }
        }
        projectile.position = touchCurrentPoint
    }
}

If we are under a dragging process, we calculate the distance A→B and if this distance is larger than the rLimit we setting the projectile’s position on the perimeter of the circle with radius rLimit.

Finally, when the user releases the finger, we either snap the projectile back to the rest position (if the drag distance from the rest position is small) or we fire the projectile:

override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
    if projectileIsDragged {
        projectileIsDragged = false
        let distance = fingerDistanceFromProjectileRestPosition(touchCurrentPoint, fingerPosition: touchStartingPoint)
        if distance > Settings.Metrics.projectileSnapLimit {
            let vectorX = touchStartingPoint.x - touchCurrentPoint.x
            let vectorY = touchStartingPoint.y - touchCurrentPoint.y
            projectile.physicsBody = SKPhysicsBody(circleOfRadius: Settings.Metrics.projectileRadius)
            projectile.physicsBody?.applyImpulse(
                CGVector(
                    dx: vectorX * Settings.Metrics.forceMultiplier,
                    dy: vectorY * Settings.Metrics.forceMultiplier
                )
            )
        } else {
            projectile.physicsBody = nil
            projectile.position = Settings.Metrics.projectileRestPosition
        }
    }
}

vectorX and vectorY are the vector components for the force we want to apply on the projectile.

That was it! You can now run the project.

Final words

This was a basic implementation of the Angry birds game. You can download the XCode project from here
If i have enough time in the future, i will update this tutotiral with collision detection callbacks using collisionMasks.

Thanks for reading!