Posted on by

In this week’s Tuesday Tutorial, Corona Ambassador Brent Sorrentino explores the basics of parallax scrolling and walks you through a demo project to implement a customizable touch-and-drag parallax view in your app.

We Live in a Parallax World

Parallax is a broad term with numerous definitions in its application to astronomy, photography, and optics. In a 2-dimensional scrolling simulation, which especially concerns mobile game developers, parallax is used to provide a sense of “distance” between the camera and moving objects that recede back into the simulated space. This method has been used since the 16-bit Nintendo days and it continues to be used in modern apps like “Angry Birds” and “Squids”.

Corona’s usage of display groups as specifically-ordered layers makes parallax scrolling simple to implement, at least in theory — but many developers get confused in the coding stage. This tutorial and demo project aims to address that!

1. Getting Started

The first thing we’ll do is configure our parallax “layers” as display groups. Open a blank project in Corona and copy the following lines into “main.lua”:

--First, declare a local reference variable for the stage.
local stage = display.currentStage

--Declare your parallax layers in back-to-front order and specify their distance ratio.
--This "distanceRatio" sets the layer's scrolling speed in relation to the foreground (1.0) layer.
local back00 = display.newGroup() ; back00.distanceRatio = 0.2
local back01 = display.newGroup() ; back01.distanceRatio = 0.4
local back02 = display.newGroup() ; back02.distanceRatio = 0.6
local back03 = display.newGroup() ; back03.distanceRatio = 0.8
local foreground = display.newGroup() ; foreground.distanceRatio = 1.0

--IMPORTANT NOTE 1: You MUST have one layer set to 1.0 ratio, even if it won't contain any objects.
--IMPORTANT NOTE 2: Attach a reference variable named "refGroup" to the stage table and set its
--value to the 1.0 layer from above. This group will be used as the core reference group in
--relation to screen touch-and-drag.
stage.refGroup = foreground

Notice that each parallax layer has a specified “distanceRatio” parameter — this is very important!  We’ll adjust these values to determine the scrolling speed of each layer in relation to 1:1 scrolling (1.0) of the foreground. If a specific layer should be exempted from parallax scrolling, simply leave this parameter undeclared.  For example, in the overall hierarchy of your display group setup, you’ll likely have one or two layers which do not scroll — a UI/button/widget layer for example. Only layers with the “distanceRatio” parameter will scroll within the parallax behavior!

Additional note: the “distanceRatio” parameter can be greater than 1.0 if you wish. Using a value such as 1.4 will provide a layer which moves faster than the foreground layer, for example, a layer of opaque mist which resides between the camera and the foreground.

2. Setting Scroll Limits

In almost every scenario, we’ll need to constrain the boundaries of the “world” — that is, limit how far the user can drag the world in any direction. This is accomplished with the following four variables, each of which we’ll declare as parameters of the stage for easy access later in the project.  Add the following line to your own project now:

stage.xMin = -400 ; stage.xMax = 400 ; stage.yMin = -150 ; stage.yMax = 1000

These four parameters simply limit the world’s boundaries. xMin will typically be a negative value, defining how far to the left the world can scroll. xMax limits the movement to the right, and yMin and yMax limit upward and downward scrolling respectively.

Note that all limits are in pixels, not percentage, and they constrain the foreground 1:1 layer’s movement. Layers behind or in front will not be constrained by these limits.

3. Add Layers to a Containing Table

We’ll now add our parallax layers to a containing table named paraGroups, and that table will be set as a sub-table of the stage. Copy the following lines into your project:

