Parallax scrolling for iOS with Swift and Sprite Kit

Learn how to add this popular visual effect to your iOS project.

Up until the mid 1990s, the pinnacle of video game graphics was parallax scrolling: the use of multiple scrolling backgrounds, which created a sense of depth and perspective in the game. When you’re being a 2D game in Sprite Kit, you can create this effect by creating multiple sprites, and managing their position over time.

In this example, we’re creating a scene where there are four components, listed in order of proximity:

  • A dirt path
  • Some nearby hills
  • Some further distant hills
  • The sky

You can see the final scene below:


The final parallax scrolling scene.

The final parallax scrolling scene.

In this scene, we’ve drawn the art so that each of these components is a separate image. Additionally, each of these images can tile horizontally without visible edges. The art has been put in a texture atlas. The names of the textures for each of the components are Sky.png, DistantHills.png, Hills.png, and Path.png (the components are shown below).

The components of the parallax scene. Note that all four components can tile horizontally.

The components of the parallax scene. Note that all four components can tile horizontally.

With that out of the way, here’s the source code for the SKScene that shows these four components scrolling horizontally at different speeds:


class ParallaxScene: SKScene {

    // Sky
    var skyNode : SKSpriteNode
    var skyNodeNext : SKSpriteNode

    // Foreground hills
    var hillsNode : SKSpriteNode
    var hillsNodeNext : SKSpriteNode

    // Background hills
    var distantHillsNode : SKSpriteNode
    var distantHillsNodeNext : SKSpriteNode

    // Path
    var pathNode : SKSpriteNode
    var pathNodeNext : SKSpriteNode

    // Time of last frame
    var lastFrameTime : NSTimeInterval = 0

    // Time since last frame
    var deltaTime : NSTimeInterval = 0

    override init(size: CGSize) {

        // Prepare the sky sprites
        skyNode = SKSpriteNode(texture: 
            SKTexture(imageNamed: "Sky"))
        skyNode.position = CGPoint(x: size.width / 2.0,
            y: size.height / 2.0)

        skyNodeNext = skyNode.copy() as! SKSpriteNode
        skyNodeNext.position =
            CGPoint(x: skyNode.position.x + skyNode.size.width,
                y: skyNode.position.y)

        // Prepare the background hill sprites
        distantHillsNode = SKSpriteNode(texture: 
            SKTexture(imageNamed: "DistantHills"))
        distantHillsNode.position =
            CGPoint(x: size.width / 2.0,
                y: size.height - 284)

        distantHillsNodeNext = distantHillsNode.copy() as! SKSpriteNode
        distantHillsNodeNext.position =
            CGPoint(x: distantHillsNode.position.x +
                distantHillsNode.size.width,
                y: distantHillsNode.position.y)

        // Prepare the foreground hill sprites
        hillsNode = SKSpriteNode(texture: 
            SKTexture(imageNamed: "Hills"))
        hillsNode.position =
            CGPoint(x: size.width / 2.0,
                y: size.height - 384)

        hillsNodeNext = hillsNode.copy() as! SKSpriteNode
        hillsNodeNext.position =
            CGPoint(x: hillsNode.position.x + hillsNode.size.width,
                y: hillsNode.position.y)

        // Prepare the path sprites
        pathNode = SKSpriteNode(texture: 
            SKTexture(imageNamed: "Path"))
        pathNode.position =
            CGPoint(x: size.width / 2.0,
                y: size.height - 424)

        pathNodeNext = pathNode.copy() as! SKSpriteNode
        pathNodeNext.position =
            CGPoint(x: pathNode.position.x +
                pathNode.size.width,
                y: pathNode.position.y)

        super.init(size: size)


        // Add the sprites to the scene
        self.addChild(skyNode)
        self.addChild(skyNodeNext)

        self.addChild(distantHillsNode)
        self.addChild(distantHillsNodeNext)

        self.addChild(hillsNode)
        self.addChild(hillsNodeNext)

        self.addChild(pathNode)
        self.addChild(pathNodeNext)
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("Not implemented")
    }

    // Move a pair of sprites leftward based on a speed value;
    // when either of the sprites goes off-screen, move it to the
    // right so that it appears to be seamless movement
    func moveSprite(sprite : SKSpriteNode,
        nextSprite : SKSpriteNode, speed : Float) -> Void {
        var newPosition = CGPointZero

        // For both the sprite and its duplicate:
        for spriteToMove in [sprite, nextSprite] {

            // Shift the sprite leftward based on the speed
            newPosition = spriteToMove.position
            newPosition.x -= CGFloat(speed * Float(deltaTime))
            spriteToMove.position = newPosition

            // If this sprite is now offscreen (i.e., its rightmost edge is
            // farther left than the scene's leftmost edge):
            if spriteToMove.frame.maxX < self.frame.minX {

                // Shift it over so that it's now to the immediate right
                // of the other sprite.
                // This means that the two sprites are effectively
                // leap-frogging each other as they both move.
                spriteToMove.position =
                    CGPoint(x: spriteToMove.position.x +
                        spriteToMove.size.width * 2,
                        y: spriteToMove.position.y)
            }

        }
    }

    override func update(currentTime: NSTimeInterval) {
        // First, update the delta time values:

        // If we don't have a last frame time value, this is the first frame,
        // so delta time will be zero.
        if lastFrameTime <= 0 {
            lastFrameTime = currentTime
        }

        // Update delta time
        deltaTime = currentTime - lastFrameTime

        // Set last frame time to current time
        lastFrameTime = currentTime

        // Next, move each of the four pairs of sprites.
        // Objects that should appear move slower than foreground objects.
        self.moveSprite(skyNode, nextSprite:skyNodeNext, speed:25.0)
        self.moveSprite(distantHillsNode, nextSprite:distantHillsNodeNext,
            speed:50.0)
        self.moveSprite(hillsNode, nextSprite:hillsNodeNext, speed:100.0)
        self.moveSprite(pathNode, nextSprite:pathNodeNext, speed:150.0)
    }

}

Parallax scrolling is no more complicated than moving some things quickly and other things slowly. In Sprite Kit, the real trick is getting a sprite to appear to be continuously scrolling, showing no gaps.

In this solution, each of the four components in the scene — the sky, hills, distant hills, and path — are drawn with two sprites each: one shown onscreen, and one to its immediate right. For each pair of sprites, they both slide to the left until one of them has moved completely off the screen. At that point, it’s repositioned so it’s placed to the right of the other sprite.

In this manner, the two sprites are leap-frogging each other as they move. You can see the process illustrated below:

The scrolling process.

The scrolling process.

Getting the speed values right for your scene is a matter of personal taste. However, it’s important to make sure that the relationships between the speeds of the different layers makes sense: if you have an object that’s in the foreground and is moving much, much faster than a relatively close background, it won’t look right.

Simulating perspective using parallax scrolling is a great and simple technique, but be careful with it. Your fearless authors wrote this recipe while in the back of a car that was driving down a winding road, and we developed a little motion sickness while testing the source code.

Something to keep in mind! Motion sickness in games, sometimes known as “simulation sickness,” is a real thing that affects many game players around the world. If you’re making a game that simulates perspective — either in a 3D game or a 2D game where you’re faking perspective — make sure you test with as many people as you can find.

tags: , , , , ,