Scrolling Tile Game - 2

View Shockwave Demo | (source not available)

In part 4 this series, we are going to add some actors ('game sprites'), as well as a 'camera object' and a 'mini-map'. But first, we are going to separate a main game script from the 'game.behaviour'. The following diagram shows the main objects in our game:

Elements of game engine

The Game Behaviour

Behaviours are the interface between abstract objects (created completely in lingo) and the 'physical' objects occupying the score: sprites and the framescript slot. Our basic behaviour for creating the game system and feeding it events will look like this:

[Stipped Down version of the 'Game Behaviour']
-- This behaviour creates and instance of main Game script 
-- And feeds it stage events (enterframe, mouseClicks etc)

property Stack    -- List of objects to send stage events
property GameObj  -- Main Game Object

on beginSprite (me)
  GameObj = script("Game.main").new()
  me.NewGame()
end

on NewGame (me)
  GameObj.NewGame()
  Stack = [GameObj]
end


on enterframe (me)
  call(#Update, Stack)
end


on mouseDown (me)
  call(#MouseClick, Stack)
end

As you can see from this simple script, the behaviour does four main things:

  • Create the Game object
  • Provide a method for starting a new game
  • Sends an Update message with each enterframe event
  • Sends a MouseClick message with each mouseDown.

One of the reasons why this behaviour stores the Game object in a list (called stack) is that it makes it easy to toggle the game state or pause the game (stop it from receiving events) by simply removing the relevant objects from the stack. For example, a pauseGame and a continueGame method would look like this:

[Game Behaviour continued]

on PauseGame (me)
  Stack.DeleteOne(Game)
  Game.DisplayMessage("Paused")
end

on ContinueGame (me)
  if not(Stack.Getone(Game)) then
    Stack.addAt(1, Game)
  end if
end

The Main Game 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.

The new method of the demo Game.main script looks like this:

["Game.Main" (Parent Script) - Partial]

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 main 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 (we'll use this 
  -- when we're loading the map and various assets
  ProgressBar = script("Widget.ProgressBar").new()
  ProgressBar.Initialise(buffer, rect(40,80,340,94))
  
  return me
  
end

This method sets some parameters unique to this particular game (the rect of the main view area, as well as the rect for the minimap). It also initialises the main display object - because we'll use this display to show messages as the game loads. It also creates a ProgressBar object which will be used during the startup process.

This new method doesn't actually create a game - it just creates a few objects that we will use as we create the game. To create a new game, we will use a newGame handler. Here is the newGame method in the demo movie:

[Game.Main Parent Script - continued]

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, #CreateOverlays, #GetAndLoadMap, \
  #CreateMinimap,  #CreateAvatar, #CreateMainCameras,  #FinishStartUp]
  listeners = [me]
  if StartUpDaemon.ilk = #instance then StartUpDaemon.Destroy()
  StartUpDaemon = script("Daemon.ExecuteSequence").new(listeners, cmds)
  
end

This method displays a "Starting..." message and then creates a utility object (the 'StartUpDaemon') which will call a sequence of methods which will complete the startup process. The reason why we are using the StartUpDaemon rather than simply executing all the commands in the one method call is that some of the commands may be slow or may be asynchronous - that is, the may involves tasks such as reading XML or communicating with a server which do not immediately return a result (we need to wait for the XML to be read or the server to respond).

The Daemon.ExecuteSequence is a simple script which uses a timeout object. When a timeout occurs, it executes the next command in the sequence. Whilst Director waits for each timeout to occur, it will keep playing, generating exitframe and idle events which might be used by other objects.

Here is the Daemon.ExecuteSequence script:

["Daemon.ExecuteSequence" (Parent Script)]
-- Utility object to execute a series of commands 
-- (exiting the call stack between commands). 

property Listeners, Tasks


-----------------------
-- PUBLIC
-----------------------

on new (me, targets, methods)
  me.Listeners = targets
  me.Tasks = methods
  tmp = timeout(me.string).new(10, #__NextTask, me)
  return me
end

on Destroy (me)
  Tasks = []
end


-----------------------
-- PRIVATE
-----------------------

on __NextTask (me, timeoutObj)
  if me.Tasks.count then 
    task = Tasks[1]
    me.Tasks.deleteAt(1)
    call(task, me.Listeners, me)
  else
    timeoutObj.forget()
  end if
end

The sequence of commands to be executed basically create all the objects used in the game. The methods that are called are:

  • CreatetileEngine
  • CreateOverlays
  • GetAndLoadMap
  • CreateMinimap
  • CreateAvatar
  • CreateMainCamera
  • FinishStartUp

Note that the order in which the objects are created may be important (for example, the minimap requires a map loaded - so it is created after the map is loaded).

The final command that as executed is the FinishStartUp. The method for this looks like this:

[Game.Main Parent Script - Continued]


on FinishStartUp (me)
  -- finished startup. Ask user to click to start
  MessageDisplay.Display("Click to start...")
  MessageDisplay.PaintAndUpdate()

  -- kill the daemon
  if StartUpDaemon.ilk = #instance then 
    StartUpDaemon.Destroy()
    StartUpDaemon = VOID
  end if

  -- notify everyone we are ready to play
  sendAllSprites(#GameReady, me)
end

When the game is finally ready, it sends a GameReady message to all the sprites. So, the startup process is like this:

1. Send a NewGame message to the Game behaviour (from a "start game" button click etc). The behaviour then sends a NewGame message to the GameObj, and waits.

2. The GameObj responds to the NewGame message by creating a 'Daemon' (utility object) to execute a sequence of commands.

3. When the startup process is complete, the Game object then sends a GameReady message to all sprites to let them know that the game is ready to be started.

In the demo movie, the 'Game' behaviour listens for this GameReady message. When it receives the GameReady message, it displays a "Click to start game" message in the game display and creates another little utility object (an instance of the "clickToStart" script) which will listen for a mouseClick. When the clickToStart object gets the mouse click, it sends a StartGame message to the Game behaviour (and disposes of itself).

Here is the fleshed out version of the "Game.Behaviour"

-- ["Game.Behaviour" (behaviour)]
-- This behaviour creates an instance of main Game script and stores this
-- in a property called 'GameObj'. When the game is 'in play', this behaviour
-- feeds the GameObj stage events (enterframe, mouseClicks etc)

property Stack    -- List of objects to send stage events
property GameObj  -- Main Game Object
property State    -- Current Game System state (#stopped, #initialising \
                  #ready, #playing, #paused)


on beginSprite (me)
  State = #Stopped
  GameObj = script("Game.main").new()
  me.NewGame()
end


-----------------------------------------------------------
--  New Game
-----------------------------------------------------------


on NewGame (me)
  -- set the state to 'initialising', and send #NewGame to the 
  -- GamObject. When the game has finished initialising, it
  -- will send a #GameReady message back to here.
  
  State = #Intialising
  GameObj.NewGame()
  Stack = []
end


on GameReady (me)
  -- The GameObj should send a 'GameReady' message when it has
  -- finished initialising (loading map, assets, etc).
  -- Set the state to 'ready', and wait for a message
  -- to actually start the game
  
  i = script("clickToStart").new()
  sprite(me.spriteNum).scriptInstanceList.append(i)
  State = #Ready
end


-----------------------------------------------------------
--  Start & Stop Game
-----------------------------------------------------------


on StartGame (me)
  -- If the game engine is 'ready', then add it
  -- to the stack and return true. Otherwise, 
  -- return false.
  
  if State = #Ready then
    -- Game is loaded and ready
    Stack = [GameObj]
    State = #Playing
    return 1
  else
    -- Game isn't ready yet
    return 0
  end if
end

on QuitGame (me)
  -- Change the state, and stop the game
  State = #Stopped
  GameObj.Destroy()
end


-----------------------------------------------------------
--  Pause / Continue
-----------------------------------------------------------

on PauseGame (me)
  State = #Paused
  Stack = []
  GameObj.FlashGameState()
  GameObj.DisplayMessage("Paused")
end

on ContinueGame (me)
  State = #Playing
  Stack = [GameObj]
  GameObj.FlashGameState()
  GameObj.DisplayMessage("Continuing")
end


-----------------------------------------------------------
--  Event Handling
-----------------------------------------------------------

on enterframe (me)
  call(#Update, Stack)
end


on mouseDown (me)
  call(#MouseClick, Stack)
end

This version of the behaviour also has a State property used to track the state of the game system (#stopped, #initialising, #ready, #playing, #paused). So far, we are only interested in checking whether the system is in a #ready condition before sending a StartGame message.

When the behaviour receives the GameReady, it could just start the game. However, in this version - rather than starting immediately, it asks the user to click the screen to start. This ensures that Shockwave gains focus and will thus be able to detect keypresses.

The clickToStart script is another utility script with one simple purpose in life: Receive a mouseDown on a sprite, send a StartGame message to that sprite, and then delete itself from the sprite. It looks like this:

-- [clickToStart Behaviour ]

on mouseDown (me)
  started = sendSprite(me.spriteNum, #StartGame)
  if started then
    -- this instance's work is done now, so kill it
    -- (should only be one instance on the sprite, but
    -- delete any others just to be on the safe side)
    sil = sprite(me.spriteNum).scriptInstancelist
    repeat while sil.getOne(me)
      sil.deleteOne(me)
    end repeat
  end if
end

A Quick Summary

So far, this tutorial has outlined how we have separated the main game script from a behaviour to connect the game to the score. To create a game from the scripts and start the game, the following steps occur:

1. On beginSprite, the behaviour (creatively called "Game Behaviour") will create a new instance of the main game script (the 'GameObj').

2. The behaviour will respond to a NewGame message by sending a NewGame message to the GameObject.

3. When the GameObject has finished getting ready (creating other objects, loading a map etc), it sends a GameReady message to all sprites.

4. When the Game.Behaviour receives the GameReady message, it loads up instance of a another behaviour (the "clickToStart" behaviour) which listens for a mouseClick.

5. When the mouse is clicked, this second behaviour sends a StartGame message to the Game Behaviour which then loads the GameObject into a 'Stack' and starts sending Update and MouseDown messages to the GameObject.

6. To pause the game, we can send PauseGame messages to the Game.Behaviour which will temporarily remove the GameObject from its 'Stack' (so the Game.Object stops receiving Update messages).

Last updated 10th November, 2005

© 2006 MeccaMedialight. Site Powered by Wrangler 8.