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

First published 22/10/2005