iOS GameKit (Swift) Controller

A Leisurely Evening Exercise

Tonight I decided to Explore SpriteKit. I decided a quick project would be to create a simple touch controller (using a gamepad control stick as a metaphor). I drew most of the inspiration for this project from the iOS Chrono Trigger port which I always enjoyed.

Quick Attribution

Setting Up The SKSpriteNodes

I’m still learning the SpriteKit and game scene environments. I admit, the implementation here might be a bit wonky. I wanted to follow a singleton model for my cursor, but only have it visible (and applicable) when the user was touching and dragging.


    func sharedCursorBg() -> SKSpriteNode {
        //Background Cursor
        let kCursorBGName : String = "tacticalCursorBackground"
        let n : SKSpriteNode
        if ((self.childNode(withName: kCursorBGName)) != nil) {
            n = self.childNode(withName: kCursorBGName) as! SKSpriteNode
        } else {
            n = SKSpriteNode(imageNamed: kCursorBGName)
            n.name = kCursorBGName
            self.addChild(n)
        }
        n.alpha = 0.75

        return n
    }
    
    func sharedCursorFg() -> SKSpriteNode {
        //Forground Cursor
        let kCursorFGName : String = "tacticalCursorForeground"
        let n : SKSpriteNode
        if ((self.childNode(withName: kCursorFGName)) != nil) {
            n = self.childNode(withName: kCursorFGName) as! SKSpriteNode
        } else {
            n = SKSpriteNode(imageNamed: kCursorFGName)
            n.name = kCursorFGName
            self.addChild(n)
        }
        n.alpha = 0.75
        
        return n
    }

Looking At Touch Events

The game scene has several touch events provided in the Hello World app:


    func touchDown(atPoint pos : CGPoint) {
        ...
    }

    func touchMoved(toPoint pos : CGPoint) {
        ...
    }
    func touchUp(atPoint pos : CGPoint) {
        ...
    }

    override func touchesBegan(_ touches: Set, with event: UIEvent?) {
        for t in touches { self.touchDown(atPoint: t.location(in: self)) }
    }
    
    override func touchesMoved(_ touches: Set, with event: UIEvent?) {
        for t in touches { self.touchMoved(toPoint: t.location(in: self)) }
    }
    
    override func touchesEnded(_ touches: Set, with event: UIEvent?) {
        for t in touches { self.touchUp(atPoint: t.location(in: self)) }
    }
    
    override func touchesCancelled(_ touches: Set, with event: UIEvent?) {
        for t in touches { self.touchUp(atPoint: t.location(in: self)) }
    }

Displaying The Cursor

