Scrolling Games

From GPWiki
Jump to: navigation, search
40px-broken-icon.png   This page has broken markup.
If you fix this problem, please remove this banner.

"Scrolling games" is a bit of a generic name for a wide variety of predominantly 2D games whose primary gameplay involves navigating obstacles and bonuses in a horizontal or vertical (or in rare instances, both) orthogonal scrolling field. This article focuses on such systems realized using traditional 2D raster graphics; see Further Reading for more information.


Scrolling Basics

Viewports and Coordinates

The simplest orthogonal scrolling games have a field as wide (for vertical scrolling games) or high (for horizontal scrolling) as the display area, advancing continuously in one direction. Examples include Raptor: Call of the Shadows, Gradius, and select levels of the NES and Super NES Mario Bros. games. Only a small portion of the overall field is visible at any time, which we will refer to as the viewport. Theoretically, the viewport moves along the field, and we are attached to the viewport. The implication of this is that we effect scrolling by changing viewport position, which is defined in terms of a coordinate system employed by all objects. This world coordinate system conveniently has an origin at the start point of the level and, in the case of single-direction scrolling games, increases in the direction of advance.

Scrolling Viewports.jpg

In the image above, the viewport is at the beginning of the level (with advance being to the right) when its coordinate is zero. As the coordinate value increases, its position is further offset from the left. The unit of this coordinate is entirely implementation dependent; it can be in pixels, for easy one-to-one correspondence between the object model of the level and viewport and the display, but that can constrain you to a single video mode. It is best to decide on an abstract scale, and criteria for choosing a good one will be discussed below.

Advance Velocity

In many scrolling games, the viewport advances at a constant speed. This creates a time pressure, either to eliminate an obstacle or enemy - or obtain a bonus or powerup - before the viewport passes a certain point, or to survive a danger until it is outside the viewport. In other games, the viewport advances based on the player character, with the aim of keeping the character approximately in the middle of the screen. In the former class, the advance velocity of the viewpoint is constant, until the viewpoint is at the maximum coordinate (end of the level). In the latter class, the advance velocity is a function of the user position and velocity. It is not uncommon for such systems to apply a little damping to advance velocity, so that the screen does not move jerkily in response to erratic player movement.

Varying advance velocity can increase or reduce difficulty - a game with higher advance velocity requires the user to complete challenges within the viewport more quickly, although slowing advance velocity through areas thick with dangers can also make the game more challenging! Experiment with advance velocity in your games to create the right balance of challenge, including changing advance velocity within a level.


Scroll Bounds

Some games restrict the ability to move in a particular direction. For instance, the Mario Bros. games do not allow you to scroll back over covered ground, whereas Yoshi's Island allowed you to walk freely in both directions. Scroll bounds are essentially minima and maxima for the viewport coordinates, so the freedom for the character to move beyond the middle of the screen at the end of a level is due to a scroll bound having been attained, and game logic therefore discarding any further application of advance velocity.

Scroll bounds can be arbitrarily complex, and even implemented as complex polyhedra in games that allow scrolling in multiple directions. We'll cover this later.


Implementing Basic Scrolling

Let us implement a demo with simple horizontal scrolling in both directions. I have supplied a single large bitmap, 2636x480, available at right, that we will use as the background for our demo. Our viewport will be 640x480 pixels.

Code samples in this article use Python and PyGame due to the simplicity of the language syntax - it can be read by even programmers who don't know Python. Remember, the focus remains squarely on the concepts, which should be easy to translate to the language and API of your choice. No object oriented abstraction and so forth is included for that reason. This is instruction code, not production!

Place the image and the source code below in the same directory.

#! usr/bin/env python

import pygame

screen = pygame.display.set_mode((800, 600)) background = pygame.image.load('Scrolling_BasicBackground.jpg').convert()

  1. we render the viewport in a smaller rectangle centered within the screen
  2. to give us room to print statistics.

vpRenderOffset = (80,60) vpStatsOffset = (80,540)

vpCoordinate = 0 minHorzScrollBounds = 0 maxHorzScrollBounds = 1996 # 2636 - 640 advanceVelocity = 0 scrollVelocity = 10

  1. generate an event 30 times a second, and perform simulation update. this
  2. keeps the game running at the same speed in framerate-independent fashion.

UPDATE = pygame.USEREVENT pygame.time.set_timer(UPDATE, int(1000.0/30))

if not pygame.font.get_init():

   pygame.font.init()

arialFnt = pygame.font.SysFont('Arial', 16)

