MacBook Pro touch bar runner game tutorial

So the new MacBook Pro's are here, and as usual everyone is whining about the spec, and the price. Come on guys... these machines are amazing. Read this first, and dare to tell me again that you need 32 gigabytes of ram. Pricing? Like you have to buy a new mac every goddamn year...

Can We Stop the Apple-Bitching Already?

Now do some work here. We are going to make a super easy game for the brand new touch bar. First of all, you can check the human interface guidelines here, but there is a cheatsheet on github if you don't like to read that much. The first tutorials are already out, and if you download the sample codes here, you will have a great starting point.

A basic runner game for the touch bar

Touch Bar API

In order to have a touch bar support in your application you have to just some minimal extra coding, so it's really easy to adapt this new technology. You can even create games if you want, if you check the official catalog sample by Apple you'll notice how easy is to add a custom view to the Touch Bar.

Let's make a runner game with the help of SpriteKit!

Define the custom touch bar identifier

fileprivate extension NSTouchBarCustomizationIdentifier {  
    static let customTouchBar = NSTouchBarCustomizationIdentifier("com.tiborbodecs.touchbar.customTouchBar")
}

Define your item identifiers

fileprivate extension NSTouchBarItemIdentifier {  
    static let customView = NSTouchBarItemIdentifier("com.tiborbodecs.touchbar.items.customView")
}

Create your own NSTouchBar instance (for example in your custom window controller subclass)

class WindowController: NSWindowController {

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

    @available(OSX 10.12.1, *)
    override func makeTouchBar() -> NSTouchBar? {
        let touchBar = NSTouchBar()
        touchBar.delegate = self
        touchBar.customizationIdentifier = .customTouchBar
        touchBar.defaultItemIdentifiers = [.customView]
        touchBar.customizationAllowedItemIdentifiers = [.customView]

        return touchBar
    }
}

Create the elements for the defined item identifiers

@available(OSX 10.12.1, *)
extension WindowController: NSTouchBarDelegate {

    func touchBar(_ touchBar: NSTouchBar, makeItemForIdentifier identifier: NSTouchBarItemIdentifier) -> NSTouchBarItem? {

        switch identifier {
        case NSTouchBarItemIdentifier.customView:
            let gameView = SKView()
            let scene    = GameScene()
            let item     = NSCustomTouchBarItem(identifier: identifier)
            item.view    = gameView
            gameView.presentScene(scene)

            return item

        default:
            return nil
        }
    }
}

That's it. Our custom SKView is on the touch bar, and we can start to work on our game logic, as usual... Oh, wait let's handle some key events first...

let jumpNotification = Notification.Name("JumpNotificationIdentifier")


class Window: NSWindow {

    override func keyDown(with event: NSEvent) {
//        super.keyDown(with: event) //we don't want the beep sound...

        if event.keyCode == 49 { //space 
            NotificationCenter.default.post(name: jumpNotification, 
                                          object: nil)
        }
    }
}

 

Game logic

 

Lights, camera, action!

In this phase, we're going to create the ground object and the player. It's super easy we just need two SKSpriteNode objects, with a physics body. We'll also handle the jump event.

import SpriteKit  
import GameplayKit


class GameScene: SKScene {

    let playerVelocity: CGFloat = 500

    enum PhysicsCategory: UInt32 {
        case player   = 0
        case ground   = 1
        case obstacle = 2
    }

    var ground: SKSpriteNode!
    var player: SKSpriteNode!
}


extension GameScene {

    override func didMove(to view: SKView) {
        super.didMove(to: view)

        //we set the scale of the view
        self.scaleMode       = .resizeFill
        self.backgroundColor = .white

        //some debug info
        let debug           = true
        view.showsPhysics   = debug
        view.showsFPS       = debug
        view.showsNodeCount = debug

        //subscribe to the key down notification
        NotificationCenter.default
                          .addObserver(self, selector: #selector(self.jump), 
                                                 name: jumpNotification, 
                                               object: nil)
        //make the ground
        let ground  = self.createGround()
        self.ground = ground
        self.addChild(self.ground)

        //add the player
        let player  = self.createPlayer()
        self.player = player
        self.addChild(self.player)

        //add a camera object 
        let camera      = SKCameraNode()
        camera.position = CGPoint(x: self.player.position.x, y: 15.5)
        self.camera     = camera
        self.addChild(camera)
    }

    //update the camera & the ground according to the player
    override func didFinishUpdate() {
        super.didFinishUpdate()

        self.player.physicsBody?.velocity.dx = self.playerVelocity

        self.ground.position.x  = self.player.position.x
        self.camera?.position.x = self.player.position.x
    }
}

In order to move the camera, we have to re-center it after every update cycle, so we'll set the x position to the new value. We'll also have to keep the velocity of the player node.

extension GameScene {

