Sponsored By

UML State Charts for C++ continued

Recently I found some time to polish on my state chart library presented in the first post...

Ivica Aracic, Blogger

September 11, 2010

3 Min Read
Game Developer logo in a gray background | Game Developer

Now that our current game project has reached the finishing line, I found some time to polish on the state chart library which I outlined in the first post. I also tried it in the real life context and I gained the following insights:

1/ I gave up searching for a perfect textual representation of the state chart. It is hopeless. As soon as applied to real life examples, it simply gets messy. Because of: a) the essential complexity of representing complex nested graph structures and b) accidental complexity of representing it as text. 

2/ I gave up implementing the full UML state chart specification of the form

SOURCE -- event [guardCondition] /action --> TARGET.

Instead of that, I changed it to:

SOURCE -- [transitionCondition] /action --> TARGET

Basically, summarizing event [guardCondition] to [transitionCondition].
This makes it easier to use the library in system which are not event-driven.

I didn't investigate this further, but I think "event [guardCondition] /action" could be seen as a specification of the more generic  "[transitionCondition] /action" definition.

3/ I got rid of macros in definition, because it gets messy easily. Instead of that I use overloaded >>, <<, [], and / operators to specify the transition rules.

SOURCE [transitionCondition] /action >> TARGET;

TARGET [transitionCondition] /action << SOURCE;

4/ Finally, I implemented nested states, which significantly contribute to reducing the size of the state chart definition.

 

The following image depicts the (reverse engineered) state chart of the PlantAttack object from our game (-> http://www.chick-chick-boom.com/). I apologize for the free-style notation, however, I wasn't able to find a good tool capable of representing state charts with nested states.

PlantAttack State Chart

PlantAttack State Chart

PlantAttack in Action

PlantAttack in Action

The code bellow shows the definition in the code:

PlantAttack::StateMachineDef* PlantAttack::getStateMachineDef() {
    ...
    static State      Init ...;
    static State      Inactive ...;
    static State      Active ...;
    static StateFnPtr Active_Approaching ...;
    static StateFnPtr Active_Sprout (
        ATTACK_PLANT_MODE_SPROUT,   // int id of this node
        "Sprout",                   // string id of this node
        ...                         // ...other parameters...
        NULL,                       // no update function
        &PlantAttack::sSproutEnter, // enter function 
        NULL);                      // no exit function
    ...
    static StateFnPtr Disappear	...;
    static State      Dead ...;

    Init     >> Inactive;
    Inactive >> Active;
    Active              [gc(TakeFireDamage) 
                         && gc(TakeDamage)] /tf(TakeDamage) >> Charred;
    Charred             [gc(Timeout)]                       >> CharredDisappear;
    Active              [!gc(TakeFireDamage)  
                         && gc(TakeDamage)] /tf(TakeDamage) >> Rot;
    Inactive            [gc(Timeout)]             << CharredDisappear;
    Active_Approaching  [gc(Timeout)]             >> Active_Sprout;
    Active_Sprout       [gc(Timeout)]             >> Active_BiteCooldown;
    Active_BiteCooldown [gc(BarrierInteraction)]  >> Active_Grow;
    Active_BiteCooldown [gc(Timeout)]             >> Active_Effective;
    Active_Effective    [gc(Starved)||gc(FinishRequested)] >> Rot;
    Rot                 [gc(Timeout)]             >> AfterRot;
    AfterRot_Falling    [gc(HeadAtFloor)]         >> AfterRot_Lie;
    Disappear           [gc(LastDrawPointZero)]   << AfterRot_Lie;
    Inactive            [gc(Timeout)]             << Disappear;
    Active_Effective    [gc(BarrierInteraction)]  >> Active_Grow;
    Active_Effective    [!gc(BarrierInteraction)] << Active_Grow;
    Active_Effective    [gc(BitableInRange)]      >> Active_BitePrepare;
    Active_BitePrepare  [gc(Timeout)]             >> Active_BiteExecute;
    Active_BiteCooldown [gc(Timeout)]             << Active_BiteExecute;
    ...
}

/* STATE FUNCTIONS (UPDATE/ENTER/EXIT) */
void PlantAttack::sRot(float dTime) ...
void PlantAttack::sAfterRot(float dTime) ...
void PlantAttack::sFallingEnter() ...
void PlantAttack::sFalling(float dTime) ...
void PlantAttack::sSproutEnter() ...
void PlantAttack::sBiteExecuteEnter() ...
void PlantAttack::sBiteExecuteExit() ...
void PlantAttack::sGrow(float dTime) ...
void PlantAttack::sGrowExit() ...

/* TRANSITION FUNCTIONS */
void PlantAttack::tfTakeDamage() ...

/* GUARD CONDITIONS */
bool PlantAttack::gcBitableInRange(State* ctx) ...
bool PlantAttack::gcLastDrawPointZero(State* ctx) ...
bool PlantAttack::gcHeadAtFloor(State* ctx) ...
bool PlantAttack::gcStarved(State* ctx)	...
bool PlantAttack::gcFinishRequested(State* ctx) ...
bool PlantAttack::gcBarrierInteraction(State* ctx) ...
bool PlantAttack::gcTakeFireDamage(State* ctx) ...
bool PlantAttack::gcTakeDamage(State* ctx) ...
bool PlantAttack::gcTimeout(State* ctx)	...

Note: in order to simplify the creation of GuardConditions and TransitionActions, I define two simple macros called gc and tf. gc simplifies the creation of GuardCondition objects, which point to member functions of type bool(*)(State*) and tf simplifies the creation of TransitionFunction objects, which point to member functions of the type void(*)().

Implementation and a simple example can be found here: gosm.zip

Read more about:

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

You May Also Like