Webcomic Courtesy of Ethanol & Entropy
5.1 Introduction to Finite State Machines (FSM).
Just about every game includes some sort of Finite State Machine (FSM). It is the Developers go to structure for keeping track of what state the game is in and can be used to simulate intelligent behaviour. FSM's are popular because they are:
- Simple to code;
- Adaptable & flexible;
- Easy to debug and maintain;
- Quick; and
- Easy to understand.
5.2 What is a FSM?
FSM's were originally invented to solve mathematical problems. It is an abstract machine that can be in one of a finite number of states. The machine can only ever be in one state at a time which is called the current state. You need an event to occur to transition from the current state to a new state. A simple example of a FSM is a switch (see Figure 1). It has two states on and off. Transition from one state to another occurs when your finger flicks the switch. Note this is a European switch, in Australia they work the other way around (i.e. down for on and up for off).
Figure 1.
5.3 Implementing a FSM.
There are a number of different approaches to implementing a Finite State Machine. The easiest way is to use a series of if-then statements. We don't have a switch statement in Lua or enumerated types, so we can't use those.
In our Spacewar example we could add the following in our Main class setup() function. You need to have added the Twinkle class discussed in Interlude 5. This will simulate the game running portion of the game for now.
function setup()
-- Keep track of Game State
stateSplash = 0
stateMenu = 1
stateRun = 2
stateWon = 3
stateLost = 4
gameState = stateSplash
-- Create our menu button
button = Button(" Start ")
button.action = function() buttonPressed() end
-- Create the splash screen
splashScreen = SplashScreen("Spacewar!", 10)
-- Create the twinkling star background
twinkleBackground = Twinkle(100)
end
And then in our draw() class, we could add:
function draw()
-- Set background to black
background(0, 0, 0)
-- Draw the appropriate screen based on gameState
if gameState == stateSplash then
splashScreen: draw()
elseif gameState == stateMenu then
drawButton()
elseif gameState == stateRun then
twinkleBackground: draw()
end
end
The drawButton() and touched(touch) functions are unchanged from the previous tutorial. Modify buttonPressed so that it looks like this:
function buttonPressed()
-- If the menu button is pressed we transition to the next state.
gameState = stateRun
end
Finally in the SplashScreen class you need to modify the fadeAnimationDone() function as follows:
function fadeAnimationDone()
-- Call back function for Fader complete
-- Splash screen done, transition to menu state
gameState = stateMenu
end
And you are done, try it out. Your game is now operating as a Finite State Machine. The problem with this approach is that as your game gets more complex you can end up with spaghetti code, which is ok in the kitchen but not so good on the iPad.
5.4 Alternate Approach
@aciolino over on the Codea Forums uses an alternate approach which we really like. We will let him explain in his own words...
Changing the GAMESTATE variable will immediately change the state of the app to whatever screen you want it to go to, all defined in the GAMESTATES table.
Note that this change is IMMEDIATE, so if you didn't set up some values in the destination screen, you're likely to get frustrated quickly. That's why I have a call to ScreenSplash:init() BEFORE I change the GAMESTATE variable.
Also, the screens that you redirect to WILL get the draw() and touched() calls, unlike the comments that Codea generates when you create a new class.
--
-- Main.lua
--
-- Sample of Cofender's main()
function setup()
displayMode(FULLSCREEN)
-- define consts
GAMESTATE_MENU = 1
GAMESTATE_PLAYING = 2
GAMESTATE_SHIPEXPLODE = 3
GAMESTATE_LEVELCOMPLETE=5
GAMESTATE_ENDED = 4
GAMESTATE_SPLASH =6
GAMESTATE_OPTIONS =7
GAMESTATE_PAUSED = 8
GAMESTATES = { ScreenTitle, ScreenGame, ScreenShipExplode, ScreenEnd,
ScreenLevelComplete, ScreenSplash, ScreenOptions , ScreenPaused }
ScreenSplash:init()
GAMESTATE = GAMESTATE_SPLASH
end
-- This function gets called once every frame
function draw()
-- This sets a dark background color
background(0, 0, 0, 255)
--state machine
GAMESTATES[GAMESTATE]:draw()
end
function touched(touch)
--state machine
GAMESTATES[GAMESTATE]:touched(touch)
end
--to send keys to proper screen for processing
function keyboard(key)
GAMESTATES[GAMESTATE]:keyboard(key)
end
Hey David! So when we do these examples, do you suggest that we 'recreate' (copy/paste) previously made classes, or just mark them as dependencies? Thanks!
ReplyDeleteHi Josh,
DeleteWhat I started doing was collecting all the reusable classes into a library which would contain the latest version and mark the ones I needed as a dependency. Have a look at Tutorial 9 (http://codeatuts.blogspot.com.au/2012/07/tutorial-9-dependencies-libraries.html).
However, the current Codea runtime doesn't support dependencies so this model is no good for Apps you want to submit to the App store. Because of this I usually copy classes I need into a new project - but I would use dependencies if I could. For doing these tutes I would definitely make use of dependencies.
And I keep getting that nagging nil error on the buttonPressed() call, it work in my splashScreen project but not here, I will continue the examination.
ReplyDeleteFound it! To my shame I think it was a spelling error. :(
DeleteGood debugging practise...
Delete