running = True while running:

   for evt in pygame.event.get():
       if evt.type == pygame.QUIT:
           running = False

       elif evt.type == pygame.KEYDOWN:
           if evt.key == pygame.K_LEFT:
               advanceVelocity += -scrollVelocity
           elif evt.key == pygame.K_RIGHT:
               advanceVelocity += scrollVelocity

           elif evt.key == pygame.K_ESCAPE:
               pygame.event.post(pygame.event.Event(pygame.QUIT, {}))

       elif evt.type == pygame.KEYUP:
           if evt.key == pygame.K_LEFT:
               advanceVelocity += scrollVelocity
           elif evt.key == pygame.K_RIGHT:
               advanceVelocity += -scrollVelocity

       elif evt.type == UPDATE:
           vpCoordinate += advanceVelocity
           if vpCoordinate < minHorzScrollBounds:
               vpCoordinate = minHorzScrollBounds
           if vpCoordinate > maxHorzScrollBounds:
               vpCoordinate = maxHorzScrollBounds

   # render
   screen.fill((0,0,0))
   viewport = background.subsurface((vpCoordinate, 0) + (640, 480))
   screen.blit(viewport, vpRenderOffset)
   screen.blit(arialFnt.render('coordinate: %4d' % vpCoordinate, True, (255,255,255)), vpStatsOffset)
   pygame.display.flip()

Play around with scroll bounds, advance velocity and so on - experiment! It's key to learning. For instance, modify the program above so that the user can not scroll left at all - make scroll advance only in one direction - just by modifying how the bounds variables are treated.


Scroll Wrap

If the beginning and end of your bitmap are carefully designed to seamlessly blend into each other, you can wrap the background to create an endless, if repetitive, level. It is simply a matter of setting the viewport coordinate to the minimum when it exceeds the maximum, and to the maximum when it drifts below the minimum. Try modifying the code in the section above to give an endless scroll, just by modifying how the bounds variables are treated. The fact that the background doesn't wrap seamlessly will give you visual feedback, though it's not as cool as seeing the background scroll endlessly. Of course, you can easily create a "wrap-friendly" background: an image that consists of just a horizontal line will wrap perfectly. Give it a shot.


Procedural Backgrounds

Having a single large bitmap that represents your level background is not practical for several reasons:

  1. a large bitmap consumes a lot of memory, relatively speaking, especially if it doesn't include large expanses of solid color that compress easily;
  2. the bitmap is also fixed, which means that you can't have, say, portions of terrain disappear without implementing a decal system that interacts with your collision logic;
  3. every level in your game will require a bitmap of its own, even if they are highly similar; and so on.

Fortunately, we are not constrained to using bitmaps for background representation. A number of techniques exist to enable us procedurally generate our backgrounds, from simple tiling to more advanced generative graphic methods.


Tiled Backgrounds and Levels

Tiling is an extremely powerful and flexible solution to both the memory consumption and diversity problems of bitmapped backdrops, while allowing for an extremely high level of design control. By breaking the entire field into a regular grid and placing smaller individual bitmaps into the cells, a high amount of variation can be created at very little cost. The key to tiling is to assign unique identifiers to each tile, such that the identifier-based representation of the field is extremely compact - a simple text file with a fixed number of characters per line can represent a rectangular grid, for instance - and at runtime an image is assembled by copying the corresponding tile bitmap in place of the identifier.

In older tiling systems, the viewport was designed to have dimensions that were a simple multiple of the tile sizes and movement occurred in tile-sized steps. This simplified implementation, but resulted in slightly jerky motion, regardless of the framerate or other smoothing efforts. In modern systems, an area slightly larger than the viewport is tiled in a temporary composition surface, and then the viewport area is copied to the screen, resulting in smooth scrolling animation. This approach requires careful attention to background composition, as it is possible for the viewport to partially overlap tiles as in the image below:

Scrolling Tile Overlap.jpg

Given tile width wt and viewport width wV, the temporary composition surface must have a horizontal dimension of at least ceil \left ( w_V / w_t \right ).


Building on the earlier sample, the example below tiles a field of equal size to the bitmap in the original (i.e., 2636x480 pixels) using six tiles of 128x144 pixels each and a text file containing five lines of 21 characters. The tiles are taken from Reiner's Tilesets, a fantastic resource for both prototyping/development and final production. It may seem like we're doing more work, but simply by adding more characters to the text file, we can extend the tiled environment indefinitely.

The data file is a simple text file containing 21 integer digits randomly selected from the set [0, 1, 2, 3, 4, 5]. Each digit corresponds to a tile to be placed, resulting in a field that is 21 \times 128 = 2688 pixels wide. Our scroll bounds haven't changed, however, so additional content beyond the 2636 hard limit is never seen. Try modifying the code to automatically determine scroll bounds based on the available data, making your code more flexible. (In general, you want to avoid hard-coding data values; read them in from configuration files or compute them at runtime whenever possible.)

Here is the data file I used:

154215550004411355140
445440213412014505032
301445333151451035124
002544325431145325113
521225045103445054144

And here is the program source, once again in Python:

import pygame import math

