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