After being developing for some time with the new 2D game engine SpriteKit, I have to admit that I’m in love with it. I find it really simple to use, easy to learn and very powerful. While developing my new game, I was faced with the need to introduce parallax scrolling to give a sense of depth to the game. I have published a class on Github to easily add Parallax Backgrounds in any SpriteKit game, so if you are, like me, an iOS developer interested in SpriteKit games, here is how you can implement your own parallax effect for your SpriteKit games.

Parallax Scrolling is a 2D effect used to create a fake sense of 3D depth in a 2D game. It is based on a trick of the eye that happens when you are traveling, i.e: in a car. As you move forwards, the closest items seem to move faster than the distant ones, as if there were several layers moving at different speeds, the closer the faster. The parallax effect tries to clone this trick by setting several images acting as background layers, and animating all of them in the same direction, but at different speeds.

parallaxEffect

Now let’s create our own Parallax Scrolling effect for our SpriteKit App. First we will create a new project in Xcode. Choose iOS -> Application -> SpriteKit Game.

parallax1

Pick a name for the project, like “ParallaxBackground” and a prefix for your classes, like “PB”, and Xcode will set the initial environment and needed classes for the development of a SpriteKit app, as show here:

parallax2

First, we need to do some changes to clean and set everything. We need to edit PBViewController and move the code that initializes the SKScene from viewDidLoad (where Xcode originally puts it) to viewWillLayoutSubviews. Why? because in viewDidLoad, our view still has not been presented to screen, so a lot of properties have not been set, like size, for example. It is way better to initialize the scene when our view is ready to be shown and everything has been set. To avoid the view appearing on the screen and then initializing the SKScene with a noticeable flash, we will use viewWillLayoutSubviews (called prior to viewDidAppear) instead of viewDidAppear. So our PBViewController will be like this:

- (void)viewDidLoad
{
    [super viewDidLoad];
}

- (void) viewWillLayoutSubviews
{
    [super viewWillLayoutSubviews];

    // Configure the view.
    SKView * skView = (SKView *)self.view;
    if (!skView.scene) { // because viewWillLayoutSubviews is called twice!
        skView.showsFPS = YES;
        skView.showsNodeCount = YES;

        // Create and configure the scene.
        SKScene * scene = [[PBMyScene alloc] initWithSize:skView.bounds.size];
        scene.scaleMode = SKSceneScaleModeAspectFill;

        // Present the scene.
        [skView presentScene:scene];
    }
}

Now we should delete the Spaceship.png and edit the touchesBegan:withEvent: method, so no spaceship is shown if we touch the screen. Then, we can start writing our parallax scrolling code.

We will start by creating a class that will represent our Parallax Scrolling entity. This entity will have several backgrounds, from foreground to background. To create the illusion of a never-ending movement, we will clone each background and put this copy next to the original. If we design our backgrounds in a way that the edges of the image create a sense of continuity, and we move them in the same direction at the same speed, the image will be seen as a continuous flow. Thus, we will have two identical images together moving as a unity for each background layer, and we will make sure that when a image escapes completely from the scene from one of the sides, we reset its coordinates so it is “entering” the scene again.

movement

Thus, our class will have a NSArray of backgrounds and a NSArray of cloned backgrounds, and will combine them to create the parallax layers. As each layer will move at a different speed. We will also specify a direction for the moving images, and a global size (that will be the size of the scene). We add a new Objective-C class and select as superclass SKSpriteNode. We will call this class PBParallaxScrolling. Then, in PBParallaxScrolling.m, we will add a private interface with all the properties:

 

@interface PBParallaxScrolling ()

/** The array containing the set of SKSpriteNode nodes representing the different backgrounds */
@property (nonatomic, strong) NSArray * backgrounds;

/** The array containing the set of duplicated background nodes that will appear when the background starts sliding out of the screen */
@property (nonatomic, strong) NSArray * clonedBackgrounds;

/** The array of speeds for every background */
@property (nonatomic, strong) NSArray * speeds;

/** Number of backgrounds in this parallax background set */
@property (nonatomic) NSUInteger numberOfBackgrounds;

/** The movement direction of the parallax backgrounds */
@property (nonatomic) PBParallaxBackgroundDirection direction;

/** The size of the parallax background set */
@property (nonatomic) CGSize size;

@end

The PBParallaxBackgroundDirection will define the direction for the movement of the backgrounds. We will define it on PBParallaxScrolling.h:

typedef enum {
   kPBParallaxBackgroundDirectionUp = 0,
   kPBParallaxBackgroundDirectionDown,
   kPBParallaxBackgroundDirectionRight,
   kPBParallaxBackgroundDirectionLeft
} PBParallaxBackgroundDirection;

Depending on the direction, we will apply different values to the coordinates to move and reset our backgrounds. Next, we will define the two methods needed for our class: one for initializing the parallax scrolling and another one for moving it. We do this on PBParallaxScrolling.h:

- (id) initWithBackgrounds: (NSArray *) backgrounds size: (CGSize) size direction: (PBParallaxBackgroundDirection) direction fastestSpeed: (CGFloat) speed andSpeedDecrease: (CGFloat) differential;

- (void) update: (NSTimeInterval) currentTime;

