Saturday, February 12, 2011

Game state management using cooperative multitasking

The game state management sample on the App Hub shows how to organize a game in screens. I strongly recommend all new-comers to game programming to study it, regardless of their amount of programming experience. Modern graphical non-game applications seldom follow this pattern, with the notable exception of step-by-step wizards.

The overview of this sample states:


The sample implements a simple game flow with a main menu, an options screen, some actual gameplay, and a pause menu. It displays a loading screen between the menus and gameplay, and uses a popup message box to confirm whether the user really wants to quit.
The ScreenManager class is a reusable component that maintains a stack of one or more GameScreen instances. It coordinates the transitions from one screen to another, and takes care of routing user input to whichever screen is on top of the stack.

A typical problem with this sample is that it is sometimes difficult to handle transitions from a screen to the next screen in the game flow.
For me, it's easier to specify the flow of screens from a bird's view:
The game starts with the press start screen, then the main menu is showed. Depending on the entry selected, the game may go into actual gameplay, show a credits screen...
When you implement your game, this bird view is not available by default. Instead, it's up to each screen to tell the screen manager which is the next screen to show when it's time for the first screen to remove itself. It's not unlike continuation-passing-style, in a way.

It is possible to do the inter-screen wiring from the top using events and handlers, but I find that using events and handlers for workflows is a bit ugly. The introduction of async in C# 5 indicates I'm not alone thinking that way.

To make things worse, providing the next screen to show with data it needs to initialize itself can get tricky if not carefully planned during the design phase. It also tends to cause "field overgrowth" in screen classes.

For all these reasons, I've been looking for a nicer solution using Eventually workflows and cooperative multitasking.

From the original game state management I have kept screens and the screen manager (on a conceptual level, implementation is new). These encapsulate state well, and I like the decomposition of a game into individual screens.

In particular, screens each have their ContentManager and load specific content when added to the screen manager, and release assets when removed. Sharing a ContentManager and keeping assets in memory is of course still possible using conventional methods.

The significant change is the removal of Update() and HandleInput() methods from screens. Instead, each screen has a number of Eventually computation expressions (which I also call tasks) which implement the logic of the screen.

To give a more concrete picture of the whole thing, here are bits from the MenuScreen. For full code, see github.

