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.
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.
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:
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.
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:
❤️ Enjoying this post so far?
If you find this content useful, consider showing your appreciation by buying me a coffee using the button below 👇.
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.
You can get the full Xcode from my Github repository.
36 Comments
Link DeTestare
Your mode of describing the whole thing in this
piece of writing is in fact pleasant, all be
capable of without difficulty understand it, Thanks a lot.
Mike
Thanks for the code. On your code you have this kParallaxBackgroundAntiFlickeringAdjustment constant. Can you explain what it does? Thanks.
nacho
Hi Mike,
It is all explained in this post: http://digitalleaves.com/blog/2014/01/the-pains-of-being-a-floating-point-number/
Basically, it is a constant that corrects possible errors due to floating point representation of the position of the backgrounds.
Mike
Fantastic explanation, thanks!
Uwe
Is it iPad-only?
In Xcode, no iPhone-simulator is visible?
Thanks!
nacho
It works for iPhone, iPad and iPod. The example code is iPad only. I suggest you to read the usage section of the class at my github: https://github.com/DigitalLeaves/ParallaxBackground
ask
good afternoon.? I am a beginner in xcode. how to remodel your parallax for iphone landscape. thank you very much.
nacho
Hello,
You just have to follow the instructions of the "Usage" section of my github page: https://github.com/DigitalLeaves/ParallaxBackground
If you have any concrete question please let me know.
andrew
Good afternoon. as a force the size and position of the array of images? Thank you.
nacho
Hello,
I'm afraid I did not completely understand your question. The size of the parallax background is equal to the size of the background images. All background images must be the same size.
Best!
andrew
the matter is that if you run your parallax images with a size 3000x2000 emulator siphon in the landscape, the images enlarged and not amenable to reduce the size of the screen. I am a beginner and do not understand how little they fit the screen size. thank you
nacho
If you are working on a iPhone simulator, your images should be 640x1136 / 640x960 or viceversa, no 3000x2000. If you are a beginner I suggest you to start with a SpriteKit without parallax and build it later when you have a deeper understanding of the system.
andrew
Reduce the size of pictures. 0 effect. they increased! how to make the original size?
nacho
Sorry, I am not teaching you how to program. Learn a little more, then follow the usage guide, watch the example, and you will eventually make it work. Best of lucks.
Simon
Thanks for this very useful class and example. I enjoyed the tutorial, but also enjoyed your conversation with the numpty who seems to want you to teach him how to code. lol ;)
nacho
Thanks to you, Simon. Yes, I try to be helpful, but sometimes it bothers me when people seem to ask us to give them the code directly, without even trying to learn and improve as developers.
HMFifty
I'd like to add subnodes to one of the parallax nodes just before the parallax node is actually visible. Where would you suggest I implement this? Ie. I have skphysics bodies that that I want the player to interact with on the parallax layer.
nacho
Hello,
I would probably implement a new initialization method in the PBParallaxScrolling class. You could send this method an NSDictionary of SKSpriteNodes, where the keys would be the parallax layer number, and the content would be an NSArray with the SKSpriteNodes. This initWith... andAttachedObjects: method would add this SKSpriteNodes to the appropriate layer. Don't forget to give a physic body to those objects (no gravity, but a contact and collision mask) to interact with the player or other nodes in the scene.
I just hope this subnodes you are talking about are not green pipes, though ;)
Good luck.
rgb
Great tutorial.
I have tried the PBParallaxScrolling classes and works as described.
Before adding any parallax effect, the app is running at 60 fps and everything is working fine. But after adding the parallax effect using the PBParallaxScrolling class, the app is running at 15 fps and it is sluggish.
Any idea or some sort of setting to make it run at a higher fps?
Thanks.
nacho
Never experienced this fps drop you describe. Are you testing this in the simulator?
rgb
Yes i was running it on a simulator and it was slow.
I ran it on an idevice and the fps is correct at 60 so everything is good.
Thanks.
Cameron Frank
First of all: phenomenal class! Great work. By far the best, more straightforward parallax implementation I've come across. I am having one weird thing though. When I'm using @2x images, it seems like the cloned image isn't appearing in the scene until the anchor point of the original image falls off the screen(I'm using the left direction) I feel like one quick fix would be to make the @2x image at least 1280px wide, but I've done a fair amount of graphics work so far, that I'd rather not have to redo haha. Any idea what would cause this? My first thought was to try and move the anchor point of the backgrounds to the bottom left corner, and then adjust their placement accordingly, but I don't want to break your class inadvertently. Any thoughts? Thanks!
Ignacio Nieto Carvajal
Hi Cameron, glad to know you are using the class and finding it useful. That's a weird issue you are facing. The Parallax is configured so that the images must be the same size of the scene, so when the background is about to end, the new one comes from the edge of the scene.
I guess that your problem is that your scene (SKScene) is probably shorter than your images (i.e: scene 1024 width, image 1280 width), so the new image enters midway. Try setting your SKScene size to fit the images size. Hope that helps!
Nguy
Hi there!
I'm working with the simulator with an iPad and iPad - retina. I have found that the scrolling with the iPad works perfectly but it is very choppy when I am using it with the retina. Is this actually an issue or is it just a problem with the simulator?
-Thanks
Ignacio Nieto Carvajal
The simulator doesn't have hardware graphics acceleration, you cannot properly test anything SpriteKit related in the simulator. May I suggest you to learn a little more before actually trying to build a game. Thanks!
Johnathan
Hello,
Will you be covering this tutorial in swift?
Ignacio Nieto Carvajal
Hi, not probably, as adapting the code to swift is really trivial, and you can still use it as is in your swift project. Thanks for your interest.
Johan
Hi,
Thanks a lot for this explanation. You really helped me a lot.
Oliver
Do you know why when I use a larger image the image appears black. At the minute I am using an image with a width of 6000px but I don't know if this is technically allowed.
Ignacio Nieto Carvajal
Hi Oliver. You need to use images with the exact size of the device.
Yash
Seriously greate tutorial on the internet that exactly I want. Thanks for this post.