Friday, November 20, 2015

Quick Tip: Endless scrolling with SpriteKit and SWIFT (Part 2 of 2)

Natural endless scrolling with easing

Welcome to my tutorial series about scrolling:
  • Part 1: Endless scrolling with background tiles
  • Part 2: Natural endless scrolling with easing


In part one I showed how to implement endless scrolling. This is working fine, but there is still room for improvements. The scrolling starts immediately with full speed and also stops directly after the touch ends. A more natural movement would be to increase and decrease the speed slowly, till the target speed is reached or the movement is stopped. These is also known as ease in or ease out animation.
Thank you to hamobi who helped me with my Stackoverflow question for finding the right solution.

To give you an impression compare the two videos:

Without Easing:




With Easing:







First repeat the steps from Part 1 

The main difference is the calculation of the speed which is done in the scroll function:

// Calculate the new speed with the easing function (New speed is influence by current speed)
let  newSpeed = (currentSpeed + (speed - currentSpeed) * easeOutfactor)
           


Open GameScene.swift:


Tutorial: Endless scrolling with SpriteKit and SWIFT


Replace the complete code with this snippet: 

(For explanation check the comments inside the code snippet)

//
//  GameScene.swift
//  EndlessScrollingDemo
//
//  Created by STEFAN JOSTEN on 13/11/15.
//  Copyright (c) 2015 Stefan. All rights reserved.
//

import SpriteKit

class GameScene: SKScene {
    
    // Some global variables to preserve the state and store the touch positions
    var lastUpdateTime: CFTimeInterval?
    var currentSpeed: CGFloat = 0.0
    var nodeTileWidth: CGFloat = 0.0
    var xTouchCurrentPosition: CGFloat = 0.0
    var xTouchStartPosition: CGFloat = 0.0
    var xTouchDistance: CGFloat = 0.0
    
    // Some global constants to configure the speed
    let speedFactor:CGFloat = 5.0
    let easeOutfactor: CGFloat = 0.04
    
    // Declare the globaly needed sprite kit nodes
    var worldNode: SKSpriteNode?
    var spriteNode: SKSpriteNode?
    
    override func didMoveToView(view: SKView) {
        
        // Setup static background
        let backgroundNode = SKSpriteNode(imageNamed: "Background")
        backgroundNode.size = CGSize(width: self.frame.width, height: self.frame.height)
        backgroundNode.anchorPoint = CGPoint(x: 0, y: 0)
        backgroundNode.zPosition = -10
        self.addChild(backgroundNode)
        
        // Setup dynamic background tiles
        worldNode = SKSpriteNode()
        self.addChild(worldNode!)
        
        // Create the dynamic background tiles. Image of left and right node must be identical
        let leftNode = SKSpriteNode(imageNamed: "LeftTile")
        let middleNode = SKSpriteNode(imageNamed: "RightTile")
        let rightNode = SKSpriteNode(imageNamed: "LeftTile")
        
        // store this value globaly to avoid recalculations during each update call
        nodeTileWidth = leftNode.frame.size.width
        
        leftNode.anchorPoint = CGPoint(x: 0, y: 0)
        leftNode.position = CGPoint(x: 0, y: 0)
        middleNode.anchorPoint = CGPoint(x: 0, y: 0)
        middleNode.position = CGPoint(x: nodeTileWidth, y: 0)
        rightNode.anchorPoint = CGPoint(x: 0, y: 0)
        rightNode.position = CGPoint(x: nodeTileWidth * 2, y: 0)
        
        // Add the tiles to worldNode. worldNode is used to realize the scrolling
        worldNode!.addChild(leftNode)
        worldNode!.addChild(rightNode)
        worldNode!.addChild(middleNode)
        
        
        // Setup spaceship sprite
        spriteNode = SKSpriteNode(imageNamed: "Spaceship")
        spriteNode?.position = CGPoint(x:CGRectGetMidX(self.frame), y:CGRectGetMidY(self.frame))
        spriteNode?.xScale = 0.1
        spriteNode?.yScale = 0.1
        spriteNode?.zPosition = 10
        self.addChild(spriteNode!)
    }
    
    // Store the start touch position
    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        for touch in touches {
            xTouchStartPosition = touch.locationInNode(self).x
        }
    }
    
    // Calculate the distance of the toch movement
    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        for touch in touches {
            xTouchCurrentPosition = touch.locationInNode(self).x
            xTouchDistance = xTouchStartPosition - xTouchCurrentPosition
        }
    }
    
    // Reset all movement states
    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        xTouchCurrentPosition = 0.0
        xTouchDistance = 0.0
        xTouchStartPosition = 0.0
    }
    
    // SpriteKits gameloop function
    override func update(currentTime: CFTimeInterval) {
        
        // Scroll the background
        scroll(xTouchDistance, currentTime: currentTime)

        // Rotate sprite depending on direction
        if xTouchDistance > 0 {
            spriteNode?.zRotation = CGFloat(M_PI/2.0)
        } else if xTouchDistance < 0 {
            spriteNode?.zRotation = -CGFloat(M_PI/2.0)
        }

    }
    
    // Implement the scrolling
    func scroll(speed: CGFloat, currentTime: CFTimeInterval) {
        if lastUpdateTime != nil {
            
            // Calculate the new speed with the easing function (New speed is influence by current speed)
            let  newSpeed = (currentSpeed + (speed - currentSpeed) * easeOutfactor)
            currentSpeed = newSpeed
            
            // Set the new x position depending on the timeframe since the last calls.
            // This is needed because spritekit cannot guarantee that the timeframe is allways the same
            worldNode!.position.x = worldNode!.position.x + newSpeed * CGFloat((currentTime - lastUpdateTime!)) * speedFactor
            
            // Check if right end is reached
            if worldNode!.position.x < -(2 * self.nodeTileWidth) {
                worldNode!.position.x = 0.0
                // Check if left end is reached
            } else if worldNode!.position.x > 0 {
                worldNode!.position.x = -(2 * self.nodeTileWidth)
            }
        }
        lastUpdateTime = currentTime
    }
    
}


You can download the complete sample from my Github repository.

If you want to support me, please download my Apps from the Apple AppStore:



That's all for today.

Cheers,
Stefan





No comments:

Post a Comment