And implement them on PBParallaxScrolling.h. The method initWithBackgrounds:size:direction:fastestSpeed:andSpeedDecrease: (wow!) will receive all the needed params to initialize the parallax. We will allow a NSArray of backgrounds containing either NSStrings (with the name of the images to be used), UIImages, SKTexture or the SKSpriteNodes. We will convert each of these into SKSpriteNodes to be added to our class.

for (id obj in backgrounds) {

   // determine the type of background
   SKSpriteNode * node = nil;

   if ([obj isKindOfClass:[UIImage class]]) {
      node = [[SKSpriteNode alloc] initWithTexture:[SKTexture textureWithImage:(UIImage *) obj]];
   } else if ([obj isKindOfClass:[NSString class]])  {
      node = [[SKSpriteNode alloc] initWithImageNamed:(NSString *) obj]; 
   } else if ([obj isKindOfClass:[SKTexture class]]) {
      node = [[SKSpriteNode alloc] initWithTexture:(SKTexture *) obj];
   } else if ([obj isKindOfClass:[SKSpriteNode class]]) {
      node = (SKSpriteNode *) obj;
   } else continue;
   ...
   calculate node position, create clon and adjust clon's position.
}

Now we must determine the node’s position and create a duplicate to attach to the original background. Depending on the direction of the movement, we will place this duplicate in one of the edges.

// create the duplicate and insert both at their proper locations.
node.zPosition = self.zPosition - (zPos + (zPos * bgNumber));
node.position = CGPointMake(0, self.size.height);

SKSpriteNode * clonedNode = [node copy];
CGFloat clonedPosX = node.position.x, clonedPosY = node.position.y;

switch (direction) { // calculate clone's position
   case kPBParallaxBackgroundDirectionUp:
      clonedPosY = 0;
      break;
   case kPBParallaxBackgroundDirectionDown:
      clonedPosY = node.size.height * 2;
      break;
   case kPBParallaxBackgroundDirectionRight:
      clonedPosX = - node.size.width;
      break;
   case kPBParallaxBackgroundDirectionLeft:
      clonedPosX = node.size.width;
      break;
   default:
      break;
}

Now we need to set the speeds for the node and the cloned node. Going from foreground (the closest layer) to background (the most distant layer) means we need to decrease the speed of every layer by a measure. We will use the speed decrease differential to reduce the speed between 0% (no speed reduction) when differential=0 to 100% (half speed) when differential=1.

// add the velocity for this node and adjust the next current velocity.
[spds addObject:[NSNumber numberWithFloat:currentSpeed]];
currentSpeed = currentSpeed / (1 + differential);

So now we just need to add this two nodes to the scene.

// add to the scene
[self addChild:node];
[self addChild:clonedNode];

We will do this for every background, creating all the layers. Then, we would need to update the positions of the backgrounds on every iteration of the game loop. The method will, depending on the direction of the movement, adjust the position for every background and its clon:

- (void) update:(NSTimeInterval)currentTime {
   for (NSUInteger i = 0; i < self.numberOfBackgrounds; i++) {
      // determine the speed of each node
      CGFloat speed = [[self.speeds objectAtIndex:i] floatValue];

      // adjust positions
      SKSpriteNode * bg = [self.backgrounds objectAtIndex:i];
      SKSpriteNode * cBg = [self.clonedBackgrounds objectAtIndex:i];
      CGFloat newBgX = bg.position.x, newBgY = bg.position.y, newCbgX = cBg.position.x, newCbgY = cBg.position.y;

      // position depends on direction.
      switch (self.direction) {
         case kPBParallaxBackgroundDirectionUp:
            newBgY += speed;
            newCbgY += speed;
            if (newBgY >= (bg.size.height * 2)) newBgY = -(bg.size.height * 2);
            if (newCbgY >= (cBg.size.height * 2)) newCbgY = -(cBg.size.height * 2);
            break;

         case kPBParallaxBackgroundDirectionDown:
            newBgY -= speed;
            newCbgY -= speed;
            if (newBgY <= 0) newBgY += (bg.size.height * 2);
            if (newCbgY <= 0) newCbgY += (cBg.size.height * 2);
            break;

         case kPBParallaxBackgroundDirectionRight:
            newBgX += speed;
            newCbgX += speed;
            if (newBgX >= bg.size.width) newBgX -= 2*bg.size.width;
            if (newCbgX >= cBg.size.width) newCbgX -= 2*cBg.size.width;
            break;

         case kPBParallaxBackgroundDirectionLeft:
            newBgX -= speed;
            newCbgX -= speed;
            if (newBgX <= -bg.size.width) newBgX += 2*bg.size.width;
            if (newCbgX <= -cBg.size.width) newCbgX += 2*cBg.size.width;
            break;
         default:
            break;
      }

      // update positions with the right coordinates.
      bg.position = CGPointMake(newBgX, newBgY);
      cBg.position = CGPointMake(newCbgX, newCbgY);
   }
}

We will need to call to PBParallaxScrolling’s update in our PBMyScene update’s method:

-(void)update:(CFTimeInterval)currentTime {
   [self.parallaxBackground update:currentTime];
}

The resulting flow will appear as if we are moving in a 3D environment.

parallax3

You can get the full Xcode from my Github repository.