A Scrolling Tile Game

View Shockwave Demo | Download Source Movie

In the previous tutorial, we looked at creating a scrolling isometric game by creating a large rendered version of the map and then copying a smaller 'view' of this image. In this version, we are going to look at another technique for scrolling tiles that involves only painting the visible tiles directly into the 'view'. We are also going to start introducing more objects into the system: a new 'game display' object for managing our off-screen buffer and painting the buffer to the screen, and well as some overlay objects for painting text to the game display (such as high scores and messages).

Scrolling a rectangular grid of tiles

Like with the previous examples, the 'map' is a list of lists that will look something like this:

Map = [

[tile, tile, tile, tile],

[tile, tile, tile, tile],

[tile, tile, tile, tile],

[tile, tile, tile, tile],

[tile, tile, tile, tile],

[tile, tile, tile, tile],

[tile, tile, tile, tile],

]

In this tutorial - to keep things simple - we are going to assume that the tiles are equally sized rectangles or squares. This means that tiles will not overlap each other.

To place these tiles, we loop through the list placing each tile in a row along to the right. When we get to the end of the row, we move down by a a tile-height and start again. The basic routine looks like this

x_offset = 0

y_offset = 0



repeat with y = 1 to MapList.count

  repeat with x = 1 to MapList[y].count

    thisTile = MapList[y][x]

    mappedTile = script("ISO.Tile.Mapped").new(thisTile, x_offset, y_offset)

    MapList[y][x] = mappedTile

    x_offset = x_offset + Tile_Width 

  end repeat

x_offset = 0

y_offset = y_offset + Tile_Height

end repeat

Now, when we actually paint the tiles to the output image, we are only going to paint the tiles that are visible. The number of tiles we will need to draw can be determined like this:

MaxTilesToDrawX = RectOnCanvas.width/Tile_Width +1

MaxTilesToDrawY = RectOnCanvas.height/Tile_Height +1

To paint only the visible tiles, we need to work out the top-left tile that is currently visible. This can be calculated by simply dividing the amount of pixels we have scrolled (MapXi and MapYi) by the width and height of the tiles respectively, and adding 1 to the result (since our list index starts at one, not zero).

-- work out the start and end of the x/y coordinates to draw

startX = (-MapXi/Tile_Width) + 1

startY = (-MapYi/Tile_Height) + 1

endX = Min(MapList[1].count, startX + MaxTilesToDrawX)

endY = Min(MapList.count, startY + MaxTilesToDrawY)



-- now draw the visible tiles

repeat with y = startY to endY

  repeat with x = startX to endX

    thisTile = MapList[y][x]

    thisTile.paint(canvas, MapXi,MapYi)

  end repeat

end repeat

Note that even though we are only painting the visible tiles, if you have 20 rows of 20 tiles visible, that is still 400 copyPixel commands - which will be slow. The trick is to find the right balance between tile size and the number of tiles visible (in the example movie, the canvas is 480×480 pixels and each tile is 48×48 pixels - meaning that there will be one hundred (10×10) copyPixels with each update).

You might also notice that some tiles will appear partly over the edges of the specified rect on the canvas. If we were painting directly to the stage, and we had a nice border around the edges of the map, then this would be a problem. To avoid this, we could either crop the individual tiles that lie along the edges, or else paint to a intermediate 'buffer' image (that is the correct size) and then paint the intermediate image to the final output.

Tiles to buffer to canvas

Any tiles that go over the edges of the buffer will be effectively 'cropped' when painted on to the buffer.

In the example covered in this tutorial, we will use the second approach. The main reason for this is that we will be adding other overlays (images) to the offscreen buffer before rendering the final result.

The Game Display

The script that manages the buffer is fairly simple at this stage.

Parent Script "TileEngine.GameDisplay"



property myCanvas

property myDrect

property myBuffer

property myBufferRect



on Initialise (me, outputImg, outputRect)

  myCanvas = outputImg

  myDRect = outputRect

  myBuffer = image(outputRect.width, outputRect.height, 16)

  myBufferRect =myBuffer.rect

  myBuffer.fill(myBufferRect, rgb(0,0,0))

  myCanvas.copyPixels(myBuffer, myDRect, myBufferRect)

