Sunday, July 1, 2012

Tutorial 5 - Finite State Machines (Update 23/01/16)

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.
Even if you want to include some of the funkier game AI techniques like fuzzy logic or neural networks, FSM's form a solid foundation for the incorporation of these.


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

5 comments:

  1. 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!

    ReplyDelete
    Replies
    1. Hi Josh,

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

      Delete
  2. 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.

    ReplyDelete
    Replies
    1. Found it! To my shame I think it was a spelling error. :(

      Delete