Pablo Garcia

Apps for iPhone, iPad, Apple TV and Apple Watch

Spritekit - Background Scrolling - I

May 10, 2019

There are a lot of games using Infinite-Scrolling-Background technique to simulate characters movement. The character is always on the same place in the screen and the background (buildings, trees, clouds...) moves toward left or right, depending on whether the character is moving to the right or to the left. We have this image:

With this technique we'd have something like this:

But, how could we achieve something like that? Do we have to have an infinite image? The solution is very simple. Have a look at this gif:

In this gif, we have simulated the screen (lines in red) and four consecutives backgrounds (white, blue, red and green), when a background get off the screen we put it at the end and so on. Easy, isn't it?

First approach

I have created BackgroundScroll class to achieve this (bear in mind that positions are relative to the left red line)



import SpriteKit

class BackgroundScroll {

    private let backgrounds: [SKSpriteNode]
    private unowned let scene: SKScene
    private let speed: CGFloat = 25
    private var totalWidth: CGFloat = 0

    init?(backgrounds: [SKSpriteNode], scene: SKScene){
        guard backgrounds.count > 1 else {
            print("You have to include at least two backgrounds")
            return nil
        }
        self.scene = scene
        self.backgrounds = backgrounds
        addBackgroundsToSceneAndCalculateTotalWidth()

    }

    private func addBackgroundsToSceneAndCalculateTotalWidth() {

        for background in backgrounds {
            self.scene.addChild(background)
            totalWidth+=background.frame.width
        }
    }

    public func scrollRight() {
        var offset: CGFloat = 0
        var spaceToBeOutsideScene: CGFloat = 0
        let xOrigin = self.scene.frame.width/2

        for node in backgrounds {
            spaceToBeOutsideScene += node.frame.width
            node.position = CGPoint(x: xOrigin+offset+node.frame.width/2, y: self.scene.frame.height/2)
            let actionMoveOutsideScene = SKAction.moveTo(x: xOrigin-node.frame.width/2, duration: TimeInterval(spaceToBeOutsideScene/speed))
            let actionMoveRightToLeft = SKAction.moveTo(x: xOrigin-node.frame.width/2, duration: TimeInterval((totalWidth)/speed))
            let actionMoveToLastPosition = SKAction.moveTo(x: xOrigin+totalWidth-node.frame.width/2, duration: 0)
            offset += node.size.width
            node.run(SKAction.sequence([actionMoveOutsideScene, SKAction.repeatForever(SKAction.sequence([actionMoveToLastPosition,actionMoveRightToLeft]))]))
        }
    }

}

What we have done is to create SKActions to move the backgrounds from right toward left.

Second approach

There is another way of doing this. We are going to update the backgrounds positions every single time the update function in MainScene is called. We'll use a double linkedlist for that. Each cycle, we’ll update the head node position, the next node position will be relative to the previous one and so on.



class BackgroundScroll {

    private let backgrounds: [SKSpriteNode]
    private var linkedBackgrounds = LinkedList<SKSpriteNode>()

    private unowned let scene: SKScene
    private let speed: CGFloat = 25
    private var totalWidth: CGFloat = 0

    init?(sprites: [SKSpriteNode], scene: SKScene, implementation: Implementation) {
        guard sprites.count > 1 else {
            print("You have to include at least two backgrounds")
            return nil
        }
        self.scene = scene
        self.backgrounds = sprites
        switch implementation {
        case .actions:
            addBackgroundsToSceneAndCalculateTotalWidth()
        case .linkedlist:
            createLinkedList()
        }
    }

    private func addBackgroundsToSceneAndCalculateTotalWidth() {

        for background in backgrounds {
            self.scene.addChild(background)
            totalWidth+=background.frame.width
        }
    }

    private func createLinkedList() {
        var offset: CGFloat = 0
        let xOrigin = self.scene.frame.width/2

        for sprite in backgrounds {
            sprite.position = CGPoint(x: xOrigin+offset+sprite.frame.width/2, y: self.scene.frame.height/2)
            linkedBackgrounds.append(value: sprite)
            self.scene.addChild(sprite)
            offset += sprite.size.width
        }
    }

    public func scrollRight() {
        var offset: CGFloat = 0
        var spaceToBeOutsideScene: CGFloat = 0
        let xOrigin = self.scene.frame.width/2

        for node in backgrounds {
            spaceToBeOutsideScene += node.frame.width
            node.position = CGPoint(x: xOrigin+offset+node.frame.width/2, y: self.scene.frame.height/2)
            let actionMoveOutsideScene = SKAction.moveTo(x: xOrigin-node.frame.width/2,
                                                         duration: TimeInterval(spaceToBeOutsideScene/speed))
            let actionMoveRightToLeft = SKAction.moveTo(x: xOrigin-node.frame.width/2,
                                                        duration: TimeInterval((totalWidth)/speed))
            let actionMoveToLastPosition = SKAction.moveTo(x: xOrigin+totalWidth-node.frame.width/2, duration: 0)
            offset += node.size.width
            node.run(SKAction.sequence([actionMoveOutsideScene, SKAction.repeatForever(SKAction.sequence([actionMoveToLastPosition, actionMoveRightToLeft]))]))
        }
    }

    public func update(deltaTime: TimeInterval) {

        guard !linkedBackgrounds.isEmpty else {
            return
        }

        if let head = linkedBackgrounds.first {
            let headSprite = head.value
            headSprite.position = CGPoint(x: headSprite.position.x-CGFloat(deltaTime)*speed, y: headSprite.position.y)
            var node = linkedBackgrounds.first
            while node?.next != nil {
                let previous = node
                node = node?.next
                node?.value.position = CGPoint(x: (previous?.value.position.x)!+(previous?.value.frame.width)!,
                                               y: (previous?.value.position.y)!)
            }
            if spriteIsOutOfBounds(sprite: head.value) {
                let node = linkedBackgrounds.remove(node: head)
                linkedBackgrounds.append(value: node)
            }
        }

    }

    private func spriteIsOutOfBounds(sprite: SKSpriteNode) -> Bool {

        let origin = self.scene.frame.width/2

        let outOfBounds = (sprite.position.x+sprite.frame.width/2)<origin

        return outOfBounds
    }
}


You have the code here. Hope you enjoy it!

And that's all folks!! See you in part two!!!