Sponsored By

UML State Charts for C++

UML State Charts provide a clean and readable top-level view of object's behavior. In this blog I share my thoughts on a clean, minimalistic and readable implementation of state charts in C++.

Ivica Aracic, Blogger

June 11, 2010

13 Min Read

Introduction

 

UML State Charts are really cool. In fact, when it comes to modeling new game objects and its behavior, the UML state chart is the first thing that I start with. They provide a clean and readable top-level view of object's behavior. However, when the coding starts, things get more and more messy. As the code evolves, the diagram gets out of sync if not updated correspondingly (we all know about documentation going out of sync with code :-)). In other words, it gets useless. At the end the code is the only place where one can get a complete and correct description of the behavior. However, it is far away from having the same readability as a state chart diagram.

 

The solution is to bring state charts as an explicit definition to the C++ code and make it executable. I googled for C++ frameworks which do exactly that, but everything looked so heavy-weighted or unreadable (e.g. if...else if / switch constructs, class explosion, ...).  Finally, I gave up the search and started implementing my own solution. 

 

Before I go into details, let me start with an example: In our current game project, we have a game object called PlantAttack. Basically, it is a flower, which appears in the level and then it starts to bite creatures in its range. If the player draws a line, the flower starts growing along the line. If creatures get out of the range, the plant starves and finally it dies.

 

 

UML State Charts in Plain Text

 

The first problem I came across is: how to make a readable plain text version of a UML state chart? After experimenting for a while, the following definition emerged for the PlantAttack:

 

Init --> [Always] /NoAction --> Inactive

Inactive,

  --> [Always] /NoAction, 

  --> Appear

      Appear

        --> [Timeout] /NoAction

        --> Effective

            Effective --> [LineContact] /NoAction  --> Grow

            Effective <-- [!LineContact] /NoAction <-- Grow

            Effective

              --> [CreatureInRange] /BiteAction

              --> Bite

                  Bite --> [Timeout] /NoAction --> BiteCooldown

            Effective <-- [Timeout] /NoAction  <-- BiteCooldown

            Effective --> [Starved || Beheaded] /NoAction --> Disappear

Dead <-- [Timeout] /NoAction                              <-- Disappear

 

What we have here in general, is a plan text representation of the UML state chart as a set of unique transition rules of the form (nesting ignored so far):

 

sourceState --> [guardCondition] /action --> targetState

 

From the UML specification:

  • guardCondition is a boolean expression. If it evaluates to true the transition takes place

  • /action is the function executed when the transition is triggered.

 

In summary, it is impossible for the plain text description of the diagram to be more readable as its graphical representation, however,  applying the following formatting rules, we get very good results:

  • Keep equal or similar states aligned on the same column

  • Use reversed direction when returning from a state

  • Break and indent lines which would contribute to long indentations

  • Use a small indentation, e.g. two space characters

 

 

Framework

 

The following code describes the key framework classes. GoStateMachineDef serves as a container for transition rules. The State interface requires doEnter (called when the state is entered), doExit (called when the state is exited), and finally the doExec method (the update method called every frame). GuardCondition and TransitionAction are also pretty straight forward.

 

class GoStateMachineDef {

public:

void GoStateMachineDef::addTransition(

State* state1, const char* dir, State* state2, 

GuardCondition* gc, TransitionAction* ta);

...

};

class State {

public:

virtual void doExec() = 0;

virtual void doEnter() = 0;

virtual void doExit() = 0;

};

class GuardCondition {

public:

virtual bool eval() = 0;

};

class TransitionAction {

public:

virtual void exec() = 0;

};

 

Now, given this framework, we can start implementing the PlantAttack state chart. In order to avoid class explosion (every state having its own class), we provide a generic implementation of the State interface which supports function pointers to class members. Furthermore, we do the same for GuardConditions and TransitionActions.

 

Finally, the code looks something like this.

 

class PlantAttack {

GoStateMachineDef* createStatemachineDefinition() {

GoStateMachineDef* smDef = ...

State* Appearing = new StateMemberFnPtr(

&PlantAttack::stateAppearing,

&PlantAttack::stateAppearingEnter,

&PlantAttack::stateAppearingExit);

State* Effective = ...

...

smDef->addTransition(

Appearing, "-->", Effective, 

new GuardConditionMemberFnPtr(

this, &PlantAttack::gcTimeout),

new TransitionFnMemberFnPtr(

this, &PlantAttack::tfNoAction));

...

return smDef;

}

 

// STATE ENTER/EXIT/EXEC FUNCTIONS

void stateAppearing() {...}

void stateAppearingEnter() {...}

void stateAppearingExit() {...}

 

void stateEffective() {}

void stateEffectiveEnter() {...}

void stateEffectiveExit() {...}

 

...

 

 

// GUARD CONDITIONS

bool gcTimeout() {...}

bool gcAlways() {return true;}

...

 

// TRANSITION ACTIONS

void tfNoAction() {}

...

};

 

 

Syntax Sugar

 

Now, that we have the framework, let us introduce some syntax sugar for  GoStateMachineDef::addTransition calls which easily get messy and unreadable. For this purpose, we use macros of the form:

 

T_(sourceState,-->,GuardCondition,TransitionAction,-->,targetState)

 

where T_ is defined as:

 

#define T_(_state1, dir1, _gc, _transFn, dir2, _state2)\

smDef.addTransition(\

_state1, #dir1, _state2,\ 

_new GuardConditionMemberFnPtr(\

this, &PlantAttack::gc##_gc),\

new TransitionFnMemberFnPtr(\

this, &PlantAttack::tf##_transFn)))

 

We use naming conventions for guard conditions (gc-prefix) and transition functions (tf-prefix). Moreover, dir2 is only for the readability purposes, only dir1 argument is used.

 

Finally, we end up having the code which is really close to our desired plain text definition of the state chart:

 

T_(Init,-->,Always,NoAction,-->,Inactive);

T_(Inactive,

     -->,Always,NoAction, 

     -->,Appear);

T_(      Appear,

           -->,Timeout,NoAction, 

           -->,Effective);

T_(            Effective,-->,LineContact,NoAction,  -->,Grow);

T_(            Effective,<--,NoLineContact,NoAction,<--,Grow);

T_(            Effective,

                 -->,BitableInRange,BiteAction,

                 -->,Bite);

T_(                  Bite,-->,Timeout,NoAction,-->,BiteCooldown);

T_(            Effective,<--,Timeout,NoAction, <--,BiteCooldown);

T_(            Effective,-->,StarvedOrBeheaded,NoAction,-->,Disappear);

T_(Dead,<--,Timeout,NoAction,                           <--,Disappear);

 

 

Thats all for now. Thank you for reading my first gamasutra blog and I hope you may find this article useful for your own project. Comments welcome. Mail me if you want to have the complete source code.


Read more about:

Blogs

About the Author(s)

Daily news, dev blogs, and stories from Game Developer straight to your inbox

You May Also Like