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

Last updated 18th November, 2005

© 2006 MeccaMedialight. Site Powered by Wrangler 8.