Lesson 19: Layers and Parallax

Adding layers to a game gives it the illusion depth and a feeling of being larger than it is. A layer can be thought of as an area that is either in front of or behind other areas. This is typically achieved by creating images that are displayed in a specified order that hides or reveals certain aspects of the game. There are a few different methods of incorporating layers into a game. Let's start off with the image layer method.

Image Layering

The most common method of layering is to use images that are displayed in a coordinated manner. Each image layer is typically the same size as the overall game screen. In the following example three separate image layers are used. The first layer is the sky and clouds image which is the farthest layer away from the game player. This layer image will be drawn first and acts as the backdrop for the game's scene. The second layer image contains pillars and will be drawn second. Anything that needs to be seen behind the pillars will be drawn before the pillar layer image. This will give the illusion of those objects being farther away from the player. The third image layer contains the ground and a bush in the foreground.

The flying birds are drawn in between the layers to give them a sense of depth. The birds are also drawn smaller as they recede into the distance once again adding the to illusion of depth. By keeping track of when to display layers and when to draw sprites in between them a world with a simulated distance to the background can be created.

( This code can be found at .\tutorial\Lesson19\Layers.bas )

'** Demo Layers
'** Bird sprites: https://www.spriters-resource.com/fullview/123364/
'** Layer images: https://en.wikipedia.org/wiki/Parallax_scrolling

TYPE BIRD '                  bird definition
    x AS INTEGER '           X location of bird
    y AS INTEGER '           Y location of bird
    image AS INTEGER '       current bird image
END TYPE

DIM Bird(3) AS BIRD '        bird array
DIM Sky AS LONG '            image of sky
DIM Ground AS LONG '         image of ground
DIM Pillars AS LONG '        image of pillars
DIM BirdSheet AS LONG '      bird sprite sheet
DIM BirdSprite(19) AS LONG ' 20 individual bird sprites
DIM Count AS INTEGER '       generic counter

RANDOMIZE TIMER '                                                 seed RND generator
Sky = _LOADIMAGE(".\tutorial\Lesson19\sky.png", 32) '             load sky image
Ground = _LOADIMAGE(".\tutorial\Lesson19\tground.png", 32) '      load ground image
Pillars = _LOADIMAGE(".\tutorial\Lesson19\tpillars.png", 32) '    load pillars image
BirdSheet = _LOADIMAGE(".\tutorial\Lesson19\bird64x72.png", 32) ' load sprite sheet
FOR Count = 0 TO 19 '                                             cycle through sprite sheet images
    BirdSprite&(Count) = _NEWIMAGE(64, 72, 32) '                  create sprite holder
   _PUTIMAGE , BirdSheet, BirdSprite(Count), (Count * 64, 0)-(Count * 64 + 63, 71) ' get sprite from sheet
NEXT Count
FOR Count = 1 TO 3 '                                              cycle through bird array
    Bird(Count).x = 640 '                                         set bird X location
    Bird(Count).y = 320 '                                         set bird Y location
    Bird(Count).image = INT(RND(1) * 20) '                        start with random image
NEXT Count%
SCREEN _NEWIMAGE(640, 480, 32) '                                  enter graphics csreen
_DELAY .25 '                                                      give window time to spawn
_SCREENMOVE _MIDDLE '                                             move window to center of desktop
_TITLE "Layers" '                                                 give window a title
DO '                                                              begin main loop
   _LIMIT 30 '                                                    30 frames per second
   CLS '                                                          clear screen
   _PUTIMAGE , Sky '                                              display first layer then bird
   _PUTIMAGE (Bird(1).x, Bird(1).y)-(Bird(1).x + 31, Bird(1).y + 35), BirdSprite(Bird(1).image) ' 1/2 size
   _PUTIMAGE , Pillars '                                          display second layer then bird
   _PUTIMAGE (Bird(2).x, Bird(2).y)-(Bird(2).x + 47, Bird(2).y + 53), BirdSprite(Bird(2).image) ' 3/4 size
   _PUTIMAGE , Ground '                                           display third layer then bird
   _PUTIMAGE (Bird(3).x, Bird(3).y), BirdSprite(Bird(3).image) '                                  full size
   FOR Count = 1 TO 3 '                                           cycle through bird array
        Bird(Count).x = Bird(Count).x - Count '                   move bird to left
       IF Bird(Count).x < -64 THEN Bird(Count).x = 640 '          start at right side when left reached
        Bird(Count).image = Bird(Count).image + 1 '               go to next image
       IF Bird(Count).image = 20 THEN Bird(Count).image = 0 '     go back to first image when last reached
   NEXT Count
   _DISPLAY '                                                     update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                         leave loop if ESC pressed
_FREEIMAGE Sky '                                                  remove images from memory
_FREEIMAGE Ground
_FREEIMAGE Pillars
_FREEIMAGE BirdSheet
FOR Count = 0 TO 19
   _FREEIMAGE BirdSprite(Count)
NEXT Count
SYSTEM '                                                          return to operating system

Figure 1: A layered world

As you can see the effect is fairly simple to achieve. The first layer image, the sky, contains no transparent areas since we want it to cover the entire scene. Once the sky layer has been drawn any sprites or objects that need to be the furthest away are then drawn. Lines 40 and 41 display the sky image and then a flying bird sprite drawn at half size. Not only will this bird be furthest away but by drawing it at half size adds to the illusion of distance.

The pillars image layer comes next. It contains a transparent area that includes everything except the pillars themselves. When our first bird drawn flies past the pillars the pillars will be drawn over the bird making it appear the bird is behind the pillars. Once the pillar layer is drawn any objects or sprites that should appear to move in front of the pillars need to be drawn. Lines 42 and 43 draw the pillar image and then a flying bird sprite is drawn at three quarters size. This bird will now appear closer than the first bird sprite that was drawn.

Finally the ground image layer is drawn. Once again, it contains a transparent area that encompasses everything except for the ground and bush. Another bird sprite is drawn this time at full size making it appear to be in front of everything.  Notice also that the speed of each bird is varied depending on its distance. The farther away a moving object is the slower it moves which adds even more realism to the depth illusion.

Sprite Layering

Another way to layer images is to assign layer information to sprites. In the following example each sprite has been assigned a layer number it belongs to. Layer one is closest to the player while layer five is furthest away. By drawing the sprites from layer five to layer one the sprites closer to the player are guaranteed to be drawn over sprites further away. Also, as shown in the example, varying the size of the sprites according to their layer also adds to the illusion of depth on the screen.

( This code can be found at .\tutorial\Lesson19\SpriteLayers.bas )

'** Demo Sprite Layers
'** Bird sprites: https://www.spriters-resource.com/fullview/123364/
'** Sky image: https://en.wikipedia.org/wiki/Parallax_scrolling

CONST BIRDS = 100 '       number of birds on screen

TYPE BIRD '                  bird definition
    x AS INTEGER '           X location of bird
    y AS INTEGER '           Y location of bird
    xdir AS INTEGER '        X direction of bird
    layer AS INTEGER '       layer bird is on
    image AS INTEGER '       current bird image
END TYPE

DIM Bird(BIRDS) AS BIRD '    bird array
DIM Sky AS LONG '            image of sky
DIM BirdSheet AS LONG '      bird sprite sheet
DIM BirdSprite(19) AS LONG ' 20 individual bird sprites
DIM Count AS INTEGER '       generic counter
DIM x2 AS INTEGER '          bird image lower right x
DIM y2 AS INTEGER '          bird image lower right y
DIM Layer AS INTEGER '       layer counter

RANDOMIZE TIMER '                                                         seed RND generator
Sky = _LOADIMAGE(".\tutorial\Lesson19\sky.png", 32) '                     load sky image
BirdSheet = _LOADIMAGE(".\tutorial\Lesson19\bird64x72.png", 32) '         load sprite sheet
FOR Count = 0 TO 19 '                                                     cycle through sprite sheet images
    BirdSprite(Count) = _NEWIMAGE(64, 72, 32) '                           create sprite holder
   _PUTIMAGE , BirdSheet, BirdSprite(Count), (Count * 64, 0)-(Count * 64 + 63, 71) ' get sprite from sheet
NEXT Count
FOR Count = 1 TO BIRDS '                                                  cycle through bird array
    Bird(Count).layer = INT(RND(1) * 5) + 1 '                             place bird on random layer
   IF Bird(Count).layer MOD 2 = 0 THEN '                                  bird on even layer?
        Bird(Count).xdir = 1 '                                            yes, bird heading right
   ELSE '                                                                 no, on odd layer
        Bird(Count).xdir = -1 '                                           bird heading left
   END IF
    Bird(Count).x = INT(RND(1) * 640) '                                   random X location
    Bird(Count).y = INT(RND(1) * 400) + 20 '                              random Y location
    Bird(Count).image = INT(RND(1) * 20) '                                start with random image
