Trending
Opinion: How will Project 2025 impact game developers?
The Heritage Foundation's manifesto for the possible next administration could do great harm to many, including large portions of the game development community.
Recently I found some time to polish on my state chart library presented in the first post...
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 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
You May Also Like