    func jump() {
        self.player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: -15))
    }

    func createGround() -> SKSpriteNode {
        let ground       = SKSpriteNode()
        ground.size      = CGSize(width: 1085, height: 0)
        ground.position  = .zero
        ground.zPosition = 1

        let body              = SKPhysicsBody(edgeLoopFrom: ground.frame)
        body.categoryBitMask  = PhysicsCategory.ground.rawValue
        body.fieldBitMask     = PhysicsCategory.ground.rawValue
        body.collisionBitMask = PhysicsCategory.player.rawValue
        body.isDynamic        = false
        ground.physicsBody    = body
        return ground
    }

    func createPlayer() -> SKSpriteNode {
        let frames           = SKTextureAtlas(named: "PlayerMovement").textureNames.map { SKTexture(imageNamed: $0) }
        let playerNode       = SKSpriteNode(texture: frames.first)
        playerNode.size      = CGSize(width: 15, height: 15)
        playerNode.position  = .zero
        playerNode.zPosition = 1

        playerNode.run(SKAction.repeatForever(
            SKAction.animate(with: frames,
                             timePerFrame: 0.1,
                             resize: false,
                             restore: true)),
                       withKey:"player-movement-animation")

        let body                = SKPhysicsBody(rectangleOf: playerNode.size)
        body.isDynamic          = true
        body.affectedByGravity  = true
        body.allowsRotation     = false
        body.velocity.dx        = self.playerVelocity
        body.categoryBitMask    = PhysicsCategory.player.rawValue
        body.collisionBitMask   = PhysicsCategory.ground.rawValue
        body.contactTestBitMask = PhysicsCategory.obstacle.rawValue
        playerNode.physicsBody  = body
        playerNode.physicsBody?.applyImpulse(CGVector(dx: self.playerVelocity, dy: 0))

        return playerNode
    }
}

 

Background

We'll create a new background node in every x cycle, and we'll remove the unnecessary background nodes, after we have a lot of them...

class GameScene: SKScene {

    let backgroundTime: CFTimeInterval         = 1.0
    var backgroundPreviousTime: CFTimeInterval = 0
    var backgroundTimeCount: CFTimeInterval    = 0
    var backgrounds: [SKSpriteNode]            = []

    override func update(_ currentTime: TimeInterval) {

        self.backgroundTimeCount += currentTime - self.backgroundPreviousTime

        if self.backgroundTimeCount > self.backgroundTime {
            self.createBackground()

            self.backgroundTimeCount = 0
        }

        self.backgroundPreviousTime = currentTime
    }

    func createBackground() {
        let background       = SKSpriteNode(imageNamed: "background")
        background.size      = CGSize(width: 1920, height: 60)
        background.zPosition = 0

        if let last = self.backgrounds.last {
            background.position.x = last.position.x + last.frame.width
        }
        else {
            background.position.x = 0
        }

        self.backgrounds.append(background)
        self.addChild(background)

        if self.backgrounds.count > 100 {
            self.backgrounds.first?.removeFromParent()
            self.backgrounds.removeFirst()
        }
    }
}

 

Obstacles

Finally we're going to add some random obstacles...

extension Double {

    static var random: Double {
        return Double(arc4random()) / 0xFFFFFFFF
    }

    static func random(min: Double, max: Double) -> Double {
        return Double.random * (max - min) + min
    }    
}

class GameScene: SKScene {  
    //...
    var obstacleTime: CFTimeInterval           = 1.0
    var obstaclePreviousTime: CFTimeInterval   = 0
    var obstacleTimeCount: CFTimeInterval      = 0
    var obstacles: [SKSpriteNode]              = []
}

extension GameScene {

    override func didMove(to view: SKView) {
        super.didMove(to: view)
        //"collision" delegate
        self.physicsWorld.contactDelegate = self
        //...
    }

    override func update(_ currentTime: TimeInterval) {
        self.obstacleTimeCount += currentTime - self.obstaclePreviousTime

        if self.obstacleTimeCount > self.obstacleTime {
            self.obstacleTimeCount = 0
            self.obstacleTime = Double.random(min: 0.5, max: 1.5)
            self.handleObstacles()
        }
        self.obstaclePreviousTime = currentTime
        //...
    }
}

extension GameScene: SKPhysicsContactDelegate {

    func didBegin(_ contact: SKPhysicsContact) {
        self.isPaused  = true
        self.alpha     = 0.5
    }
}

extension GameScene {

    func jump() {
        self.player.physicsBody?.applyImpulse(CGVector(dx: 0, dy: -15))

        if self.isPaused {
            self.isPaused = false
            self.alpha    = 1.0
        }
    }

    func handleObstacles() {
        let x: CGFloat     = self.player.position.x + 2000
        let y: CGFloat     = 8
        let rect           = CGRect(x: x, y: y, width: 18, height: 18)
        let obstacle       = SKSpriteNode(texture: SKTexture(imageNamed: "obstacle"))
        obstacle.size      = rect.size
        obstacle.position  = rect.origin
        obstacle.zPosition = 2

        let body                = SKPhysicsBody(rectangleOf: obstacle.frame.size)
        body.affectedByGravity  = false
        body.allowsRotation     = false
        body.isDynamic          = false
        body.categoryBitMask    = PhysicsCategory.obstacle.rawValue
        body.contactTestBitMask = PhysicsCategory.player.rawValue
        obstacle.physicsBody    = body

        self.obstacles.append(obstacle)
        self.addChild(obstacle)

        if self.obstacles.count > 100 {
            self.obstacles.first?.removeFromParent()
            self.obstacles.removeFirst()
        }
    }
}

 

Firstly, I wanted to make a Philips HUE controller, but @viaszkadi came up with this awesome runner game idea and I really liked the concept, so we made it. The final code will be available after I can test this on my new MacBook Pro.