NEXT Count%
SCREEN _NEWIMAGE(640, 480, 32) '                                          enter graphics screen
DO '                                                                      begin main loop
   _LIMIT 30 '                                                            30 frames per second
   _PUTIMAGE , Sky '                                                      display first layer then bird
   FOR Layer = 5 TO 1 STEP -1 '                                           cycle through layers
       FOR Count = 1 TO BIRDS '                                           cycle through bird array
           IF Bird(Count).layer = Layer THEN '                            bird on current layer?
                Bird(Count).image = Bird(Count%).image + 1 '              yes, increment bird image
               IF Bird(Count).image = 20 THEN Bird(Count).image = 0 '     keep image within limits
                Bird(Count).x = Bird(Count%).x + Bird(Count).xdir * (6 - Bird(Count%).layer) ' move bird
               IF Bird(Count).x < -64 OR Bird(Count).x > 640 THEN '       bird off screen?
                    Bird(Count).xdir = -Bird(Count).xdir '                yes, change X direction
                    Bird(Count).layer = Bird(Count).layer - 1 '           bring bird one layer forward
                   IF Bird(Count).layer = 0 THEN Bird(Count).layer = 5 '  keep layer within limits
                    Bird(Count).y = INT(RND(1) * 400) + 20 '              new random Y location for bird
               END IF
                x2 = Bird(Count).x + 64 / Bird(Count).layer '             calculate image box lower right X
                y2 = Bird(Count).y + 72 / Bird(Count).layer '             calculate image box lower right Y
               IF SGN(Bird(Count).xdir) = -1 THEN '                       bird heading left?
                   _PUTIMAGE (Bird(Count).x, Bird(Count).y)-(x2, y2), BirdSprite(Bird(Count).image) ' left
               ELSE '                                                     no, bird heading right
                   _PUTIMAGE (x2, Bird(Count).y)-(Bird(Count).x, y2), BirdSprite(Bird(Count%).image) ' right
               END IF
           END IF
       NEXT Count
   NEXT Layer
   _DISPLAY '                                                             update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                                 leave loop if ESC pressed
_FREEIMAGE Sky '                                                          remove images from memory
_FREEIMAGE BirdSheet
FOR Count = 0 TO 19
   _FREEIMAGE BirdSprite(Count)
NEXT Count
SYSTEM '                                                                  return to operating system

Figure 2: Birds of a feather flock together

Within the TYPE definition on line 11 an integer variable named .layer has been added to assign the layer number each sprite belongs to. In line 32 a random layer is added to each bird sprite created. The FOR...NEXT statement in line 46 then cycles through the layers from 5 to 1. The FOR...NEXT statement in line 47 then cycles through the sprite array and if line 48 detects that the sprite belongs to the current layer it is processed and drawn to the screen. This mechanism ensures that sprites further away are drawn first. As the birds progress through their flights they are brought closer to the player by decreasing their layer number. Using a method such as this you could for instance have enemy ships in the background circling around waiting to come forward and attack the player. Only when a sprite's layer is equal to the player's layer can the two interact.

Parallax Scrolling

Parallax scrolling is used to give the illusion of depth to moving scenery. The process was pioneered by Moon Patrol in 1982 and was revolutionary for its time. I remember the first time I encountered Moon Patrol in the arcade. I was fascinated with the new use of graphics and subsequently spent my entire day waiting in line to play it. In honor of Moon Patrol let's recreate the effect in the next example program.

( This code can be found at .\tutorial\Lesson19\MoonPatrol.bas )

'** Demo Moon Patrol Parallax

TYPE LAYER '            layer definition
    x AS INTEGER '      X location of layer
    y AS INTEGER '      Y location of layer
    xdir AS INTEGER '   X speed and direction of layer
    image AS LONG '     layer image
END TYPE

DIM Layer(3) AS LAYER ' array of 3 layers
DIM Music AS LONG '     Moon Patrol arcade background music
DIM c AS INTEGER '      layer counter

Layer(1).image = _LOADIMAGE(".\tutorial\Lesson19\moonpatrol1.png", 32) ' load layer images
Layer(2).image = _LOADIMAGE(".\tutorial\Lesson19\moonpatrol2.png", 32)
Layer(3).image = _LOADIMAGE(".\tutorial\Lesson19\moonpatrol3.png", 32)
Music = _SNDOPEN(".\tutorial\Lesson19\MoonPatrol.ogg")
Layer(1).xdir = -1 '                                                     set layer direction and speed
Layer(2).xdir = -2
Layer(3).xdir = -4
Layer(1).y = 95 '                                                        set layer Y position
Layer(2).y = 127
Layer(3).y = 191
SCREEN _NEWIMAGE(256, 256, 32) '                                         enter graphics screen
_SNDPLAY Music '                                                         play background music
DO '                                                                     begin main loop
   _LIMIT 15 '                                                           15 frames per second
   CLS '                                                                 clear screen
    c = 0 '                                                              reset layer counter
   DO '                                                                  cycle through layers
        c = c + 1 '                                                      increment layer counter
       _PUTIMAGE (Layer(c).x, Layer(c).y), Layer(c).image '              place layer on screen
        Layer(c).x = Layer(c).x + Layer(c).xdir '                        move layer to the left
       IF Layer(c).x < -256 THEN Layer(c).x = Layer(c).x + 256 '         reset layer when end reached
   LOOP UNTIL c = 3 '                                                    leave when all layers placed
   _DISPLAY '                                                            update screen with changes