As I was trying to replicate the control structure for Chrono Trigger’s iOS Port I determined that navigation should live within the confines of touchMoved. This will reserve tapping for other interactions (such as tapping an object) without bringing up the navigation cursor—should I continue this game project. The initial implementation looked like this:


    func touchMoved(toPoint pos : CGPoint) {
        //ZBEYER TODO: CLEAN THIS UP!
        
        //Only set the cursorBackground once (fixed)
        if (cursorBackground == nil) {
            cursorBackground = sharedCursorBg()
            cursorBackground?.position = pos
        } else if (self.childNode(withName: (cursorBackground?.name)!) == nil) {
            cursorBackground = sharedCursorBg()
            cursorBackground?.position = pos
        }
        
        //cursorMain (pointer) is dynamic
        if (cursorMain == nil) {
            cursorMain = sharedCursorFg()
        }
        cursorMain?.position = pos

simulator-screen-shot-oct-26-2016-7-15-33-pm

Binding The Cursor

I set about restricting the main cursor (or control stick metaphor) to the global bounds of the background cursor:


    func touchMoved(toPoint pos : CGPoint) {
        //ZBEYER TODO: CLEAN THIS UP!
        
        //Only set the cursorBackground once (fixed)
        if (cursorBackground == nil) {
            cursorBackground = sharedCursorBg()
            cursorBackground?.position = pos
        } else if (self.childNode(withName: (cursorBackground?.name)!) == nil) {
            cursorBackground = sharedCursorBg()
            cursorBackground?.position = pos
        }
        
        //cursorMain (pointer) is dynamic
        if (cursorMain == nil) {
            cursorMain = sharedCursorFg()
        }
        
        //Compute Min / Max Bounds for cursor
        var point : CGPoint = pos
            //Because the anchor is set to the middle of each sprite be default use half size offsets
        let maxBoundX: CGFloat = (cursorBackground?.position.x)! + cursorBackground!.size.width * 0.5
        let minBoundX: CGFloat = (cursorBackground?.position.x)! - cursorBackground!.size.width * 0.5
        let maxBoundY: CGFloat = (cursorBackground?.position.y)! + cursorBackground!.size.height * 0.5
        let minBoundY: CGFloat = (cursorBackground?.position.y)! - cursorBackground!.size.height * 0.5

        //Enforce Min / Max Bounds for cursor
        if pos.x > maxBoundX {
            point = CGPoint(x: maxBoundX, y: pos.y)
        } else if pos.x < minBoundX { point = CGPoint(x: minBoundX, y: pos.y) } if pos.y > maxBoundY {
            point = CGPoint(x: point.x, y: maxBoundY)
        } else if pos.y < minBoundY {
            point = CGPoint(x: point.x, y: minBoundY)
        }

        //Set Cursor to point
        cursorMain?.position = point
    }

simulator-screen-shot-oct-26-2016-7-45-33-pm

Implementing Movement

Next, it was time to actually add a player sprite, background, and movement (to visually show the effect).


    override func update(_ currentTime: TimeInterval) {
        // Called before each frame is rendered
        
        // Initialize _lastUpdateTime if it has not already been
        if (self.lastUpdateTime == 0) {
            self.lastUpdateTime = currentTime
        }
        
        
        //ZBEYER TODO: clean this too
        //If we have an active navigation cursor...
        if ((cursorMain != nil) && (cursorBackground != nil)) {
            //Determine velocity as interpolated value: 0-1
            let v: CGPoint? = CGPoint(x: ((cursorMain?.position.x)! - (cursorBackground?.position.x)!) / (cursorBackground!.size.width * 0.5),
                                      y: ((cursorMain?.position.y)! - (cursorBackground?.position.y)!) / (cursorBackground!.size.height * 0.5))
            //Move at Velocity
            movePlayerAtVelocity(velocity: v!);
        }
        
        // Calculate time since last update
        let dt = currentTime - self.lastUpdateTime
        
        // Update entities
        for entity in self.entities {
            entity.update(deltaTime: dt)
        }
        
        self.lastUpdateTime = currentTime
    }


    func movePlayerAtVelocity(velocity:CGPoint) {
        //Constant Speed
        let kPLayerSpeed : CGFloat = 10
        
        //Define Player & Camera
        let player:SKSpriteNode = self.childNode(withName: "playerNode") as! SKSpriteNode
        let camera:SKCameraNode = self.childNode(withName: "mainCamera") as! SKCameraNode
        
        //Move Player and Camera together so Player is always centered
        player.position = CGPoint(x:player.position.x + velocity.x * kPLayerSpeed, y:player.position.y + velocity.y * kPLayerSpeed)
        camera.position = player.position
        
        //Angle calculation borrowed from https://www.raywenderlich.com/118225/introduction-sprite-kit-scene-editor
        let angle = atan2(velocity.y, velocity.x) + CGFloat(M_PI)
        let rotateAction = SKAction.rotate(toAngle: angle + CGFloat(M_PI*0.5), duration: 0)
        
        //Apply rotation
        player.run(rotateAction)

        //Move cursor with player
        cursorBackground?.position = CGPoint(x:(cursorBackground?.position.x)! + velocity.x * kPLayerSpeed, y:(cursorBackground?.position.y)! + velocity.y * kPLayerSpeed)
        cursorMain?.position = CGPoint(x:(cursorMain?.position.x)! + velocity.x * kPLayerSpeed, y:(cursorMain?.position.y)! + velocity.y * kPLayerSpeed)
    }
    
    func touchUp(atPoint pos : CGPoint) {
        //Remove Cursor on touchup
        cursorMain?.removeFromParent()
        cursorMain = nil
        
        if (cursorBackground != nil) {
            cursorBackground?.run(SKAction.sequence([SKAction.fadeOut(withDuration: 0.1),
                                          SKAction.removeFromParent()]))
            cursorBackground = nil
            
        } else {
            //TOUCH

            if let n = self.spinnyNode?.copy() as! SKShapeNode? {
                n.position = pos
                n.strokeColor = SKColor.green
                self.addChild(n)
            }
            
            print("TOUCH EVENT")
        }   
    }

simulator-screen-shot-oct-26-2016-8-52-18-pm

Wrapping Up

I spent a few minutes making better cursor images and ran on the iPad simulator to get a better picture:

simulator-screen-shot-oct-26-2016-9-47-39-pm

Bookmark and Share

One thought on “iOS GameKit (Swift) Controller

Leave a Reply

Your email address will not be published. Required fields are marked *