stage.paraGroups = {}
for g=1,stage.numChildren do
  if ( stage[g].distanceRatio ) then stage.paraGroups[#stage.paraGroups+1] = stage[g] end
end

In this code block, we first declare the containing table. Then we loop through all display groups, each of which is a child of the core stage. However, only the layers with a “distanceRatio” parameter are added — all other layers, such as those you wish to exempt from scrolling, will be ignored.

4. Populate the World

In the following code, we’ll set a gradient background and add some sample objects to our world. I won’t go through this line-by-line since your own implementation of a parallax world will vary — just note that we’re adding one object to each parallax layer, but you’ll want to populate your world with many objects and images.

local cX = display.contentWidth/2 ; local cY = display.contentHeight/2

local bkgrad = graphics.newGradient( {42,17,10}, {0,0,0}, "up" )
local bk = display.newRect( 0,0,cX*2,cY*2 ) ; bk:setFillColor( bkgrad ) ; bk:toBack()

local object00 = display.newRect( back00,cX-16,cY-16,32,32 ) ; object00:setFillColor(150,0,50,120)
local object01 = display.newRect( back01,cX-32,cY-32,64,64 ) ; object01:setFillColor(150,20,50,140)
local object02 = display.newRect( back02,cX-48,cY-48,96,96 ) ; object02:setFillColor(150,40,50,160)
local object03 = display.newRect( back03,cX-64,cY-64,128,128 ) ; object03:setFillColor(150,60,50,180)
local foreObject = display.newRect( foreground,cX-80,cY-80,160,160 ) ; foreObject:setFillColor(150,80,50,200)

5. Implementing Touch and Drag

A scrollable world isn’t much good if you can’t actually move it! The following function handles the screen touch “began” and “moved” phases and manipulates all groups in the paraGroups table as you drag around the screen. Please copy this function and touch listener into your project:

local function touchScreen( event )

  local stageID = event.target
  local refGroup = stageID.refGroup ; local paraGroups = stageID.paraGroups
  local eventX = event.x ; local eventY = event.y

  if ( #paraGroups < 1 ) then return end

  if ( event.phase == "began" ) then

    for i=1,#paraGroups do
      paraGroups[i].offsetX = eventX - refGroup.x
      paraGroups[i].offsetY = eventY - refGroup.y
    end

  elseif ( event.phase == "moved" ) then

    local xDif = eventX - refGroup.offsetX
    local yDif = eventY - refGroup.offsetY

    --If you are NOT limiting the boundaries of your world, comment out these 2 lines.
    if ( xDif < stageID.xMin ) then xDif = stageID.xMin elseif ( xDif > stageID.xMax ) then xDif = stageID.xMax end
    if ( yDif < stageID.yMin ) then yDif = stageID.yMin elseif ( yDif > stageID.yMax ) then yDif = stageID.yMax end

    for i=1,#paraGroups do
      local group = paraGroups[i]
      group.x = xDif * group.distanceRatio
      group.y = yDif * group.distanceRatio
      group = nil
    end
    xDif, yDif = nil,nil

  end

  stageID, refGroup, paraGroups, eventX, eventY = nil,nil,nil,nil,nil
  return true

end

stage:addEventListener( "touch", touchScreen )

Again, I won’t walk through this function line-by-line, but I’ll summarize its behavior. In the began phase, we loop through the paraGroups table and set the proper “offset” X/Y values in relation to the foreground reference group. This is necessary to place all layers in a positional reference based on the position of the core 1:1 group.

The moved phase is where the real action occurs. First, we determine the xDif and yDif “delta” values in which to move the reference group, based on where the user started the touch and how far the touch has moved since the previous phase check.

Next, we check if either the resulting X or Y position will be outside the world boundaries declared in Step #2 above. If any limit has been exceeded, we pre-adjust the xDif or yDif value so that the world will never move beyond its limits. As noted in the code, you may comment out these lines if you don’t wish to (or don’t need to) impose any scrolling limits.

Finally, we loop through the paraGroups table and adjust the position of each parallax layer in relation to the distanceRatio value you specified for the group. The foreground group, with a distance ratio of 1.0, will follow/track the user’s touch exactly.  Each layer behind or in front will move at the distance ratio that you specified.  Perhaps the best part is that you can tweak and adjust your ratios in one place (where you declare the groups) and see the result reflected immediately in your app.  After all, setting the proper ratios is based more on “feel” than an exact mathematical calculation, and you’ll need to determine which ratio feels best for each layer depending on the images/object within it and the “implied distance” from the camera.

It’s that simple! We now have a flexible method to implement a scrolling touch-and-drag parallax world with no practical limitation on the number of layers. Adjusting the movement ratio of each layer is equally simple and can be set dynamically from a database or a text file if you wish.

6. Cleaning Up

Don’t forget to clean up your layer references, especially if you’re using a scene manager like Storyboard or Director. In addition to using standard cleanup practices for each respective scene manager, we’ll need to clear out the paraGroups table that we created since it contains references to your parallax display groups, all of which will likely be removed when you change scenes.  Likewise, don’t forget to remove the touch listener, unless you’re implementing this entire method at a global level and intend to use it from scene to scene.

for i=#stage.paraGroups,1,-1 do
  stage.paraGroups[i] = nil
end
stage:removeEventListener( "touch", touchScreen )

Looking Forward!

In a future Tuesday Tutorial, we’ll build upon this method and introduce a convenient zoom feature to dynamically zoom your parallax layers in/out at the proper dimensional ratios.  Until then, please test out and tweak this demo project to your liking, and post your questions and suggestions below.


Posted by . Thanks for reading...

41 Responses to “Tutorial: Parallax Simplified”

  1. Chris Leyton

    Just wanted to say thanks for continuing to provide tutorials on more advanced subjects – exactly what Corona needs.

    Reply
  2. Dave Baxter

    How come everyone uses display.contentWidth/2 instead of display.contentCenterX ?

    Also I think all examples on the blog should be explained with StoryBoard in mind, so we know where to put things.

    Dave

    Reply
  3. Byron

    Any chance you could provide an example or source files? Love to see the tutorial in action… even a movie clip of the results we should expect.

    Great tutorial…. please keep it up!

    B

    Reply
  4. Brent Sorrentino

    Hi Byron,
    I was hoping that readers would follow through the tutorial step-by-step, copying the code blocks to create the entire project. :) However, in the interest of saving time, I’ve created a link to the sample project below… you’ll just need to rename it to “main.lua” and run it in the Simulator. Enjoy!

    https://www.dropbox.com/s/rbpqfbi83vnt22m/parallax-main.lua

    Reply
  5. Brent Sorrentino

    Hi Dave Baxter,
    Thanks for your comments! “display.contentWidth/2″ and “display.contentWidth * 0.5″ are exactly the same as “display.contentCenterX”… it’s a matter of personal preference, nothing more.

    In terms of Storyboard, this sample project is “separate” from that because there are still a vast number of developers who prefer (and use) Director, or don’t necessarily use a scene manager at all. So, I hesitate to put everything into context of Storyboard, as it might be confusing to those who don’t use it. But, your point is valid and when I write future tutorials, I’ll try to design them “Storyboard friendly”… I believe this one is already.

    Reply
  6. Richie G

    Have CoronaLabs got any update on this bug

    14030 – Showstopper – Graphics Jitter

    Most endless runners utilise parallax scrolling, so before you delve into it, maybe check this first..

    Waiting patiently since May
    Richie

    Reply
  7. Brent Sorrentino

    Hello Richie,

    Can you please update me on the method you’re using for this? Are you still using LevelHelper? Are you trying to move dozens of physical objects across the screen by “physical” methods (setting velocity, etc.)? Are you trying to move them by “transition” methods? Constant runtime X/Y position updating? I read through the forum thread and the bug report but it’s hard to tell what the real issue might be.

    I have a parallax 2D side-scroller under development, and there’s no stutter at all, not at 30FPS or 60FPS. The “world” consists of 100% physical objects which move across the screen, and there are dozens of them on the screen at the same time. But, these objects need to be managed (activated and deactivated) very carefully for sheer device performance issues… something which LevelHelper might not do. What I mean is, you won’t be able to have 300 physical objects moving around a simulation, some of them on-screen, many of them off-screen, and expect smooth performance on a mobile device.

    If you’re trying to move your entire parallax world via transitions, then I might suggest there are “better ways” (as in, what I’m doing in my endless runner, using physics methods). Physical objects shouldn’t be moved by transition anyway, if you need collision detection… that has been proven as unreliable within Box2D and its collision detection system.

    All of that being said, I should note to other readers that this parallax module isn’t designed for use in an “endless runner” or huge side-scrolling game. This is better suited for a game/app with “worlds” of a reasonably limited size… something like Angry Birds… where you can scroll around a level but its limits are are not “endless”. An endless runner or side-scroller like Super Mario absolutely requires a different approach!

    Brent

    Reply
    • Richie G

      Hi Brent

      Thanks for your response. You probably found from trawling the forums that this graphics jitter is a common problem which people have tried to get around using the different approaches you mentioned… ie. physics velocity, transitions, :translate() etc etc… .but to no avail.

      I am not using any 3rd party tools.

      Its not a frame drop issues, as my fps is pretty steady… at 30fps… and its not a sluggish scroll.. its more like vibrating display objects as they move. The example attached the bug is a very simple example and shows the symptoms clearly.

      I have had people test my app and they dont necessarily notice it… I do :/

      The response to that bug I came across suggests its an engine issue.. but it has been sitting in the “Whats Next” bug list since May.

      Cheers
      Richie

      Reply
    • Brent Sorrentino

      Hi George,
      Putting together your own parallax world is basically up to you, in terms of content. You can place images (using the image APIs), text, vector shapes, etc. into the proper parallax display groups, and it should work fine. Each developer’s implementation will vary and this tutorial is primarily about laying the “groundwork” for a parallax scrolling environment.

      Reply
      • Julian

        Hi Brent

        I’m also having trouble replacing the NEW RECT with images

        I changed to:
        local object00 = display.newImageRect( imgDir.. “p1_bg_4.png”, 1024, 768 );
        BG_4.x = 512; BG_4.y = 384; BG_4.alpha = 1; BG_4.oldAlpha = 1

        But its not working. What else do I have to change in your code?

        Thanks!

        Reply
  8. Julian

    Hi Brent

    Thanks for the tutorial!
    I’m also having trouble with replacing the RECT with my own images. I tried the follwing:
    local object00 = display.newImageRect( imgDir.. “p1_back00.png”, 1024, 768 );
    back00.x = 1000; back00.y = 384; back00.alpha = 1; back00.oldAlpha = 1

    Do I have to change something else in your code?
    Cheers

    Reply
    • Brent Sorrentino

      Hi Julian,
      Is it showing the image, but the image doesn’t actually scroll? In your code, I see that you’re not inserting the image into one of the parallax display groups… this is essential to the functionality of the module. You can insert images manually using “displayGroup:insert( object )”, or you can simply provide the display group as the FIRST argument in the newImageRect() API (before you specify the location and name of the image).

      Let me know if you still have issues after doing this… thanks!

      Reply
  9. Julian

    Hi Brent
    Thanks for the answer! I tried both ways to insert images into the parallax groups, but still its showing the images without scrolling.

    I’m using kwik2 (photoshop plugin) for my main build. Is it possible that some code from there conflicts with the parallax-module? For example the images are already in another group (menuGroup). I also deleted this group, but no scrolling either…

    heres is my project, if you want to have a look at it

    https://www.dropbox.com/sh/u7dpfxyfxnmbtka/oqnXc3enec

    thanks a lot!

    Reply
    • Brent Sorrentino

      Hi Julian,
      I checked out your file “page_1.lua” and it appears that you’re “cleaning up” the module immediately after you set it up. You do this on Line 54. The cleanup step is outlined as #6 in my tutorial, and it’s necessary to perform it, but not where you currently have it in your code. The idea is to clean up the module when you change scenes in Director or similar.

      Basically what you’re doing in your file is adding a touch sensor (listener) to the screen (stage), then a few lines later, you’re removing that same sensor. This probably explains why nothing is happening. :)

      Reply
      • Julian

        Where exactly do I have to put in the ‘cleanup’? I tried it under Director.lua –> change scenes. But still its not working…

        Reply
        • Brent Sorrentino

          Hi Julian,
          Sorry, but I can’t help specifically here. I haven’t used “core” Director in a long, long time (I use a heavily-modified and bare-bones trimmed down version of it that I created for my own projects). Thus, I don’t know how the current version handles cleanup. Your best option is to ask about this in the Director sub-forum on the Corona site, as there are still MANY users of the Director module and they should know how to help you.

          Reply
        • Adrie

          Hi Jullian,

          Dis you get your Kwik2 problem solved?
          I’m struggling with the same issue.
          I’m also trying to set up a parallax page. I managed to populate Brent’s parallax simplified file with some sample images from another project. That’s working the way I want: made a simple horizontal scroll with layers scrolling in different speed.
          As I like to organize my complete project in Kwik I’m trying to get the same result with layers and groups in Kwik and add external code where needed. I managed to create the layers and by creating groups I get the same groups as in the parallax simplified but now I’m stuck because I don’t quite understand how and where to place the external code in the Director class setup in the Kwik file.

          I’m curious how you dealt with this

          Kind Regards

          A3

          Reply
  10. Julian

    by the way, do you think its possible to add a ‘delayed stop’ like its common with scrolling to the module or is that a totally different approach?

    Reply
    • Brent Sorrentino

      I’m not sure I understand what you mean by this… would that be a sort of “flick the screen” and the motion goes in the direction of the flick, then gradually slows to a stop? It’s not implemented in there now, but that’s a good idea for a future version! I eventually want to add pinch-zoom and 2-finger-rotate to this, but I’m not sure when I’ll get around to it.

      Reply
  11. Julian

    thanks Brent!

    ” “flick the screen” and the motion goes in the direction of the flick, then gradually slows to a stop?”
    thats exactly it. I was just wondering if its possible to add it later with your module or if a totally different aproach would be necessary.

    Reply
    • Brent Sorrentino

      Hi Julian… short answer is “yes”, it would be possible with my module. I’d just need to add quite a bit more functionality like sensing if the motion is a “flick” or a “slow drag”… I already have some of that capability built into my “scrolling slide view” module in the Code Exchange, but I’d have to extract and improve upon it for this parallax thing. It’s an interesting concept but I’m not sure how much “demand” there is for it, and thus, I might not get around to it for awhile… is this something you really need for your app, or are you just curious if it’s possible?

      Reply
      • Claude

        Hello Brent!

        Like Julian and Olivier I would really like if you can help us or show us how to modify your module to add the Flick and Slow drag effect to it!

        I’ve been struggling to get it working without success!

        That would be really appreciated!

        Thanks

        Reply
  12. Olivier

    Hi Brent
    Thank you for this great tutorial :)
    I tried without success to add to your example the smooth effect which exist in scrollView widget :
    when you stop drag, the content slows down instead of stopping immediately. (the velocity decrease progressively)
    Could you explain us how to do this in a parallax world like your example?
    Thank you so much
    Cheers
    Olivier

    Reply
  13. Olivier

    Ooops sorry,
    It seems that Julian already asked the same thing…
    So It’s something I’d like to add to my app too…
    Thanks again
    Olivier

    Reply
  14. Olivier

    In fact, it can be seen as a detail but it’s such detail which makes your app really professional…
    Thanks again
    Olivier

    Reply
  15. Julian

    I’ve implemented a ‘magnetic drop area’, lets say “D”, to ‘back02′. Now I have a dragable object “A” which is not part of any parallax group. Is there a way to insert object “A” to group ‘back02′ after it was ‘dropped’ to “D” so thats its moving with the parallax background?

    thanks for any help!

    Reply
  16. Julian

    How can I calculate the relative movement of each group from before and after one scrolling event?

    The only thing I get is the absolut position of each layer, but I couldn’t figure out how to calculate the relative movement.

    Any ideas?

    Reply
    • Brent Sorrentino

      Hi Julian,
      I’ll try to answer both of your questions here:
      For a “magnetic drop” area, yes, you should be able to insert and remove objects from any parallax group, into or out of a group which isn’t part of the parallax scrolling.

      Not sure what you mean by “relative movement”. Are you trying to build a kind of scenario where the user “swipes” the screen (instead of just touch-dragging) and the world has some kind of residual movement, i.e. they can swipe around the world and it slows down gradually after they’ve lifted their finger off? Please explain the usage you’re trying to implement, and hopefully I can help you.

      Reply
  17. Julian

    Hi Brent,

    thanks for your answer! unfortunately I just saw it yesterday…

    Theres a problem with the ‘magnetic drop’. Instead of removing an object from a group and insert it to another, I tried to change the distanceRatio value from a group.

    its working if I switch from a background with a distanceRatio bigger than ’0′ to another bigger than ’0′. But theres a strange behaviour if I drop an object back to a distanceRatio with the value “0″.

    The object is moving to the right position but after starting scrolling the parallax-background the object jumpes quickly to another position.

    Theres another thing: When I test the parallax-module on a device it behaves strangely when touching the screen with two fingers (jumps forth and back). Is it possible to transform this multitouch event to a ‘single-touch’ event?

    I hope its clear enough…
    Thanks!

    Reply
  18. Claude Turcot

    Brent or anybody, did somebody found a way to make a “flick”, “bounce at the end”, “slow scroll” effects when scrolling from left to right to be added in the touchscreen function?

    I’ve been using this function for my game, It’s working really good but I’m missing that iOS effects when I scroll and since I’m almost ready to publish my game I really need some help trying a way to enhance this function to achieve that!

    Can somebody help?

    Thanks!

    Claude

    Reply
    • Brent Sorrentino

      Hi Claude,
      I haven’t yet implemented this, but the process should (in theory) not be too difficult… at least the “slow scroll” functionality. I would recommend that you simply check if the “world” is beyond its limits, then as the user continues to scroll, a fractional percentage of movement is applied. Then, when the user releases (lifts off touch), run a function that transitions the scene back to its limit value, with an easing-out transition so it looks more natural.

      Hope this helps somewhat. I may be able to revisit this module eventually and add more functionality, but I don’t have an ETA on that.

      Brent

      Reply
  19. Radim

    Hello,

    I’m using composer and I’m calling overlay scene which is “game options” scene. However this overlay scene shows bellow parallax layers and I need it above all layers.

    part of my code:

    stage:insert(composer.stage) — current stage – static background
    stage:insert(back) — parallax background
    stage:insert(foreground) — parallax foreground
    stage:insert(scoresView) — score board above all layers

    now I need to insert composer overlay scene above all scene’s layers. How to do that?

    Reply
    • Brent

      Hi Radim,
      Since you’re inserting the composer.stage first, it will reside behind the others. You should declare that at the end of your series of :insert(s), so it remains at the front.

      Reply
  20. Nigel

    Hi, Could you please modify this for pinch zooming ?,
    Ive tried combining this coronalabs.com/blog/2013/01/22/implementing-pinch-zoom-rotate/
    but the layers dont seem to behave correctly, Be great for any info.

    Thanks

    Reply

Leave a Reply

  • (Will Not Be Published)