screen = pygame.display.set_mode((800, 600))

  1. this sets up a list (dynamic array) where tiles[N] corresponds to TiledScrolling_Tile{N}.jpg

tiles = [pygame.image.load('TiledScrolling_Tile%d.jpg' % n).convert() for n in range(6)] tileWidth, tileHeight = tiles[0].get_size()

  1. the following is roughly equivalent to
  2. outer_temp = []
  3. for s in file('TiledScrolling_Tiledata.txt'): # line-by-line
  4. inner_temp = []
  5. for c in list(s):
  6. inner_temp.append(ord(c) - ord('0'))
  7. outer_temp.append(inner_temp)
  8. tileData = outer_temp
  9. List comprehensions are a fantastic thing. Learn them if you use Python!

tileData = open('TiledScrolling_Tiledata.txt').readlines() tileData = [[ord(c) - ord('0') for c in list(s)] for s in tileData]

vpRenderOffset = (80,60) vpStatsOffset = (80,540)

vpCoordinate = 0 vpDimensions = (640, 480) minHorzScrollBounds = 0 maxHorzScrollBounds = 1996 advanceVelocity = 0 scrollVelocity = 10

numXTiles = int(math.ceil(float(vpDimensions[0]) / tileWidth)) + 1 numYTiles = int(math.ceil(float(vpDimensions[1]) / tileHeight)) tiledBG = pygame.Surface((numXTiles * tileWidth, numYTiles * tileHeight)).convert()

UPDATE = pygame.USEREVENT pygame.time.set_timer(UPDATE, int(1000.0/30))

if not pygame.font.get_init():

   pygame.font.init()

arialFnt = pygame.font.SysFont('Arial', 16)

running = True while running:

   for evt in pygame.event.get():
       if evt.type == pygame.QUIT:
           running = False

       elif evt.type == pygame.KEYDOWN:
           if evt.key == pygame.K_LEFT:
               advanceVelocity += -scrollVelocity
           elif evt.key == pygame.K_RIGHT:
               advanceVelocity += scrollVelocity

           elif evt.key == pygame.K_ESCAPE:
               pygame.event.post(pygame.event.Event(pygame.QUIT, {}))

       elif evt.type == pygame.KEYUP:
           if evt.key == pygame.K_LEFT:
               advanceVelocity += scrollVelocity
           elif evt.key == pygame.K_RIGHT:
               advanceVelocity += -scrollVelocity

       elif evt.type == UPDATE:
           vpCoordinate += advanceVelocity
           if vpCoordinate < minHorzScrollBounds:
               vpCoordinate = minHorzScrollBounds
           if vpCoordinate > maxHorzScrollBounds:
               vpCoordinate = maxHorzScrollBounds

   # render
   screen.fill((0,0,0))
   startXTile = math.floor(float(vpCoordinate) / tileWidth)
   for x in range(startXTile, startXTile + numXTiles):
       for y in range(0, numYTiles):
           tiledBG.blit(tiles[tileData[y][x]], ((x - startXTile) * tileWidth, y * tileHeight))

   screen.blit(tiledBG, vpRenderOffset, (vpCoordinate - (startXTile * tileWidth), 0) + vpDimensions)
   screen.blit(arialFnt.render('coordinate: %4d' % vpCoordinate, True, (255,255,255)), vpStatsOffset)
   pygame.display.flip()

By being more deliberate about which tiles go where, you can create detailed, complex environments even with just a few tiles. Complex tiling systems support multiple layers per tile, allowing you to blend different effects together, as well as partial transparencies to improve the transition from one tile or layer to the next.

As quick implementation note: if you wish to use color key transparency in your tile images, do not use a lossy format like JPEG which dithers large expanses of color. Despite being smaller, it modifies the pixel values enough that your color key will not work properly.

Diamond (Isometric) Tiled Systems

Isometric - or, more properly axonometric tiled systems move away from the rectangular grid we have covered so far and present a system in which each tile represents an arbitrary parallelogram's worth of virtual floor space.

Scrolling axonometric grid equivalence.jpg

While the virtual floor space features no overlap, there is overlap in the bitmap tiles that much represent the axonometric tiles.

Scrolling axonometric tile overlap.jpg

The image above also shows why isometric is the most popular of the axonometric projections: when the axes are not equally foreshortened, diagonally adjacent (in the rectangular grid; laterally adjacent as axonometric tiles, or the green and red/peach tiles in the diagram) tiles are rendered vertically offset, complicating the mathematical model. However, a complete axonometric tiling system will seamlessly accomodate dimetric and isometric tiles.

Because of the overlap in tiles, axonometric projection requires that tiles be rendered in a variant on the Painter's algorithm, from the logical "back" of the scene (top of the screen) to "front". This way, any image portions extending beyond the parallelogram of virtual floor space represented are painted over by "closer" tiles.

Hexagonal Tiled Systems

Generated Backgrounds

Further Reading