SpriteKit

SpriteKit

SpriteKit Framework
Create a New Project

Create A New Example Game

Setup
1
2
3
4
5
6
7
8
9
import SpriteKit
import GameplayKit

class GameScene: SKScene {

    override func didMove(to view: SKView) {

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import UIKit
import SpriteKit
import GameplayKit

class GameViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
    }

    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return .landscape
    }

    override var prefersStatusBarHidden: Bool {
        return true
    }
}
View, Node, and Scene
Create your first scene
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
override func viewDidLoad() {
    super.viewDidLoad()
    //create the view
    if let view = self.view as! SKView? {
        //create the scene
        let scene = GameScene(size: CGSize(width: 1336, height: 1024))

        //set the scale mode to scale to fill the view window
        scene.scaleMode = .aspectFill

        //set the background colo
        scene.backgroundColor = UIColor(red: 105/255, green: 157/255, blue: 181/255, alpha: 1.0)

        //present the scene
        view.presentScene(scene)

        //set the view options
        view.ignoresSiblingOrder = false
        view.showsPhysics = false
        view.showsFPS = true
        view.showsNodeCount = true
    }
}
Creating your first visual node
1
2
3
4
5
6
7
8
9
10
11
override func didMove(to view: SKView) {
    let background = SKSpriteNode(imageNamed: "background")
    background.anchorPoint = CGPoint(x:0, y:0)
    background.position = CGPoint(x:10, y:330)
    addChild(background)

    let foreground = SKSpriteNode(imageNamed: "foreground")
    foreground.anchorPoint = CGPoint(x: 0, y: 0)
    foreground.position = CGPoint(x: 15, y: 158)
    addChild(foreground)
}
Add a player
1
2
3
4
5
6
7
8
import Foundation
import SpriteKit

enum Layer:CGFloat {
    case background
    case foreground
    case player
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import Foundation
import SpriteKit

class Player:SKSpriteNode {
    // MARK: - PROPERTIES

    // MARK: - INT
    init() {
        // set default textture
        let texture = SKTexture(imageNamed: "frame_0")

        // call to super.init
        super.init(texture: texture, color: .clear, size: texture.size())

        self.name = "player"
        self.setScale(1.0)
        self.anchorPoint = CGPoint(x: 0.5, y: 0.0)
        self.zPosition = Layer.player.rawValue
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let player = Player()
override func didMove(to view: SKView) {
    let background = SKSpriteNode(imageNamed: "background")
    background.anchorPoint = CGPoint(x:0, y:0)
    background.position = CGPoint(x:10, y:330)
    background.zPosition = Layer.background.rawValue
    addChild(background)

    let foreground = SKSpriteNode(imageNamed: "foreground")
    foreground.anchorPoint = CGPoint(x: 0, y: 0)
    foreground.position = CGPoint(x: 15, y: 158)
    foreground.zPosition = Layer.foreground.rawValue
    addChild(foreground)

    player.position = CGPoint(x: size.width/2, y: foreground.frame.maxY)
    addChild(player)
}
Add player movement
1
2
3
4
func moveToPosition(pos: CGPoint, speed: TimeInterval) {
    let moveAction = SKAction.move(to: pos, duration: speed)
    run(moveAction)
}
1
let player = Player()
1
2
3
func touchDown (atPoint pos: CGPoint) {
    player.moveToPosition(pos: pos, speed: 1.0)
}
1
2
3
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    for t in touches{self.touchDown(atPoint: t.location(in: self))}
}
Limit movement
1
2
3
4
5
func setupConstraints(floor: CGFloat) {
    let range = SKRange(lowerLimit: floor, upperLimit: floor)
    let lockToPlatform = SKConstraint.positionY(range)
    constraints = [lockToPlatform]
}
1
player.setupConstraints(floor: foreground.frame.maxY)
Set movement direction
1
2
3
4
5
6
7
8
9
10
func moveToPosition(pos: CGPoint, direction: String, speed: TimeInterval) {
    switch direction {
        case "L":
            xScale = -abs(xScale)
        default:
            xScale = abs(xScale)
    }
    let moveAction = SKAction.move(to: pos, duration: speed)
    run(moveAction)
}
1
2
3
4
5
6
7
func touchDown (atPoint pos: CGPoint) {
    if pos.x < player.position.x {
        player.moveToPosition(pos: pos, direction: "L", speed: 1.0)
    } else {
        player.moveToPosition(pos: pos, direction: "R", speed: 1.0)
    }
}

Augment Your Game

Add collectible item
1
2
3
4
5
6
enum Layer:CGFloat {
    case background
    case foreground
    case player
    case collectible
}
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
import Foundation
import SpriteKit

enum CollectibleType: String {
    case none
    case fruit
}

class Collectible: SKSpriteNode {
    private var collectibleType: CollectibleType = .none

