Scrolling Tile Game - 3.v2

View Demo | download the source (soon)

Note - the demo is 2.8MB because there is 240 32-bit bitmaps. This is due to laziness (not trimming the walk cycles down from 30 steps to about 8) not design.

In the previous tutorials, we started to encapsulate different aspects of the game system: in that tutorial, we created the main Game script ("Game.main") and a behaviour ("Game.Behaviour"). The behaviour instantiates the main Game script into the 'GameObj" property of the behaviour and then sends #Update messages to this GameObject (when the game is playing). Here is the behaviour:

"Game.Main" Script

Here is the first part of the main game script (briefly introduced in the previous tutorial) that handles to creation of games:

["Game.Main" (Parent Script)]

-- Most of the lingo in this script is specific to this particular game.  

-- It makes use of various classes (scripts) stored in the "GameEngine" 

-- cast. These other classes are more generic and could be used in 

-- different games without modifcation 





-- Main Objects 

property Display       -- main display object

property TileEngine    -- main tiling engine

property MiniMap       -- little overview map



property Avatar        -- main avatar object

property Cameras       -- list of cameras

property ActiveCamera  -- active camera object

property StartUpDaemon -- utility object used to startup



-- some rects and points use to identify different regions

-- on the stage

property MainViewRect

property MainViewOffset

property MiniMapRect

property MiniMapOffset



-- some overlays

property ProgressBar

property MessageDisplay

property FPScounter



-- for debugging

property InfoDisplay

property DebugOverlayMode



-----------------------------------------------------------

--  Create and Destroy

-----------------------------------------------------------



