Statechart Data 3 - Instance Data

In the introductory post, I used the example of a game's main menu to highlight some of the issues with data that programmers have to worry about when working with state machines.  Subsequent posts have looked at different places to store and retrieve data in order to address those issues. First, was global and machine storage, followed by using events ( particularly the state entry event ) to bounce data from place to place.

That leaves just two remaining categories: state instance data and model data.


State Instance Data

Much as machine storage associates data with the overall state machine, state instance storage associates data with the currently processing states.

In the previous rendition of the main menu state machine, we bounced the list of saved games ( "saves" ) from event to event even within the same state.  Here's was the example from last time:
   Continue Game:
   - init >> Load Saves.
      Load Saves:
      - entry/ load();
      - loaded(saves) >> Select Game(saves). 
      Select Game:
      - entry(saves) / display_saves(saves);
      - selected(saves+choice) >> Confirm Choice(saves+choice).
      Confirm Choice:
      - entry(saves+choices) / prompt(saves+choice) 
      - response(saves+choice) [no]  / >> Select Game(saves).
      - response(saves+choice) [yes] / start_game(choice) >> Run Game(choice). 

Note how in Select Game, in order to transfer the list of the saves to the next state, the list of saves were bounced back to Select Game via the "selected" event. The advantage of passing the saves via the selected event was that neither the global namespace (nor the state machine object) were required to store that data. Avoiding the use of globals, or even the state machine object, stops the code from getting cluttered. When there's just a few screens, the occasional stray pointer declaration probably doesn't matter much, but the more screen you have, the more cluttered things will become.

The event that helps stop the clutter might look something like the following:

  class SavedGameSelectedEvent:Event {
      list<SavedGame> saves;
      int selectedSave;
   };

Arguably, it's not pretty. What we really want, is a generic selected event, something that looks like a standard UI event from your everyday non-statechart UI system. Ideally, something as simple as:

  class SelectedEvent:Event {
      int selected;
   };

If we augment states with instance data, we can do just that. We can store the "saves" inside Select Game for the duration of Select Game, and we can keep the "selected" event generic to promote code reuse:

   Select Game:
   { list<SavedGame> saves; }
   - entry(saves) / this->saves= saves; display_saves(saves);
   - selected(choice) >>  Confirm Choice(this->saves+choice).


Leveraging Hierarchy

Statechart hierarchy enable us to be "in" more than one state at a time. So far in the main menu example, there hasn't been a strong need for hierarchy, but now we can use it to further simplify our events.

The following example introduces a new state "Choose Game". Instead of representing a particular piece of UI on the screen as the other states do, Choose Game represents the state of having a list of saved games loaded. By making Select Game and Confirm Choice sub-states of Choose Game they can reference their parent state in order to access that list of saves.

   Continue Game:
   - init >> Load Saves.
      Load Saves:
      - entry/ load();
      - loaded(saves) >> Select Game(saves). 
      Choose Game:
         { list<SavedGame> saves; }
         - entry(saves) / this->saves= saves;
         Select Game:
         - entry() / display_saves(parent->saves);
         - selected(choice) >> Confirm Choice(parent->saves[choice]).
         Confirm Choice:
         - entry(choice) / prompt(parent->saves+choice); 
         - response(choice) [no]  / >> Select Game.
         - response(choice) [yes] / start_game(choice) >> Run Game(choice). 

In global data ( or the state machine object ), we have the pointer to the menuing system itself, in the "loaded" event we have returned the list of saves that were loaded, and in the Choose Game state we have stored the saves for the duration that they are needed. Three different methods of data storage working together with a nice separation of responsibilities.  In the final post, I'll look at "model" storage, and show how it rounds out our ability to work with state machines.

Instance Storage Implementation

This all does beg the question how states ( and their data ) are actually represented in code, and the answer, of course, depends on the state machine implementation you are using.

Since states sit passively waiting for events to occur, the bare minimum needed to process an event is simply a hook into the code that evaluates the event, runs the desired actions in response, and points the hook to the next state when needed ( Samek's Quantum/QP ). Some implementations use just a function pointer or an enum per state to represent that hook; others, like the SMC or Boost.statechart use classes: one class per state.

Obviously, implementations that uses classes are more amenable to having instance data. The downside of using classes to represent states, however, is the allocation cost associated with creating and destroying the class instances. Both SMC and the Boost MSM use singletons for their states to avoid that cost; Boost.statechart embraces allocation fully by using class constructors to represent state entry, and instance destruction to handle state exit.

For systems that don't provide instance data already, a simple hash table can provide a quick and dirty way of associating states to instance data.

In a later post, I'll describe something I call a "context stack" which provides a structured alternative to hashing for state machine implementations that don't have instance data readily available. It's low overhead when you want instance storage, and no overhead (ish) when you don't.

0 comments: