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