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!!!