type MenuScreen<'I when 'I : equality>(
   player : PlayerIndex,
   sys : Environment,
   items : ('I * string)[],
   anim : AnimationParameters,
   placement : PlacementParameters) =

'I is a type parameter used to distinguish between entries. For instance, this discriminated union can be used for the main menu of a typical game:

type MainMenuEntries =
    | Play
    | Instructions
    | Options
    | Scores
    | Credits
    | BuyFullVersion
    | Exit

Back to MenuScreen, each constructor argument has the following role:
  • player is the index of the player who has control, i.e. the player who pressed "start" on the "press start screen"
  • sys is the object used to interact with the scheduler, which is used to spawn new tasks, wait, yield control...
  • items is a list of (menu entry - text) pairs
  • anim is a record whose fields control fade-in and fade-out effects
  • placement is another records controlling where the menu is rendered

inherit ScreenBase()

    // A mutable cell holding the index of the entry currently selected.
    let current = ref 0

    // An object wrapping a number of variables controlling fading effects.
    let animation = new Animations.MultipleFadeIn(sys, items.Length, anim.period, anim.shift)

    // Keeps track of button presses on the gamepad of the player with control
    let input = new InputChanges.InputChanges(player)

    // Array keeping track of the state of visibility of each entry.
    // Typically entries are visible, but some must be hidden or showed as gray
    // depending on whether the user is running the full version of the game
    // or the trial.
    let visibility = items |> Array.map (fun _ -> Visible)

    do if items.Length = 0 then invalidArg "items" "items may not be empty"

In the full code functions dealing with menu entry visibility follow, I'm skipping them in this article.

The really interesting part (IMHO) comes now:

member this.Task = task {
        this.SetDrawer(this.Drawer)

        let animator = sys.Spawn(animation.Task)

        let selected = ref false
        let backed = ref false
        while not (!selected || !backed) do
            // If this screen is not active, i.e. it is not on top or the guide is visible, wait.
            // We don't want to react to input that's not for us.
            do! sys.WaitUntil(fun () -> this.IsOnTop)

            input.Update()

            if input.IsMenuDown() then moveDown()
            elif input.IsMenuUp() then moveUp()
            elif input.IsStartPressed() then selected := true
            elif input.IsBackPressed() then backed := true

            do! sys.WaitNextFrame()

        animator.Kill()
        do! sys.WaitUntil(fun() -> animator.IsDead)

        return
            if !selected then items.[!current] |> fst |> Some
            else None
    }

If you are not familiar with F#, it's worth pointing out that "!selected" means the value of mutable cell "selected", not the negation of the value of variable "selected". I keep getting confused by that when I read F# code even today.

If you have read my earlier articles about the Eventually workflow and cooperative multi-tasking, it should be clear what this code does. Critics may point out that it does not differ much from the typical content of HandleInput in the C# game state management sample, and they would be right.
A small but important difference resides in the last lines, composed of a return statement.

Interacting with the rest of the program is greatly simplified, as shown by this code snippet:

use menu =
            new MenuScreen<_>(
                controlling_player,
                sys,
                [| Play, "Play now"
                   Instructions, "How to play"
                   Options, "Options"
                   Scores, "Scores"
                   Credits, "More information"
                   BuyFullVersion, "BuyFullVersion"
                   Exit, "Exit" |],
                menu_animation,
                menu_placement
            )

        // Show the menu
        screen_manager.AddScreen(menu)
        // Execute the menu's code, and get the selected item as a result
        let! action = menu.Task
        // We are done with the menu, hide it.
        screen_manager.RemoveScreen(menu)

        // Deal with the selection in the menu.
        match action with        
        // Back to the board.
        | Some Exit ->
            exit_game := true


If you are like me, you may have experienced that dealing with seemingly trivial tasks such as showing and saving highscores after "game over" is more pain than it should be. It involves multiple screen transitions and asynchronous file I/O that must be spread over multiple update cycles.

Here how it looks in F#:

// Deal with the selection in the menu.
match action with        
// Back to the board.
| Some Exit ->
    exit_game := true
        
// Start playing.
| Some Play ->
    // Create the screen showing the game.
    use gameplay = new GameplayScreen(sys, controlling_player)

    // Show the gameplay screen. Gameplay itself is in gameplay.Task
    let! gameplay_result = screen_manager.AddDoRemove(gameplay, gameplay.Task)

    // If the game wasn't aborted, and if a new high score was achieved,
    // add it to the score table and show the table.
    match gameplay_result with
    | Aborted _ -> ()
    | TooEarly (_, points) | TooLate (_, points) ->
        use results = new ResultScreen(sys, controlling_player, gameplay_result)
        do! screen_manager.AddDoRemove(results, results.Task)

        let player_name =
            match SignedInGamer.SignedInGamers.ItemOpt(controlling_player) with
            | None -> "Unknown"
            | Some player -> player.Gamertag

        let is_hiscore = (!scores).AddScore(player_name, points)

        if is_hiscore then
            // save the scores.
            if storage.TitleStorage.IsSome then
                do! storage.CheckTitleStorage
                let! result =
                    storage.DoTitleStorage(
                       score_container,
                       saveXml score_filename !scores)

                match result with
                | Some(Some()) -> ()
                | _ -> do! doOnGuide <| fun() -> error "Failed to save scores"
            // Show the scores screen.
            do! showScores

There is a lot more to be said, but that's enough for a single article. I think that these code extracts show how F# can simplify the development of games. If you are a C# programmer used to think "functional programming is too complicated for what I need", I hope I have managed to introduce the idea there are clear benefits to be gained by introducing F# in your toolbox.

This is still work in progress, you can follow it on bitbucket.
You can also follow me on twitter, I am @deneuxj there. The full source code of a small game demonstrating task-based game state management is also available on github, under Samples/CoopMultiTasking.
This game demonstrates the following features:
  • A typical "press start screen -> menu -> game" flow
  • Safe IO using StorageDevice, used for best scores and user preferences
  • Throwing back to the "press start screen" when sign-outs occur
  • Screen transitions
  • Input handling and pausing (not complete at the time of writing)
  • Content loading and unloading