end



on GetBuffer (me)

  return myBuffer

end



on Paint (me)

  myCanvas.copyPixels(myBuffer, myDRect, myBufferRect)

end



on ResetBuffer (me)

  myBuffer.fill(myBufferRect, rgb(0,0,0))

end

To initialise an object created from this script, we supply an output image and a rect to use. The object will then create a 'Buffer' image which is makes available (via the GetBuffer method) to other objects for painting. The Paint method of this object will then paint the buffer image to the output canvas.

Introducing The Game Behaviour

In order to receive events (such as mouseDown and enterframe), we have been using a Behaviour which creates the tile engine and modifies the game state (scrolling the view etc) in response to those events. The game behaviour in this tutorial will also create the Game Display object, as well as the various objects used in displaying overlay images.

Overview of components

Our 'Game Behaviour' which creates the tile game and display objects will look something like this:

Behaviour "GameScript"



property Display

property TileEngine



on beginSprite (me)

  

  -- Create the display object

  Display = script("TileEngine.GameDisplay").new()

  Display.Initialise((the stage).image, rect(0,0, 480, 480))



  -- get a reference to the buffer

  buffer = Display.GetBuffer()

  

  -- Create the TileEngine

  TileEngine = script("TileEngine").new()

  TileEngine.Initialise(buffer, buffer.rect)



  -- Load a map (read from file, etc)

  me.LoadRandomMap()



end

In this example, the output canvas is the stage and the rect we are going to use on the stage is rect(0,0, 480, 480). In this basic example, we are simply going to scroll around the map using arrow keys. Here is the enterframe method of the game behaviour.

on enterframe (me)

   

    -- Handle key input

    moveH = (keyPressed(124) - keyPressed(123))

    moveY = (keyPressed(125) - keyPressed(126))

    if moveH AND moveY then pixDistance = 6.0 -- sqrt(32)

    else pixDistance = 8.0

    ShiftX = moveH * pixDistance

    ShiftY = moveY * pixDistance

    TileEngine.moveView(ShiftX, ShiftY)

    

    -- Tell TileEngine to update the buffer

    TileEngine.Update()

        

    -- finally, paint the buffer to the canvas

    Display.Paint()

 

end

The demo movie also includes some over-lays, including a frames-per-second counter. Both the text overlays use the "text widget" from the OpenWidgetLib. The Lingo to create a FPS counter and tell it to use a TextOverlay looks like this:

  -- Create the FPS counter

  textDisplay = script("TextDisplay.Static.WithShadow").new()

  textDisplay.Initialise(buffer, rect(10,10,150,30),

    [#fontFace:#header, #textcolour: rgb(255,255,255)])

  FPScounter = script("FPS").new()

  FPScounter.Initialise(textDisplay)

When the textDiplay widget is intialised, it is told to use the game buffer as its output image (and it is also give a rect to use on that buffer). The FPSCounter object is initialised and told to use the textDisplay object we just created as its output device. To use the FPS counter, we would modify the enterframe handler above to look like this

on enterframe (me)

   

    -- Handle key input

    moveH = (keyPressed(124) - keyPressed(123))

    moveY = (keyPressed(125) - keyPressed(126))

    if moveH AND moveY then pixDistance = 6.0 -- sqrt(32)

    else pixDistance = 8.0

    ShiftX = moveH * pixDistance

    ShiftY = moveY * pixDistance

    TileEngine.moveView(ShiftX, ShiftY)

    

    -- Tell TileEngine to update the buffer

    TileEngine.Update()



   -- Tell the FPScounter to update its display

  -- (which will update the buffer)

   FPScounter.Update()

        

    -- finally, paint the buffer to the canvas

    Display.Paint()

 

end

Note - for more information on how the 'TextDisplay.Static' script works - see Open Widget Library. For the purposes of the game discussed in this tutorial, the actual implementation of the script is mostly irrelevant. However, it is worth noting that essentially all the Open Widget Library scripts works much like the tile.Engine script - you tell the script what image to draw to, what rect to use - and it will paint text (or a button etc depending on what type of widget) to that image in response to a "Paint" command.

First published 04/10/2005