LOOP UNTIL _KEYDOWN(27) '                                                leave when ESC key pressed
_FREEIMAGE Layer(1).image '                                              remove layer images from memory
_FREEIMAGE Layer(2).image
_FREEIMAGE Layer(3).image
IF _SNDPLAYING(Music) THEN _SNDSTOP Music '                              stop music if playing
_SNDCLOSE Music '                                                        remove sound from memory
SYSTEM '                                                                 return to operating system

Figure 3: Moon Patrol Memories

The background blue mountains and foreground green mountains are 512x128 pixel image strips. Each of the mountain images also contains a transparency layer to allow the image behind them to show through. The ground image strip was actually created procedurally by the game to allow for obstacles such as holes to appear. In this example the ground strip is a 512x64 pixel image strip used to complete the look of Moon Patrol's screen. The image strips are moved across the screen at varying speeds giving the parallax effect you see in the demo. Figure 4 below graphically demonstrates this process.

Figure 4: The parallax effect

This example is as simple as parallax scrolling gets. The image strips are twice as wide as the screen. When the image strip's X coordinate reaches -256 the strip has run the entire course. Resetting the image strip's X coordinate back to 0 continues the illusion of a never ending scene scrolling by. If you look closely you'll see that each mountain strip image is actually two of the same image side by side. Instead of placing two images side by side as this example does a single image can be used and a copy placed by its side instead. The following example will revisit the image layers used in the image layering example to scroll the sky, pillars, and ground to the right and left by using the ARROW keys.

( This code can be found at .\tutorial\Lesson19\Parallax.bas )

'** Demo Parallax Scrolling
'** Layer Images: https://en.wikipedia.org/wiki/Parallax_scrolling

DIM Sky AS LONG '         handle to hold image of sky
DIM Ground AS LONG '      handle to hold image of ground
DIM Pillars AS LONG '     handle to hold image of pillars
DIM SkyX AS INTEGER '     sky image x location
DIM GroundX AS INTEGER '  ground image x location
DIM PillarsX AS INTEGER ' pillars image x location

Sky = _LOADIMAGE(".\tutorial\Lesson19\sky.png", 32) '          load sky image
Ground = _LOADIMAGE(".\tutorial\Lesson19\tground.png", 32) '   load ground image
Pillars = _LOADIMAGE(".\tutorial\Lesson19\tpillars.png", 32) ' load pillars image
SCREEN _NEWIMAGE(640, 480, 32) '                               create graphics screen
DO
   _LIMIT 60
   CLS '                                       clear screen
   _PUTIMAGE (SkyX, 0), Sky '                  place sky image
   _PUTIMAGE (SkyX - 640, 0), Sky '            place second sky image
   _PUTIMAGE (PillarsX, 0), Pillars '          place pillars image
   _PUTIMAGE (PillarsX - 640, 0), Pillars '    place second pillars image
   _PUTIMAGE (GroundX, 0), Ground '            place ground image
   _PUTIMAGE (GroundX - 640, 0), Ground '      place second ground image
   IF _KEYDOWN(19200) THEN '                   left arrow key down?
        SkyX = SkyX - 1 '                      yes, decrement sky x position
       IF SkyX = -1 THEN SkyX = 639 '          keep image within limits
        PillarsX = PillarsX - 2 '              decrement pillars x position
       IF PillarsX = -2 THEN PillarsX = 638 '  keep image within limits
        GroundX = GroundX - 4 '                decrement ground x position
       IF GroundX = -4 THEN GroundX = 636 '    keep image within limits
   END IF
   IF _KEYDOWN(19712) THEN '                   right arrow key down?
        SkyX = SkyX + 1 '                      yes, increment sky x position
       IF SkyX = 640 THEN SkyX = 0 '           keep image within limits
        PillarsX = PillarsX + 2 '              increment pillars x position
       IF PillarsX = 640 THEN PillarsX = 2 '   keep image within limits
        GroundX% = GroundX% + 4 '              increment ground x position
       IF GroundX = 640 THEN GroundX = 4 '     keep image within limits
   END IF
   _DISPLAY '                                  update screen with changes
LOOP UNTIL _KEYDOWN(27) '                      leave loop if ESC pressed
_FREEIMAGE Sky '                               remove images from memory
_FREEIMAGE Ground
_FREEIMAGE Pillars
SYSTEM '                                       return to Windows

Figure 5: Parallax scrolling

As you can see in lines 18 through 23 two identical images are drawn side by side allowing the same image to repeat as the parallax motion continues. You get the same effect as with the Moon Patrol demo but this time the images are the same width as the screen.