Statechart Data 1 - Global and Machine Storage

In the previous post, i gave an example of a series of graphical menus, and asked how exactly do those statecharts interact with the menuing system, and specifically: how do statecharts create and manipulate user interface data?

Let's start right at the very top with the outermost menu:

   Main Menu:
   - entry / display_options();
   - button[0] >> New Game.
   - button[1] >> Continue Game.
   - button[2] >> Options.

And dig into some pseudo code.

First, lets assume we have, out there in the rest of the code, a UI system of some sort -- be it Flash, UIKit, or some custom solution. The point is only that we expect to be able to create some sort of interactive screen, and -- based on the above chart's call to "display_options()" -- that the system will allow us to dynamically add some buttons to that screen. Additionally, most modern UI systems communicate user input via events ( or at the very least can be adapted to do so ), and we expect the same here.

For discussion's sake, lets say the UI system looks something like:

   class UISystem {
      UIMenu create_new_menu( String path_to_background_asset );
      UIEvent get_next_event();
   };
   class UIMenu {
      Button add_button( String label );
   };

Let's also assume we have some sort of representation for the state machine itself. Since statemachines are reactive systems that make choices based on incoming events, we can imagine it has at least one method: process_event().

   class Statemachine {
      Statemachine( State first_state );
      void process_event( UIEvent );
   };

Finally, since this is pseudo code, we'll just ignore memory handling, thread starvation, and a hosts of other issues, and say this is our main loop:

    UISystem ui;
    Statemachine machine(first_state);
    while ( 1 ) {
       machine.process_event( ui.get_next_event() );
    }

That's all well and good, events are flowing from the ui system to the state machine system, but what does our first state look like? And how does how does "machine" get access to "ui"?  Here's some possible pseudo-code for entry into the main menu, and the action to display the options to the screen.

   void main_menu_entry(1) ( UISystem ui(2) ) {
      UIMenu menu= ui.create_new_menu(path_to_assest); 
      (3)
   }
   void display_options(1) ( UIMenu menu(2) ) { 
      Button b1= menu.add_button("New Game");
      Button b2= menu.add_button("Continue Game");
      Button b3= menu.add_button("Options");
      (4)
   }

Specifically:
  1. Are these free functions or part of a class?
  2. Who's calling these functions? How do the functions get the needed pointers?
  3. entry() could obviously call display_options() directly, passing across the "menu" pointer; but, part of the idea of a statechart is that the *machine* is calling the actions associated with the actions. If that's so: how does display_options() get that pointer?
  4. If add_button() returns handles or ids needed for hit detection, where does it store it's data?
If this were C, then all of our functions would naturally be global, and possibly some of our data. However, in C++, and most other object oriented languages, we can choose whether the functions and data are part of, or separate from, a class.

The answers to our questions, therefore, depends, slightly, on whether we have classes (or, at least structured data) to represent our machine and/or our states. These considerations lead to three obvious places where we can store data and communicate data, as well as two perhaps less obvious places:
  • Globally -- as free floating functions and data;
  • Inside of, or tightly bound to, the representation of the machine class.
  • Inside of, or tightly bound to, the representation of the individual states;
  • Inside the ui elements themselves, ex. store buttons in a derived menu;
  • In the events that are passing back and forth between the UI and the machine.
Let's take a look at these options.

Global Storage:

Global data ( aka the aether ) has a bad reputation for good reason: things tend to get messy quick. If all your states are all storing data to, and getting data from, global variables, well... i’ve seen it done. Good luck to you in your adventurous career... you're keeping contractors like me in business.

That said, globals can be appropriate in some situations: singletons, or perhaps grouping similar states together into a common file and using local statics might be okay for limited cases. In this case, no one would blame us if we defined something like:
   
   class UISystem {
      static UISystem GetSingleton();
      // ...
   };

   void main_menu_entry() {
      UISystem ui= UISystem::GetSingleton();
      ui->create_menu( path_to_asset );
   }

But, if we started littering the global context with arrays for specific menu's buttons, something is probably wrong.

The one interesting thing about global singletons is that you can use on/exit to states to initialize/destroy them if you don’t want them around all the time.

   Game:
   - entry/ create_ui_system();
   - exit / destroy_ui_system();

Machine Storage:

Almost every state machine implementation i've looked at makes either the machine itself accessible during event processing, or a user defined context object that is associated with the machine. Most implementations allow, or even encourage, sub-classing from one of these two objects to add storage.

Samek's Quantum/QP; both of Boost's implementations: MSM & Statechart; the SMC (statemapcompiler); and more; all provide this method of data storage.

In this main menu example, the setup might look something like:

   class Context {
      UIMenu menu;
      List<button> buttons;
   };

   // with inheritance, maybe this
   class MyMachine:StateMachine {
      Context context;
      void process_event( Event );
   };
 
   // or, w/o inheritance, maybe this:
   class StateMachine {
      void process_event( Context, Event );
   };

Regardless of inheritance, and regardless of *precisely* how, in these sorts of state machine systems, every the context is closely associated with the machine, and every event handling function  ( in these sorts of systems ) receives both the event and the context.

   void main_menu_entry(Context ctx) {
      UISystem ui= UISystem::GetSingleton();
      ctx.menu= ui->create_menu( path_to_asset );
   }

The downside is: any piece of data you need, for however short a time, needs a member there. Maybe you don't need menus once you get into the heart of your game.  Too bad. If you want the same statemachine to control the pre-game, and the in-game -- you will have a menu pointer in "Context" forever.  Everyone sees all your data all the time, and the bigger your machine gets the more data will clutter it up.

Although this is common practice, it doesn’t feel (much) better to me than storing data globally. My personal opinion is that people who have tried using statemachines for UI get turned off by how messy their data becomes. The state solution doesn't feel cleaner than a non-state solution.


In the next couple of posts, I'll look at some of the other options to help mitigate this messiness.

0 comments: