In the first installment of this article we had a look at how to load and display a tile based map. In this second post we will take it one step further by implementing scroll (movement) management – which is an essential part of our chosen genre of games.
Scrolling, what’s that?
The word “scroll” is not that common these days. People instead use words like “sliding” and “gliding” to describe pretty much the same thing. But we will stick to the old school term “scroll”, which is losely related to the english word “stroll”, to walk, to move at a steady pace.
What we want to scroll is naturally the map itself. We can achieve this by 3 means:
- Re-draw the map at different positions for each display
- Draw the map to an offscreen (hidden) bitmap and copy parts onto the display
- Use CSS to scroll the map
The current game project in Smart Mobile Studio is a pure 2d canvas project. Mixing CSS with raw pixel graphics at this point would be problematic and to some degree a poor choise. We could easily use the sprite3d api (sprite3d.pas) to create a css based game, but for this tutorial which focuses on “the classical way” of doing things, we will stick to method number 2.
Small levels
Im going to make use of the fact that mobile games are usually short, with quick matches and smaller levels than we traditionally have on the PC or mac. We will use this to our advantage and pre-draw an entire level onto a background bitmap. We will then copy slices of this onto the visible canvas – giving the illusion of scrolling along.
Scroll management class
Right, we want to keep things simple and straight to the point. We have the code needed to draw a map onto a canvas, and we now want to pre-draw the entire world onto a background bitmap, and copy our graphics from that instead of constantly drawing from the tileset. Here is the bare-bone scrolling manager so far:
{.$DEFINE USE_LIVE_TILE_DRAWING} type TScrollManager = class(TObject) private // map-data and tileset classes FMapData: TMapData; FTileset: TTileset; // current x/y pos on display FXpos: Integer; FYPos: Integer; // total size of map in pixels FWorldWidth: Integer; FWorldHeight: Integer; // size of target viewport FViewWidth: Integer; FViewHeight: Integer; // tiles required to fill viewport FBlocksReqX: Integer; FBlocksReqY: Integer; // our cache bitmap and canvas FContext: TW3GraphicContext; FCanvas: TW3Canvas; public constructor Create(Const aMapData: TMapData; aTileset: TTileset); virtual; procedure DrawTo(const Canvas: TW3Canvas; const startX, startY: Integer); function getCurrentX: Integer; function getCurrentY: Integer; procedure MoveTo(dx, dy: Integer); procedure MoveBy(x, y: Integer); procedure UpdateMetrics(const ViewWidth: Integer; const ViewHeight: Integer); property WorldWidth: Integer read FWorldWidth; property WorldHeight: Integer read FWorldHeight; property ViewWidth: Integer read FViewWidth; property ViewHeight: Integer read FViewHeight; end; //####################################################################### // TScrollManager //####################################################################### constructor TScrollManager.Create(Const aMapData: TMapData; aTileset: TTileset); begin inherited Create; if assigned(aMapData) then FMapData := aMapData else raise Exception.Create('Scrollmanager mapdata was NIL error'); if assigned(aTileset) then FTileset := aTileset else raise Exception.Create('Scrollmanager tileset was NIL error'); end; procedure TScrollManager.UpdateMetrics( const ViewWidth: Integer; const ViewHeight: Integer); var x, y: Integer; begin // calculate total size of "world" FWorldWidth := FMapData.Width * CNT_TILE_WIDTH; FWorldHeight := FMapData.Height * CNT_TILE_HEIGHT; // cache display size FViewWidth := ViewWidth; FViewHeight := ViewHeight; // calculate # of tiles required to fill display horizontaly FBlocksReqX := ViewWidth div CNT_TILE_WIDTH; if FBlocksReqX * CNT_TILE_WIDTH < ViewWidth then inc(FBlocksReqX); // caclulate # of tiles required to fill display vertically FBlocksReqY := viewHeight div CNT_TILE_HEIGHT; if FBlocksReqY * CNT_TILE_HEIGHT < viewHeight then inc(FBlocksReqY); // release pre-rendered context if already allocated if assigned(FContext) then begin FCanvas.free; FContext.free; FCanvas := NIL; FContext := NIL; end; // pre-allocate entire level as a single bitmap // and pre-render everything if FTileset.Ready and FMapData.Ready then begin FContext := TW3GraphicContext.Create(NIL); FContext.Allocate(FWorldWidth,FWorldHeight); FCanvas := TW3Canvas.Create(FContext); for y := 0 to FMapData.height-1 do for x := 0 to FMapData.width-1 do FTileSet.Draw(FCanvas,x * 32, y * 32, FMapData.Data[x,y]); end; end; procedure TScrollManager.DrawTo( const Canvas: TW3Canvas; const StartX, StartY: Integer); {$IFDEF USE_LIVE_TILE_DRAWING} var x,y: Integer; dx,dy: Integer; xoff: Integer; yOff: Integer; mTemp: Integer; srcXPos: Integer; srcYpos: Integer; mTileId: Integer; {$ENDIF} begin if canvas <> NIL then begin {$IFNDEF USE_LIVE_TILE_DRAWING} try Canvas.DrawImageF(FContext.Handle, FXpos,FYpos, FViewWidth-1,FViewHeight-1, StartX,StartY,FViewWidth-1,FViewHeight-1); except on e: exception do Begin showmessage(e.message); application.terminate; exit; end; end; {$ELSE} (* pixel offset left-edge *) mTemp := FXPos div CNT_TILE_WIDTH; FXOff := FXPos - (mTemp * CNT_TILE_WIDTH); (* pixel offset top-edge *) mTemp := FYPos div CNT_TILE_HEIGHT; FYOff := FYPos - (mTemp * CNT_TILE_HEIGHT); (* map Xpos *) srcXpos := FXpos div CNT_TILE_WIDTH; srcYPos := FYpos div CNT_TILE_HEIGHT; for y := 0 to FBlocksReqY do begin if srcYpos + y > FMapData.height then break; for x := 0 to FBlocksReqX do Begin if srcXpos + x > FMapData.Width then break; dx := StartX + (x * CNT_TILE_WIDTH) - FXOff; dy := startY + (y * CNT_TILE_HEIGHT) - FYOff; mTileId := FMapData.Data[srcXpos + x, srcYpos + y]; if (mTileId>=0) then FTileset.Draw(Canvas,dx,dy,mTileId); end; end; {$ENDIF} end; end; function TScrollManager.getCurrentX: Integer; begin Result := FXpos; end; function TScrollManager.getCurrentY: Integer; begin Result := FYPos; end; procedure TScrollManager.MoveTo(dx, dy: Integer); begin FXPos := dx; FYpos := dy; end; procedure TScrollManager.MoveBy(x, y: Integer); begin inc(FXPos,x); inc(FYpos,y); end;
Did you notice the {$DEFINE} constant? I included two drawing routines. The first one is the version I have mentioned so far, which copies from a pre-rendered bitmap. The second draws the present display using tiles. The latter can be handy if you want to test or somehow need to interact more closely with the tiles. One example is if you want to draw the tile-id over the tile so you can see the id number while working. Just leave the define inactive (as it is now) if this doesnt bother you.
Next up we need to alter the main-drawing loop of our app so that it uses the new technique (we also add the new class as a field of our TApplication instance). So our main drawing loop now looks like this:
procedure TApplication.PaintView(Canvas: TW3Canvas); var wd,hd: Integer; begin // cache width/height to local variables wd:=GameView.width; hd:=GameView.height; // Sanity check, resources ready etc? if FTileset.Ready and FMapData.Ready and (wd>1) and (hd>1) and not application.Terminated then begin // Clear background Canvas.FillStyle := 'rgb(0, 0, 99)'; Canvas.FillRectF(0, 0, wd, hd); // Draw our framerate on the screen Canvas.Font := '10pt verdana'; Canvas.FillStyle := 'rgb(255, 255, 255)'; Canvas.FillTextF('FPS:' + IntToStr(GameView.FrameRate), 10, 20, MAX_INT); // Check if display is altered, update map-view metrics // if size or orientation has changed. if (FScrollManager.ViewWidth<>wd) or (FScrollManager.ViewHeight<>hd) then begin FScrollManager.UpdateMetrics(wd,hd); end; // update xpos by 1 pixel (scroll right) FScrollManager.MoveBy(1,0); // copy from pre-rendered level FScrollManager.DrawTo(Canvas,0,0); end; end;
Ok, hit F9 and we get the same output as before – but this time it moves!
Navigation
Having a map that scrolls constantly to the right isnt really that exciting. I know some games deploy fixed scrolling on the iphone but I personally prefer to control a character myself. Which brings us to another very important piece of our game – namely controls.
The iPhone and Android devices dont have “real” keys. Nor do we want to have the keyboard dialog spring into action during our game. So we have to come up with some form of virtual joystick. Once we have that in place we can continue with the task of controling the map – and finally add a character, gravity, jumping routines and terrain checking. Stay tuned for Part 3!