Skip to content

4. Level design and display

Siorki edited this page Sep 28, 2019 · 7 revisions

Pest Control : Weasels

  1. Genesis. From the idea to the design
  2. Architecture and framework
  3. Critter tasks and animation
  4. Level design and display
  5. Minification and size constraints

Procedural drawing

A common scheme in oldschool platform games and other side-scrollers is the use of tiles to draw the game area. Tiles are stored in one big picture, and referenced by index. The scenery is then simply encoded by a series of numbers, each representing a tile. While this approach is perfect for a game that fits on a floppy disk (720 to 880 kb), it might requires too much space for only 13k. Let's get a rough estimate with 32x32 tiles. Each level covers 1000x256 pixels, or 32x8 tiles. With only 16 different tiles, a full scenery would be coded by 128 bytes. Add the size of the picture containing the tiles : 16x32x32 = 16.384 pixels. Even with only 4 colors (2 bitplanes) that is still 4 kb raw, maybe half of that after png compression.

Procedural content generation usually offers the best compression rates and Pest Control : Weasels is no exception to that. Instead of having hand drawn graphics for the tile, the code from the PlayField class draws the scenery on demand. That is, upon loading each level. The level encoding makes no use of tiles, and instead goes for a simpler description : a series of rectangles. The most complex levels uses only seven of them. A parameter describes the style of the drawing.

Style 1 1 : tiled icy blue blocks. Must be a multiple of 32x16 to avoid cutting a block in the middle.

Style 2 2 : grass on top of a layer of rocks, as in some levels of the original Lemmings. The top is not a straight line, the height of the grass shifts at every pixel through a pseudorandom function (guaranteed to yield the same result every time the same block is drawn). The pattern for the rock is a variation of Voronoise by Inigo Quilez.

Style 3 3 : only used in level 10, same as the previous one but with a mountain in the middle.

Traps

In addition to the background, the level description features informations such as number of weasels, objective, time allotted. It also lists the location for entrance and exit portals, plus any other features that get installed from the beginning. Balloons, entrance and exit are actually stored and managed as traps, the same way that landmines, fans or flamethrowers are. A collision between a weasel and a trap's effect area trigger the related code in the World class. It can bring harm to the critter, or give it a balloon, or exit the level, or anything else that can be coded. Multiple entrances and exits (such as level 9) are supported.

Overlay renderering

From the player standpoint, the game screen is divided in two areas, stacked vertically : the play area above, then the information bar featuring the icons, clock and helper text.

Screenshot

The implementation actually uses three layers (as in GIMP or Photoshop) stacked on top of each other. From the background to the foreground, they are :

Screen layers

  • the background, implemented as a css color gradient, with a flat color fallback if the browser does not support it.

  • a canvas containing only the scenery. It covers the whole level (1000x256), not only the visible area, and is scrolled left and right using css. The imageData from this canvas is used directly by the PlayField for all drawing operations : creating the level, digging the ground when a mine explodes, and adding stairs. All collision detections are performed on the same buffer to avoid copy operations. This is the major design choice that lets the game run on mobile or low-end desktop hardware.

  • a canvas covering both the play and information areas. Critters and traps are drawn on this one, at a location that depend on the offset of the scenery (everything must scroll synchronously). The information is kept at a constant location on screen.

Window size

The game was designed to be playable on both computers and smartphones, and thus had to adapt to a wide range of screen resolutions. That proved in practice to be really tricky, the solution is not perfect and may leave a black bar at the bottom of the browser. I first identified the screen resolutions for the most common devices in landscape mode :

  • 480x320 (iPhone)
  • 800x480 (N900, Galaxy S, Galaxy S2, HTC Desire, HTC 8XT)
  • 960x540 (HTC Droid Incredible)
  • 960x640 (iPhone 4)
  • 1136x640 (iPhone 5)
  • 1280x720 (Galaxy S3, HTC One Mini)
  • 1024x768 (iPad, legacy PCs and Macs)
  • 1280x768 (Blackberry Z10, Nexus 4)
  • 1366x768 (laptops with 15" screen)
  • 1600x900 (laptops with 17" screen)
  • 1280x1024 (legacy PCs)
  • 1920x1080 (Nexus 5, Galaxy S4, HTC One, desktop PCs, laptops with 17" screen)
  • 1600x1200 (high-end legacy PCs)
  • 1920x1200 (16/10 screen)
  • 2048x1536 (iPad Retina, high-end legacy PCs)

The possibility to scroll the screen made the actual width of the screen largely irrelevant, as long as it could accomodate all the icons at the bottom, and "feel right" .. that meant wider than higher, hence the landscape display. Because of this, I could ignore the different width-to-height ratio, and focus only on the most common screen heights : 320, 480, 540, 640, 720, 768, 900, 1024, 1080, 1200 and 1536.

There are several possibilitied to adapt to multiple screen sizes. One is "design for the smallest and scale up as needed". It has the drawback to not benefiting from larger resolutions, but in this case this proved not to be a problem.

I wanted to give the game an oldschool look, so a low resolution was not an issue. My intention was also that the zoom had to be pixelated - which is not always a success, see below why - at any resolution, thus the zoom factor had to be a integer. Fractional ratios always produce a blurry result that I wanted to avoid. Upon initializing, the Renderer measures the screen height and computes the zoom factor as the closest multiple of 320 below. With a screen 768 pixels high, the game will display at x2, or 640 pixels high.

Only the available window inner size counts, barring any toolbar / icons / status bar. This is why on some platforms (iPhones in particular, which are an exact multiple of 320 pixels high) I recommend playing fullscreen. This choice is not perfect, on resolutions slightly below a multiple of 320, such as 540 or 900 it will get a large portion of the screen unused, but this is for a lack of a better solution.

The zoom is achieved through css scaling. The canvases are given real sizes, then their css style is set to a multiple thereof.

this.sceneryCanvas.height=256;
this.sceneryCanvas.width=1000;
this.sceneryCanvas.style.width="2000px";
this.sceneryCanvas.style.height="512px";
// paste a 1000x256 image onto the canvas
this.sceneryContext.drawImage(sourceImage, 0, 0, 1000, 256, 0, 0, 1000, 256);

Unfortunately, this technique does not (yet?) allow for pixelated rendering, except with Firefox. Another possibility was to use canvas scaling through the method scale().

this.sceneryCanvas.height=512;
this.sceneryCanvas.width=2000;
this.sceneryCanvas.style.width="2000px";
this.sceneryCanvas.style.height="512px";
// paste a 1000x256 image onto the canvas, with scaling
this.sceneryContext.scale(2,2);
this.sceneryContext.drawImage(sourceImage, 0, 0, 1000, 256, 0, 0, 2000, 512);

This would have allowed for pixelated rendering with the property imageSmoothingEnabled. However it only applies to images blitted onto the canvas with drawImage(). The text would not have been pixelated but drawn with full resolution instead. Additionally, this would not have worked with the canvas containing the scenery : the size of the playfield buffer would have been multiplied by the zoom factor, requiring extra processing in the PlayField class to account for that.

There is still no silver bullet to get pixel art style rendering. Canvas scaling is the best option yet is limited to image blitting.


  1. Genesis. From the idea to the design
  2. Architecture and framework
  3. Critter tasks and animation
  4. Level design and display
  5. Minification and size constraints