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 (
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.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.
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.
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.