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.