    init(collectibleType: CollectibleType) {
        var texture: SKTexture!
        self.collectibleType = collectibleType

        //set the texture based on the Type
        switch self.collectibleType {
        case .fruit:
            texture = SKTexture(imageNamed: "fruit")
        case .none:
            break
        }
        super.init(texture: texture, color: SKColor.clear, size: texture.size())

        //set up the collectible
        self.name = "co_\(collectibleType)"
        self.anchorPoint = CGPoint(x: 0.5, y: 1.0)
        self.setScale(0.3)
        self.zPosition = Layer.collectible.rawValue
    }

    required init?(coder aDecoder: NSCoder){
        fatalError("init(coder:) has not been implemented")
    }
}
1
2
3
4
5
func spawnFruit() {
    let collectible = Collectible(collectibleType: CollectibleType.fruit)
    collectible.position = CGPoint(x: player.position.x, y:player.position.y * 2.5)
    addChild(collectible)
}
1
2
3
    ...
    spawnFruit()
}
Drop collectible item
1
2
3
4
5
6
7
8
9
10
11
12
13
func drop(dropSpeed: TimeInterval, floorLevel: CGFloat) {
    let pos = CGPoint(x: position.x, y: floorLevel)
    let scaleX = SKAction.scaleX(to: 0.5, duration: 1)
    let scaleY = SKAction.scaleY(to: 0.5, duration: 1)
    let scale = SKAction.group([scaleX, scaleY])

    let appear = SKAction.fadeAlpha(to: 1, duration: 0.25)
    let moveAction = SKAction.move(to: pos, duration: dropSpeed)
    let actionSequence = SKAction.sequence([appear, scale, moveAction])

    self.scale(to: CGSize(width: 0.25, height: 1.0))
    self.run(actionSequence, withKey: "drop")
}
1
collectible.drop(dropSpeed: TimeInterval(1.0), floorLevel: player.frame.minY)
Spawning fruits at random position
1
2
3
4
5
6
7
8
9
10
11
func spawnFruit() {
    let collectible = Collectible(collectibleType: CollectibleType.fruit)

    //set random position
    let margin = collectible.size.width * 2
    let dropRange = SKRange(lowerLimit: frame.minX + margin, upperLimit: frame.maxX - margin)
    let randomX = CGFloat.random(in: dropRange.lowerLimit...dropRange.upperLimit)
    collectible.position = CGPoint(x: randomX, y:player.position.y * 2.5)
    addChild(collectible)
    collectible.drop(dropSpeed: TimeInterval(1.0), floorLevel: player.frame.minY)
}
Spawning multiple fruits
1
2
3
4
5
6
7
8
9
func spawnMultipleFruits() {
    let wait = SKAction.wait(forDuration: TimeInterval(1.0))
    let spawn = SKAction.run {
        self.spawnFruit()
    }
    let sequence = SKAction.sequence([wait, spawn])
    let repeatAction = SKAction.repeat(sequence, count: 10)
    run(repeatAction, withKey: "fruit")
}

Physics and Collision Detection

Add physics body
1
2
3
4
5
    ...
    // add physics body
    self.physicsBody = SKPhysicsBody(rectangleOf: self.size, center: CGPoint(x:0.0, y: self.size.height/2))
    self.physicsBody?.affectedByGravity = false
}
1
2
3
4
5
6
7
8
9
10
...
foreground.zPosition = Layer.foreground.rawValue
//add physics body
foreground.physicsBody = SKPhysicsBody(edgeLoopFrom: foreground.frame)
foreground.physicsBody?.affectedByGravity = false
foreground.physicsBody?.categoryBitMask = PhysicsCategory.foreground
foreground.physicsBody?.contactTestBitMask = PhysicsCategory.collectible
foreground.physicsBody?.collisionBitMask = PhysicsCategory.none
addChild(foreground)
...
1
2
3
4
5
6
enum PhysicsCategory {
    static let none: UInt32 = 0 // a body that does not belong to any physics category
    static let player: UInt32 = 0b1 // 1: player objects
    static let collectible: UInt32 = 0b10 // 2: collectible objects
    static let foreground: UInt32 = 0b100 // 4: ground platforms or scenery that affects physics
}
Physics categories for foreground
1
2
3
4
5
foreground.physicsBody?.categoryBitMask = PhysicsCategory.foreground // set category
foreground.physicsBody?.contactTestBitMask = PhysicsCategory.collectible //test collision with collectible
foreground.physicsBody?.collisionBitMask = PhysicsCategory.none
addChild(foreground)
...
Physics categories for player
1
2
3
self.physicsBody?.categoryBitMask = PhysicsCategory.player // set category
self.physicsBody?.contactTestBitMask = PhysicsCategory.collectible //test collision with collectible
self.physicsBody?.collisionBitMask = PhysicsCategory.none
Physics categories for collectible
1
2
3
self.physicsBody?.categoryBitMask = PhysicsCategory.collectible // set category
self.physicsBody?.contactTestBitMask = PhysicsCategory.player | PhysicsCategory.foreground //test collision with collectible
self.physicsBody?.collisionBitMask = PhysicsCategory.none
Detecting physical collision
1
physicsWorld.contactDelegate = self
1
2
3
4
5
6
7
8
9
10
11
12
13
extension GameScene: SKPhysicsContactDelegate {
    func didBegin(_ contact: SKPhysicsContact) {
        let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

        if collision == PhysicsCategory.player | PhysicsCategory.collectible {
            print("player hit collectible")
        }

        if collision == PhysicsCategory.foreground | PhysicsCategory.collectible {
            print("collectible hit foreground")
        }
    }
}
Handling physical collision
1
2
3
4
5
6
7
func collected() {
    self.run(SKAction.removeFromParent())
}