on new (me)

  

  -- MainViewRect is the rect of the main game view. MainViewOffset 

  -- is the offset relative to the stage for the main game view

  MainViewOffset = point (10,10)

  MainViewRect = rect(0,0, 480, 360).offset(MainViewOffset.locH, 

						MainViewOffset.locV)

  

  -- MiniMapRect is the rect of the small 'mini map'. MiniMapOffset 

  -- is the offset relative to the stage for this rect

  MiniMapOffset = point(390,390)

  MiniMapRect = rect(0,0,100,100).offset(MiniMapOffset.locH, 

						MiniMapOffset.locV)

  

  

  

  -- Create the display object

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

  Display.Initialise((the stage).image, MainViewRect)

  

  buffer = Display.GetBuffer()

  

  -- Create a general Text Display object

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

  MessageDisplay.Initialise(buffer, rect(40,60,240,78), 

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

  

  -- Create the progress bar object

  ProgressBar = script("Widget.ProgressBar").new()

  ProgressBar.Initialise(buffer, rect(40,80,340,94))

  

  return me

  

end



on Destroy (me)

  

  -- Update the display

  Display.Reset()

  MessageDisplay.Display("Stopping...")

  Display.PaintAndUpdate()

  

  -- kill the StartUpDaemon (if necessary)

  if StartUpDaemon.ilk = #instance then StartUpDaemon.Destroy()

  StartUpDaemon = VOID

  

  -- save score etc

  -- $$ TO DO

end





-----------------------------------------------------------

--  Create a new game

-----------------------------------------------------------





on NewGame (me)

  --- Update the display

  Display.Reset()

  MessageDisplay.Display("Starting...")

  Display.PaintAndUpdate()

  --- Reset the minimap

  if MiniMap.ilk = #instance then

    MiniMap.Reset()

  end if

  

  -- Now execute a sequence of commands to complete the start-up

  cmds = [#CreatetileEngine, #CreateMinimap, #CreateOverlays, #GetAndLoadMap, 

    		#CreateAvatar, #CreateMainCameras, #FinishStartUp]

  listeners = [me]

  if StartUpDaemon.ilk = #instance then StartUpDaemon.Destroy()

  StartUpDaemon = script("Daemon.ExecuteSequence").new(listeners, cmds)

  

end





-- Startup Methods (called by the StartUpDaemon)



on CreatetileEngine (me)

  Display.Reset()

  MessageDisplay.Display("Creating tile engine...")

  Display.PaintAndUpdate()

  

  --  Create the TileEngine

  buffer = Display.GetBuffer()

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

  TileEngine.Initialise(buffer, buffer.rect)

end



on CreateMinimap (me)

  Display.Reset()

  MessageDisplay.Display("Creating minimap...")

  Display.PaintAndUpdate()

  

  -- Create the MiniMap

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

  MiniMap.Initialise(TileEngine, (the stage).image, MiniMapRect)

end



on CreateAvatar (me)

  Display.Reset()

  MessageDisplay.Display("Creating test avatar...")

  Display.PaintAndUpdate()

  

  -- Create Test Avatar

  buffer = Display.GetBuffer()

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

  Avatar.Initialise(TileEngine, buffer)

  

  sendAllSprites(#AttachAvatar, avatar)

end



on CreateMainCameras (me)

  Display.Reset()

  MessageDisplay.Display("Creating main cameras...")

  Display.PaintAndUpdate()

  

  AvatarCam = script("TileEngine.Avatar.Cam").new()

  AvatarCam.Initialise(TileEngine, Avatar)

  

  MouseCam = script("TileEngine.Mouse.Cam").new()

  MouseCam.Initialise(TileEngine, MainViewRect, MainViewOffset)

  

  -- set this to the current active camera

  Cameras = [#Avatar: AvatarCam, #Mouse: MouseCam]

  ActiveCamera = MouseCam

end



on CreateOverlays (me)

  Display.Reset()

  MessageDisplay.Display("Creating overlays...")

  Display.PaintAndUpdate()

  

  buffer = Display.GetBuffer()

  

  -- 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)

  

  -- Create a MouseInfo Display object

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

  InfoDisplay.Initialise(buffer, rect(200,10,440,30), 

 	[#fontFace:#default, #textcolour: rgb(255,255,255), #AllowWrap: true])

end



on GetAndLoadMap (me)

  Display.Reset()

  MessageDisplay.Display("Creating Random Map...")

  Display.PaintAndUpdate()

  

  aMap = script("TileGame.MapMaker").GetRandomMap()

  LoadResult = TileEngine.LoadMap(aMap, member("Tile00001"), me)

  if LoadResult = "OK" then

    MiniMap.LoadMap()

  else

    alert LoadResult

    me.Destroy()

  end if

end



on FinishStartUp (me)

  -- finished startup. Ask user to click to start

  Display.Reset()

  MessageDisplay.Display("Click to start...")

  Display.PaintAndUpdate()

  if StartUpDaemon.ilk = #instance then 

    StartUpDaemon.Destroy()

    StartUpDaemon = VOID

  end if

  sendAllSprites(#GameReady, me)

end







-----------------------------------------------------------

--  Events

-----------------------------------------------------------





on Update (me)

  

  -- Update the TileEngine

  TileEngine.Update()

  

  -- Update the main avatar

  Avatar.Update()

  

  -- Update the active camera

  ActiveCamera.Update()

  

  -- Update the minimap

  -- (build list of things to show on the map)

  ActorsToUpdate = [Avatar]

  MiniMap.Update(ActorsToUpdate)

  

  -- Show the FPS

  FPScounter.Update()

  

  -- Show mouseInfo

  me.ShowDebugInfo()

  

  -- finally, paint the buffer to the canvas

  Display.Paint()

end







on mouseClick (me)

  p = the mouseLoc

  

  if inside(p, MiniMapRect) then

    p1 = p - MiniMapOffset

    MiniMap.PushMap(p1)

    

  else if inside(p, MainViewRect) then

    p2 = p - MainViewOffset

    i = TileEngine.ViewToMap(p2)  

    if i.ilk = #Point then

      -- clicked a tile

      tile = TileEngine.GetTile(i)

      Tile.mouseDown()

      Avatar.MoveToMapPoint(p2)

    end if

  end if

end

Each of these 'startup sequence' methods are fairly similar: The first few lines display a message or progress bar, the subsequent lines actually do some work.

The "TileEngine" script

The "TileEngine" script used in this demo is a scrolling-grid style outlined in Scrolling Tile Game - 1. Here's a refined version of that script.

["Tile Engine" (NO PRERENDER) v.2.01 (Parent script)]



-- Note this version of the TileEngine does not pre-render the map. The 

-- advantage is that you can use big maps. The disadvantage is that it will be 

-- slower (especially for large display area with small tiles).





------------------------ IMAGING

property Buffer                            -- offscreen image of the map

property Canvas                            -- the output image

property RectOnCanvas                      -- dest rect on the canvas



------------------------ MAPPING

property MapList                           -- map of the current tiled image

property MapLoaded





------------------------ STATIC TILE PROPERTIES

property Tile_Width, Tile_Height           -- assume all tiles are the same size

property Tile_Rect                         

property Tile_HalfWidth

property Tile_HalfHeight



------------------------ SCROLLING

property MapY, MapX                        -- current mapping (scroll)

property MapXi, MapYi                      -- integer versions of MapX and MapY 

property MapXLimit, MapYLimit              -- limits on moving the view

property MaxTilesToDrawX, MaxTilesToDrawY  -- max number of tiles to draw 

property WorldPixelSize



------------------------ DEBUGGING

property SelectedTile

property LastPath  



--------------------------------------------------------------------------------

-- Initialise

--------------------------------------------------------------------------------







on Initialise (me, outputImage, aRect)

  

  

  Canvas = outputImage

  if aRect.ilk = #rect then RectOnCanvas = aRect

  else RectOnCanvas = Canvas.rect

  MapLoaded = false

  LastPath = []

  return me

  

end





--------------------------------------------------------------------------------

-- Load Map

--------------------------------------------------------------------------------







on LoadMap (me, aMap, baseTile, feedbackObj)

  -- Parameters:

  -- * aMap is a list of lists containing tile objects  

  -- * base tile is a member reference of a 'base tile' (used 

  --   to get width, height and regpoint)

  -- Returns

  -- "OK" for success or an error description

  

  

  -- check input parameters

  if (aMap.ilk <> #list) then return 

  	"Bad parameter: supplied Map is not a list"

  if (count(aMap) < 1) then return 

  	"Bad parameter: supplied Map is empty"

  if (aMap[1].ilk <> #list) then return 

   	"Bad parameter: supplied Map is incorrectly populated (first row is not a list)"

  if (aMap[1].count < 1) then return 

  	"Bad parameter: supplied Map is incorrectly populated (first row is empty)"

  if (baseTile.ilk <> #member) then return 

  	"Bad parameter: need a member reference for a base tile"

  if (baseTile.type <> #bitmap) then return 

  	"Bad parameter: need a bitmap member reference for a base tile"

  

  me._InitialiseMap(aMap, baseTile, feedbackObj)

  MapLoaded = true

  return "OK"

end





--------------------------------------------------------------------------------

-- Mapping Methods



-- MapLoc is the position of the tile in the map ie. point(TilesX, TilesY)

-- WorldLoc is the position is the 'world' (in pixels)

-- ViewLoc is the position in the view (ie. WorldLoc - scrollAmount)

--------------------------------------------------------------------------------







on WorldToMap (me, worldLoc)

  if MapLoaded then

    x = floor((worldLoc.locH)/float(Tile_Width)) + 1

    y = floor((worldLoc.locV)/float(Tile_Height)) + 1

    x = Max(1, Min(MapList[1].count, x))

    y = Max(1, Min(MapList.count, y))

    return point(x,y)

  end if

end





on MapToWorld (me, mapLoc)

  if MapLoaded then

    -- return the point in the middle of the specified tile

    x = (mapLoc.locH-1)*Tile_Width + Tile_HalfWidth

    y = (mapLoc.locV-1)*Tile_Height + Tile_HalfHeight

    return point(x,y)

  end if

end





on ViewToWorld (me, viewLoc)

  -- translates a point in the current view to a its 

  -- position in the world

  return point(viewLoc.locH -MapX, viewLoc.locV-MapY)

end



on WorldToView (me, p)

  -- translates a point in the world to a its 

  -- relative position in the current view

  return point(p.locH +MapX, p.locV+MapY)

end





on ViewToMap (me, viewLoc)

  -- translates a point in the current view to a its 

  -- position in the world

  

  return me.WorldToMap(point(viewLoc.locH -MapX, viewLoc.locV-MapY))

end



on MapToView (me, p)

  -- translates a point in the world to a its 

  -- relative position in the current view

  p = me.MapToWorld(p)

  return point(p.locH +MapX, p.locV+MapY)

end





on OffsetToMiddleOfView (me, PntOnMap)

  -- return a vector (point) from the specifed point

  -- to the middle of the current view

  p1 = point(PntOnMap.locH , PntOnMap.locV)

  p2 = point( RectOnCanvas.width/2-MapX, RectOnCanvas.height/2-MapY)

  return (p2-p1)

end





--------------------------------------------------------------------------------

-- Update Event

-- Redraw all the visible tiles

--------------------------------------------------------------------------------







on Update (me) 

  if MapLoaded then

 	-- update the visible tiles

      

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

    

    -- now paint the hilight 

    if SelectedTile <> VOID then

      aColor = rgb("#000")

      PaintRect = SelectedTile.destRect.offset(MapXi,MapYi)

      Canvas.draw( PaintRect, [#ShapeType:#rect, #Color: aColor])

    end if

    

  end if

end





--------------------------------------------------------------------------------

-- Painting Methods

--------------------------------------------------------------------------------







on PaintFullMap (me, intoThisImage)

  -- paint the map into a supplied image, returning the scale 

  -- of the rendered map and an offset to the top-left of the 

  -- canvas the map has been painted into. This method is used

  -- by the 'minimap' to get small version of the whole map

  

  PaintRect = intoThisImage.rect

  worldTileSize = [maplist[1].count, maplist.count]

  --

  TileOffset = [0,0]

  if worldTileSize[1] = worldTileSize[2] then 

    tileDrawWidth = PaintRect.width/float(maplist[1].count)

    tileDrawHeight = PaintRect.height/float(maplist.count)

    tileDrawRect = rect(0, 0, tileDrawWidth, tileDrawHeight)

    

  else if worldTileSize[1] > worldTileSize[2] then 

    tileDrawWidth = PaintRect.width/float(maplist[1].count)

    tileDrawHeight = tileDrawWidth

    tileDrawRect = rect(0, 0, tileDrawWidth, tileDrawHeight)

    gap = PaintRect.height - (worldTileSize[2]*tileDrawHeight) 

    TileOffset = TileOffset + [0, gap*0.5]

    

  else 

    tileDrawHeight = PaintRect.height/float(maplist.count)

    tileDrawWidth = tileDrawHeight

    tileDrawRect = rect(0, 0, tileDrawWidth, tileDrawHeight)

    gap = PaintRect.width -  (worldTileSize[1]*tileDrawWidth) 

    TileOffset = TileOffset + [gap*0.5, 0]

    

  end if

  MapScale =  float(tileDrawWidth) / tile_width

  CurrentOffset = TileOffset.duplicate()

  repeat with y = 1 to worldTileSize[2]

    aRow = maplist[y]

    repeat with x = 1 to worldTileSize[1]

      destRect = tileDrawRect + rect(CurrentOffset[1], CurrentOffset[2],

               CurrentOffset[1], CurrentOffset[2])

      imageRef = maplist[y][x].image

      intoThisImage.copyPixels(imageRef, destRect, tile_Rect)

      CurrentOffset[1] = CurrentOffset[1] + tileDrawWidth

    end repeat

    CurrentOffset[1] = TileOffset[1]

    CurrentOffset[2] = CurrentOffset[2] + tileDrawHeight

  end repeat

  

  return [#MapScale: MapScale,#TileOffset:TileOffset] 

end





--------------------------------------------------------------------------------

-- Pathfinding

--------------------------------------------------------------------------------



on GetPathCost (me, x,y)

  -- return the cost to make a path across the specified tile

  x = mapList[y][x].GetCost()

  return x

end



on ShowPath (me, aPath)

  -- just for debugging

  

  repeat with T in LastPath

    T.RestoreImage()

  end repeat

  

  LastPath.deleteAll()

  

  repeat with aLoc in aPath

    t = MapList[aLoc.locV][aLoc.locH]

    T.PathHilight()

    LastPath.append(T)

  end repeat

end





--------------------------------------------------------------------------------

-- Scrolling the view

--------------------------------------------------------------------------------







on MoveView (me, x, y) 

  -- Shifts the current map coordinates by the specified 

  -- amount and updates the buffer

  

  if MapLoaded then

    MapX = MIN(0, MAX(MapXLimit, MapX-x)) 

    MapY = MIN(0, MAX(MapYLimit, MapY-y))

    MapXi = integer(MapX)

    MapYi = integer(MapY)

  end if

end



on MoveViewTo (me, newLoc)

  -- shifts the current map coordinates to the specified point



  if MapLoaded then 

    amnt = newLoc + point(RectOnCanvas.width/2, RectOnCanvas.height/2)

    MapX = MIN(0, MAX(MapXLimit, amnt.locH)) 

    MapY = MIN(0, MAX(MapYLimit, amnt.locV))

    MapXi = integer(MapX)

    MapYi = integer(MapY)

  end if

end



on GetViewRect(me)

  return RectOnCanvas.offset(-MapXi,-MapYi)

end



--------------------------------------------------------------------------------

-- Get and Set Tile Objects

--------------------------------------------------------------------------------



on GetTile  (me, iPnt)

  -- returns a reference to the specified tile object 

  iy = iPnt.locV

  ix = iPnt.locH

  return mapList[iy][ix]

end



on SetTile  (me, iPnt, thisTile)

  -- returns a reference to the specified tile object

  

  iy = iPnt.locV

  ix = iPnt.locH

  

  x_offset = (ix-1)*Tile_Width

  y_offset = (iy-1)*Tile_Height

  mappedTile = script("TileEngine.Tile.Mapped").new(thisTile,

                                                 x_offset, y_offset)

  MapList[iy][ix] = mappedTile

  return MapList[iy][ix]

end



--------------------------------------------------------------------------------

-- Interacting with Map

--------------------------------------------------------------------------------



on  GetMovedPoint(me, startPnt, endPnt, rectToMove)

  -- return the point furthest along the line from 

  -- startPnt to endPnt that the rectToMove can move to

  

  -- in this version, simply ensure that the rect stays 

  -- within the world rect

  x = 0

  y = 0

  r = rectToMove.offset(endPnt.locH, endPnt.locV)

  if r.left < 0 then x = -r.left

  else if r.right > WorldPixelSize[1] then x = WorldPixelSize[1]-r.right

  if r.top < 0 then y = -r.top

  else if r.bottom > WorldPixelSize[2] then y = WorldPixelSize[2]-r.bottom

  return endPnt + point(x,y)

end





--------------------------------------------------------------------------------

-- Hilight a tile

--------------------------------------------------------------------------------







on HilightTile (me, aTile)

  if aTile.ilk = #instance then

    SelectedTile = aTile

  end if

end



on DeselectTile (me)

  SelectedTile = VOID

end











--------------------------------------------------------------------------------

-- Private Methods

--------------------------------------------------------------------------------





on _InitialiseMap (me, aMap, basetile, feedbackObj)

  

  MapList = aMap

  MapY = 0

  MapX = 0

  

  Tile_Width = basetile.width

  Tile_Height = basetile.height

  Tile_Rect = rect(0,0,Tile_Width,Tile_Height)

  

  Tile_HalfWidth = Tile_Width/2

  Tile_HalfHeight = Tile_Height/2

  

  WorldTileSize = [MapList[1].count, MapList.count]

  WorldPixelSize = WorldTileSize * [Tile_Width, Tile_Height]

  

  MaxTilesToDrawX = RectOnCanvas.width/Tile_Width +1

  MaxTilesToDrawY = RectOnCanvas.height/Tile_Height +1

  

  MapXLimit = -(((MapList[1].count) * Tile_Width) - RectOnCanvas.width - 1)

  MapYLimit = -(((MapList.count) * Tile_Height) - RectOnCanvas.height - 1)

  

  x_offset = 0

  y_offset = 0

  

  steps = float(MapList.count)



  repeat with y = 1 to MapList.count

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

      thisTile = MapList[y][x]

      mappedTile = script("TileEngine.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

    

    call(#ShowProgress, [feedbackObj], "Loading Map...", (y/steps))

  end repeat

  

  buffer = image(WorldPixelSize[1], WorldPixelSize[2], 16)

end

The Minimap

The next object created by the GameObj is the 'MiniMap'. The minimap shows the entire map at a reduced scale, and shows actors in the world as little dots. The Minimap also has methods for setting the scroll of the TileEngine (meaning, for example, the minimap could respond to mouseClicks and centre the view on the point clicked). Here is the MiniMap:

-- ["TileEngine.Minimap" (Parent Script)]



property TileEngine -- reference to the tileEngine object

property Canvas -- the output image

property Buffer -- internal buffer image

property PaintRect  -- dest rect on the canvas

property SourceRect  -- source rect (the rect of the buffer)

property MapScale  -- current scale of the minimap

property TileOffset -- offset to the map from the top-left corner of 

         the mini view (would be [0,0] if they are the same dimensions)



--------------------------------------------------------------------------------

-- Create and destroy

--------------------------------------------------------------------------------





on Initialise (me, tileEngineRef, outputImage, outputRect)

  TileEngine  = tileEngineRef

  Canvas = outputImage

  PaintRect = outputRect

  Buffer = image(PaintRect.width, PaintRect.height, 16)

  SourceRect = Buffer.rect

  Buffer.fill(SourceRect, RGB(0,0,0))

  Canvas.copyPixels(Buffer, PaintRect, SourceRect)

  TileOffset = [0,0]

  return me

  

end



on Destroy (me)

  -- Cleanup

  TileEngine = VOID

  me.Reset()

end





on Reset (me)

  -- Paints the default colour into the minimap

  Buffer.fill(SourceRect, RGB(0,0,0))

  Canvas.copyPixels(Buffer, PaintRect, SourceRect)

end



--------------------------------------------------------------------------------

-- Load Current Map

--------------------------------------------------------------------------------





on LoadMap (me)

  -- Updates the minimap to the current map used by the tile engine 

  r = TileEngine.PaintFullMap(Buffer)

  MapScale = r.mapScale

  TileOffset = r.TileOffset

  

end





--------------------------------------------------------------------------------

-- Update Event

--------------------------------------------------------------------------------





on Update (me, aListOfActors)

  -- Draws a rect corresponding to the current view and updates the miniMap.

  --  If a list of 'actors' is supplie, they are added to the map

  

  aCurrentViewRect = TileEngine.GetViewRect()

  destRect = (aCurrentViewRect*MapScale).offset(TileOffset[1], TileOffset[2]) 

  

  PaintBuffer = Buffer.duplicate()

  PaintBuffer.draw(destRect, [#shapeType: #rect, #Color: RGB(255,0,0)])

  -- draw the actors

  if listP(aListOfActors) then

    repeat with anActor in aListOfActors

      aPnt = anActor.WorldLoc

      aColour = anActor[#MapColour]

      if voidP(aColour) then aColour = rgb("#FF0000")

      --

      aPnt = aPnt*MapScale + TileOffset

      destRect = rect(aPnt.locH-1, aPnt.locV-1, aPnt.locH+1, aPnt.locV+1)

      PaintBuffer.draw(destRect, [#shapeType: #oval, #Color: aColour])

    end repeat

  end if

  Canvas.copyPixels(PaintBuffer,  PaintRect, SourceRect)

end





--------------------------------------------------------------------------------

-- Translating points in the game world to points on the minimap

--------------------------------------------------------------------------------



on GetPointOnMiniMap (me, aWorldPoint)

  -- Get a point on the minimap corresponding to a point in the world

  

  aPnt = aPnt*MapScale + TileOffset

  return aPnt

  

end





--------------------------------------------------------------------------------

-- Using the minimap to scroll the game view

--------------------------------------------------------------------------------







on PushMap (me, aMiniMapPoint)

  -- Centre the world view to a point on the minimap

  

  anOffset = (aMiniMapPoint - TileOffset)/ -MapScale

  TileEngine.MoveViewTo(anOffset)

  

end

The Avatar Script

The next object created is the avatar (or 'sprite' or 'actor'). It is a actor in the game controlled by the player. The avatar will respond to Update messages by doing the following things:

1. Work out what it is meant to be doing (walking, standing etc)

2. Get its current image (eg. image X in a list)

3. Determine its location 'in the world'

4. Paint this image to the buffer at this location

-- ["TileEngine.avatar"(Parent Script)]





property TileEngine, Buffer, WorldLoc, AvatarImg, AvatarAnimator,AvatarOffset



property CurrentPath, NextTile, NextPnt, MovingToMiddle, DestRect

property CurrentActivity, pathFinder



--------------------------------------------------------------------------------

-- Create and destroy

--------------------------------------------------------------------------------





on Initialise (me, theTileEngine, bufferImg)

  

  TileEngine = theTileEngine

  Buffer = bufferImg

  WorldLoc = point(100,100)

  AvatarAnimator = script("TileEngine.avatar.animator").new()

  AvatarAnimator.Initialise()

  AvatarImg = AvatarAnimator.GetImage(0,0) 

  AvatarOffset = AvatarAnimator.GetOffset()

  

  -- setup paths

  NextTile = TileEngine.WorldToMap(WorldLoc)

  NextPnt = TileEngine.MapToWorld(NextTile)

  DestRect = rect(NextPnt.locH-2, NextPnt.locV-2, 

                  NextPnt.locH+2, NextPnt.locV+2)

  CurrentPath = [NextTile]

  

  CurrentActivity = #Idle

  

end



on Destroy (me)

  CurrentActivity = #dead

  if pathFinder.ilk = #instance then pathFinder.Destroy()

end





--------------------------------------------------------------------------------

-- Main Interface



-- 'MoveToMapPoint': Tell the avatar to move to the specified 'point in the

-- current view (eg mouseLoc)'

--------------------------------------------------------------------------------





on MoveToMapPoint (me, ViewTargetPnt)

  CurrentActivity = #thinking

  mapTargetPnt = TileEngine.ViewToMap(ViewTargetPnt)

  

  if pathFinder.ilk = #instance then pathFinder.Destroy()

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

  pathFinder.createPath(TileEngine.WorldToMap(WorldLoc), mapTargetPnt, me)

end



--------------------------------------------------------------------------------

-- Pathfinder callbacks

--------------------------------------------------------------------------------



on LoadPath(me, aPath)

  -- when a path is found, the pathfinder object will use this 

  -- method to assign a path

  

  if CurrentActivity = #dead then return

  

  CurrentActivity = #walking

  CurrentPath = aPath

  TileEngine.ShowPath(aPath)

  if (count(CurrentPath)) then

    

    NextTile = TileEngine.WorldToMap(WorldLoc)

    NextPnt = TileEngine.MapToWorld(NextTile)

    NextTile = CurrentPath[1]

    NextPnt = TileEngine.MapToWorld(NextTile)

    DestRect = rect(NextPnt.locH-4, NextPnt.locV-4, 

                 NextPnt.locH+4, NextPnt.locV+4)

    CurrentPath.deleteAt(1)

  end if

end



--------------------------------------------------------------------------------

-- Update Event

--------------------------------------------------------------------------------





on Update (me)

  -- what are we doing?

  if  CurrentActivity = #idle then me.IdleActivity()

  else me.MoveAlongPath()

  

  -- Paint self to buffer

  viewOffset = TileEngine.WorldToView(WorldLoc)

  Buffer.CopyPixels(AvatarImg, AvatarImg.rect.offset(

    	viewOffset.locH - AvatarOffset.locH, viewOffset.locV-

    	AvatarOffset.locV), AvatarImg.rect, [#Ink: 36])

  

  -- add little red cross at avators 'location' point ($$ DEBUG ONLY)

  Buffer.draw(viewOffset-point(3,0), viewOffset+point(3,0), 

  		[#Shapetype: #Line, #Color: rgb(255,0,0)])

  Buffer.draw(viewOffset-point(0,3), viewOffset+point(0,3), 

  		[#Shapetype: #Line, #Color: rgb(255,0,0)])

end





--------------------------------------------------------------------------------

-- Avatar Actions

--------------------------------------------------------------------------------





on MoveAlongPath (me)

  -- move along the current pathlist

  

  -- what tile are we on?

  t =  TileEngine.WorldToMap(WorldLoc)

  

  if (t = NextTile) then

    -- on current tile

    

    --    are we near the centre?

    if inside(worldLoc, DestRect) then

      

      -- are the more tiles in the path?

      if (count(CurrentPath)>0) then

        -- get the next tile in the path

        NextTile = CurrentPath[1]

        NextPnt = TileEngine.MapToWorld(NextTile)

        DestRect = rect(NextPnt.locH-4, NextPnt.locV-4, 

               NextPnt.locH+4, NextPnt.locV+4)

        CurrentPath.deleteAt(1)

        

      else

        

        CurrentActivity = #idle

        

      end if

      

    end if

  end if

  

  

  if WorldLoc.locH > NextPnt.locH then moveH = -1

  else  if WorldLoc.locH < NextPnt.locH then moveH = 1

  else moveH = 0

  if WorldLoc.locV > NextPnt.locV then moveY = -1

  else  if WorldLoc.locV < NextPnt.locV then moveY = 1

  else moveY = 0

  

  

 

  AvatarImg = AvatarAnimator.GetImage(moveH,moveY) 

  DesiredLoc = WorldLoc + point(moveH, moveY)

  -- make sure the avatar stays within the world rect

  WorldLoc = TileEngine.GetMovedPoint(WorldLoc, DesiredLoc, 

     AvatarImg.rect)

  

end



on IdleActivity (me)

  -- nothing yet

end

The Avatar Animator

This script delegates the task of choosing an image for the avatar to another script, the "TileEngine.avatar.animator", which it creates when initialising. In the demo movie is a cast library called "Actor_1" contain images for an avatar. There are 8 walk cycles. Each bitmap is named in sequence - "E.0001", "E.0002", "E.0003" etc where the first part of the name is the direction the character is facing, and the second part of the name is the position in the animation sequence. So the Initialse method of the animator object will loop through this cast library and make 8 lists of members. When we call its GetImage() method, it will pick a member from the appropriate list.

["TileEngine.avatar.animator" (Parent Script)]

-- Animator for the avatar sprite



property memberList, myFace , idx, myNextAnim, myRate





on Initialise (me)

  Dirs = [#E, #N, #NE, #NW, #S, #SE, #SW, #W]

  mx = the number of members of castlib "Actor_1"

  the itemDelimiter = "."

  MemberList = [#E:[], #N:[], #NE:[], #NW:[], #S:[], #SE:[], #SW:[], #W:[]]

  repeat with i = 1 to mx

    mRef = member(i, "Actor_1")

    n = mRef.name

    k = symbol(n.item[1])

    MemberList[k].append(mRef)

  end repeat

  myFace = #s

  myNextAnim = 0

  myRate = 20

end



on GetImage(me, x,y)

  -- to get an image, we need to specify which direction

  -- the avatar is facing

  

  

  if x > 0 then 

    if y > 0 then myFace = #SE

    else if y < 0 then myFace = #NE

    else myFace = #E

  else if x < 0 then

    if y > 0 then myFace = #SW

    else if y < 0 then myFace = #NW

    else myFace = #W

  else

    if y > 0 then myFace = #S

    else if y < 0 then myFace = #N

    else myIndex = 1

  end if

  

  now = the milliseconds

  if now > myNextAnim then

    myNextAnim = now + myRate

    idx = idx + 1

    if idx > 30 then idx = 1

  end if

  

  if x = 0 and y = 0 then idx = 18

  

  m = memberList[myFace][idx]

  return m.image

end



on GetOffset (me)

  return memberList[myFace][idx].regPoint + point(0,10)

end

The Pathfinder

The avatar uses another object to find paths around the map. An A-star "PathFinder" script will be discussed in the next tutorial. At this point, we will just uses a skeleton script that makes a simple path across the map (ignoring collisions, costs or anything).

Because pathfinding can be slow, this pathfinder works asynchonously and only does a 'little' search with each update

["Pathfiner" (SKELETON SCRIPT) ]

property FoundPathList, CurrentPnt, EndPnt

property Thread

property TargetObj







on createPath me, fromPnt, toPnt, forObject

  CurrentPnt = fromPnt

  EndPnt = toPnt

  FoundPathList = []

  Thread = timeout("FindPath").new(1, #continueFindingPath, me)

  TargetObj = forObject

end



on Destroy (me)

  -- kill the thread

  if Thread.ilk = #timeout then Thread.forget()

  Thread = VOID

end



on ContinueFindingPath (me)

  -- continue searching for a path until we find the target

  -- or until there are no unsearched tiles left

  

  --[$$ IN REAL VERSION OF THIS SCRIPT, USE A PROPER PATHFINDING

  --  ROUTINE HERE. ]

  

  FoundPathList.append(CurrentPnt)

  if CurrentPnt = EndPnt then  

    me.CompletePathFinding()

  else

    if CurrentPnt.locH > EndPnt.locH then 

    	CurrentPnt.locH = CurrentPnt.locH -1

    else if CurrentPnt.locH < EndPnt.locH then 

    	CurrentPnt.locH = CurrentPnt.locH +1

    end if

    if CurrentPnt.locV > EndPnt.locV then 

    	CurrentPnt.locV = CurrentPnt.locV -1

    else if CurrentPnt.locV < EndPnt.locV then 

    	CurrentPnt.locV = CurrentPnt.locV +1

    end if

  end if

end 



on CompletePathFinding (me)

  me.Destroy()

  TargetObj.LoadPath(FoundPathList)

end

The Cameras

After creating the avatar, the GameObject then creates some 'Cameras' - one that follows the avatar, and another that is controlled by the mouse. The job of the "Avatar Camera" object will be to scroll the current view to keep the avatar near the middle of the screen. This object will have an Update method like this

on Update (me)

  -- work out how far the avatar is from the middle of the view

  -- Move the view to keep the avatar centered (preferably moving

  -- smoothly, not abrupt)

end

This object is going to need to interact with the Tiler Object (in order to send in Scroll messages) and the Avatar (in order to determine is location). The script looks like this:

["TileEngine.Avatar.Cam" script]



property Avatar, TileEngine, Scroll_H, Scroll_V, MaxScrollSpeed

property Threshhold, Accel, Momentum



on Initialise (me, T, A)

  TileEngine = T

  Avatar = A

  MaxScrollSpeed = 8

  Threshhold = 100

  Accel = 1.1

  Momentum = .97

end



on Update (me)

  -- determine how far the avatar is from the middle of

  -- the current view

  d  = Tileengine.OffsetToMiddleOfView(Avatar.WorldLoc)

  

  -- if it is a certain distance from the centre, we

  -- will scroll the view. 

  

  if d.locH > Threshhold then

    -- scroll the view

    if Scroll_H > -1 then Scroll_H=-1

    else Scroll_H = Scroll_H -Accel

    

  else if d.locH < -Threshhold then

    if Scroll_H < 1 then Scroll_H= 1

    else Scroll_H = Scroll_H +Accel

    

  else 

    Scroll_H = Scroll_H * Momentum

  end if

  

  if d.locV > Threshhold then

    -- scroll the view

    if Scroll_V >-1 then Scroll_V = -1

    else Scroll_V = Scroll_V - Accel

    

  else if d.locV < -Threshhold then

    if Scroll_V < 1 then Scroll_V = 1

    else Scroll_V = Scroll_V + Accel

    

  else 

    Scroll_V = Scroll_V * Momentum

  end if

  

  

  if abs(Scroll_H) < 1 then Scroll_H = 0

  else Scroll_H = Max(-MaxScrollSpeed, Min(MaxScrollSpeed, Scroll_H))

  

  if abs(Scroll_V) < 1 then Scroll_V = 0

  Scroll_V = Max(-MaxScrollSpeed, Min(MaxScrollSpeed, Scroll_V))

  

  TileEngine.MoveView(Scroll_H,Scroll_V)

end

Most of the fiddly bits involves working out how to 'pull' the camera along smoothly - like it is attached to the avatar by some elastic (rather than a rigid bit of metal). In this example, the 'scroll_H' and 'scroll_V' properties slowly increase to a maximun. Once moving, they have some 'momentum'.

First published 03/11/2005