func missed() {
    self.run(SKAction.removeFromParent())
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func didBegin(_ contact: SKPhysicsContact) {
    let collision = contact.bodyA.categoryBitMask | contact.bodyB.categoryBitMask

    if collision == PhysicsCategory.player | PhysicsCategory.collectible {
        print("player hit collectible")
        let body = contact.bodyA.categoryBitMask == PhysicsCategory.collectible ? contact.bodyA.node : contact.bodyB.node

        if let sprite = body as? Collectible {
            sprite.collected()
        }
    }

    if collision == PhysicsCategory.foreground | PhysicsCategory.collectible {
        print("collectible hit foreground")
        let body = contact.bodyA.categoryBitMask == PhysicsCategory.collectible ? contact.bodyA.node : contact.bodyB.node

        if let sprite = body as? Collectible {
            sprite.missed()
        }
    }
}
1
var scoreLevel: SKLabelNode = SKLabelNode()
1
2
3
4
5
6
7
8
9
10
11
func setupLabels() {
    scoreLevel.name = "score"
    scoreLevel.fontColor = .black
    scoreLevel.fontSize = 55.0
    scoreLevel.horizontalAlignmentMode = .right
    scoreLevel.verticalAlignmentMode = .center
    scoreLevel.zPosition = Layer.ui.rawValue
    scoreLevel.position = CGPoint(x: frame.maxX - 50, y: 700)
    scoreLevel.text = "Score: 0"
    addChild(scoreLevel)
}
1
2
3
4
5
var score : Int = 0 {
    didSet {
        scoreLevel.text = "Score: \(score)"
    }
}
1
score += 10

Properly end the game

Game Over
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func gameOver() {
    removeAction(forKey: "fruit")

    // loop through child nodes and stop actions on collectibles
    enumerateChildNodes(withName: "//co_*") {
        (node, stop) in
        node.removeAction(forKey: "drop")
        node.physicsBody = nil
    }

    let restartButton = SKSpriteNode(color: .blue, size: CGSize(width: 120, height:60))
    restartButton.name = "restartButton"
    restartButton.zPosition = Layer.ui.rawValue
    addChild(restartButton)

    let buttonText = SKLabelNode(text: "Restart")
    buttonText.fontColor = .white
    buttonText.fontSize = 30
    buttonText.verticalAlignmentMode = .center
    restartButton.addChild(buttonText)
}
1
2
var missedCount = 0
var maxAllowedMisses = 5
1
2
3
4
5
6
7
8
9
10
11
12
if collision == PhysicsCategory.foreground | PhysicsCategory.collectible {
    print("collectible hit foreground")
    let body = contact.bodyA.categoryBitMask == PhysicsCategory.collectible ? contact.bodyA.node : contact.bodyB.node

    if let sprite = body as? Collectible {
        sprite.missed()
        missedCount += 1
        if missedCount >= maxAllowedMisses {
            gameOver()
        }
    }
}    
1
2
3
4
5
6
7
8
9
10
func restartGame() {
    score = 0
    missedCount = 0
    enumerateChildNodes(withName: "//co_*") { (node, _) in node.removeFromParent()
    }

    enumerateChildNodes(withName: "restartButton") { (node, _) in
        node.removeFromParent()
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    for t in touches{
        let location = t.location(in: self)
        let nodes = nodes(at: location)

        if nodes.contains(where: { $0.name == "restartButton"}) {
            //restart game
            restartGame()
        } else {
            self.touchDown(atPoint: t.location(in: self))
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
func spawnMultipleFruits() {
    let wait = SKAction.wait(forDuration: TimeInterval(1.0))
    let spawn = SKAction.run {
        self.spawnFruit()
    }
    let sequence = SKAction.sequence([wait, spawn])
    let repeatAction = SKAction.repeat(sequence, count: 10)
    let gameOverAction = SKAction.run {
            self.gameOver()
    }

    let fullSequence = SKAction.sequence([repeatAction, gameOverAction])
    run(fullSequence, withKey: "fruit")
}