fsm compiler

All of the code and documentation posted here, is posted with the permission THQ who owns this compiler. I want to thank them for letting me share this with the game development community.

I implemented a finite state machine AI system for a game, THQ's Sinistar Unleashed. I wrote a "state machine" compiler that takes code written in C++ with the addition of special language features to support state machines. The compiler converts the code into normal c++ that can then be compiled with a commercial c++ compiler. This compiler supports concurrent hierarchical state machines. Hierarchical state machines, as you know, are state machines where every state may itself contain a complete state machine. By concurrent I simply mean that a state machine may actually be composed of several state machines each of which receives the same messages. ( Therefore the current state of the state machine is actually an array of states ). There are many other features. You can find them in our language documentation.

By coding the state machines using a special language we were able to support vastly more complex state machines than would otherwise be possible.

Here is an example of some state machine code as accepted by my compiler. This is the code used to control the state of the camera. The fsm code is highlighted in bold yellow. All the rest is c++ code that will get pasted into the proper place when the fsm is expressed in c++.

#define CAMERA_SWITCH_TIME_PROTECT \ if ( mNextValidSwitchTime > GetAIGlobal( IAIGAME )->CurrentTimeSeconds()) \ $$(REJECT); #define CAMERA_SWITCH_TIME_UPDATE \ mNextValidSwitchTime = GetAIGlobal( IAIGAME )->CurrentTimeSeconds() + 0.5F; #define REMEMBER_FIRST_PERSON \ std::cerr << "remembering 1st person" << std::endl; \ cEnvironment* pEnv = GetAIGlobal( IAIGAME )->GetEnvironment(); \ pEnv->Define( "UseFirstPerson", 1 ); #define REMEMBER_THIRD_PERSON \ cEnvironment* pEnv = GetAIGlobal( IAIGAME )->GetEnvironment(); \ pEnv->Define( "UseFirstPerson", 0 ); chfsm Camera { hfsm external SetTarget hfsm CameraControl { data %{ cPosFollowObjectBehavior mFollow; cZoomBehavior mZoom; cTDVector mFollowOffsetPos; float32 mFollowLagTime; float32 mFollowSwitchTime; cTDVector mFirstPersonOffsetPos; float32 mFirstPersonLagTime; float32 mFirstPersonSwitchTime; cTDVector mLongOffsetPos; float32 mLongLagTime; float32 mLongSwitchTime; float32 mSniperZoomTime; float32 mSniperMagnification; float32 mNextValidSwitchTime; cMailingList mCameraMail; %} entry code %{ IAITweakData* pAITweak = GetAITweak( IAIGAME ); Assert( pAITweak ); pAITweak->GetVectorData( "folOffsetPosition", mFollowOffsetPos ); pAITweak->GetFloatData( "folLagTime", mFollowLagTime ); pAITweak->GetFloatData( "folSwitchTime", mFollowSwitchTime ); pAITweak->GetVectorData( "fpOffsetPosition", mFirstPersonOffsetPos ); pAITweak->GetFloatData( "fpLagTime", mFirstPersonLagTime ); pAITweak->GetFloatData( "fpSwitchTime", mFirstPersonSwitchTime ); pAITweak->GetVectorData( "longOffsetPosition", mLongOffsetPos ); pAITweak->GetFloatData( "longLagTime", mLongLagTime ); pAITweak->GetFloatData( "longSwitchTime", mLongSwitchTime ); pAITweak->GetFloatData( "sniperZoomTime", mSniperZoomTime ); Assert( mSniperZoomTime != 0.0F ); pAITweak->GetFloatData( "sniperFov", mSniperMagnification ); Assert( mSniperMagnification != 0.0F ); // initialize following algorithm mFollow.Init( IAIGAME ); // set initial follow parameters mFollow.SetLagTime( mFollowLagTime ); mFollow.SetOffset( mFollowOffsetPos ); sPMFreeConstraint constraintData; GetPMPhysics( IAIGAME )->SetConstraint( NULL, &constraintData ); // initialize zooming in algorithm mZoom.Init( IAIGAME ); mZoom.SetArrivedMessage( $$(MSG:CameraMsg_ZoomDone) ); // get messages from the camera list. mCameraMail.Init( $$(ID:kMLCamera) ); mCameraMail.Subscribe( this ); IAIGAME->EnableCommandMessages( "UseFirstPerson", $$(MSG:InputMsg_UseFirstPerson) ); mNextValidSwitchTime = 0.0f; %} exit code %{ IAIGAME->DisableCommandMessages( "UseFirstPerson", $$(MSG:InputMsg_UseFirstPerson) ); %} transition { InputMsg_UseFirstPerson } %{ CAMERA_SWITCH_TIME_PROTECT; cCZInputMsg *pMsg = (cCZInputMsg *)( $$(MESSAGE DATA) ); Assert( pMsg ); if ( pMsg->mValue == 1.0F ) receiveMessage( $$(MSG:CameraMsg_FirstPerson) ); else receiveMessage( $$(MSG:CameraMsg_ThirdPerson) ); %} transition { SetTargetMsg_TargetDied } %{ // we really need a better way.... if ( mFollow.GetTarget() == SMGetAIGame( this, $$(MSG:SetTargetMsg_PollTarget) )) { mFollow.SetTarget(); } %} state start CameraAlive { state start NotLocked { state start ThirdPerson { } } } state CameraDead { entry code %{ /* go into a third person mode, looking at the place where the player died */ receiveMessage( $$(MSG:SetTargetMsg_Unacquire) ); IPMPhysics* pPhysics = GetPMPhysics(IAIGAME); Assert( pPhysics ); /* for now we just freeze */ sPMPosStaticData motionData; pPhysics->SetPosMotion( NULL, &motionData ); sPMOrientStaticData orientData; pPhysics->SetOrientMotion( NULL, &orientData ); /* set camera to a long shot */ mFollow.ZoomIn( mLongLagTime, mLongOffsetPos, mLongSwitchTime ); /* all base shake goes away */ mFollow.DisableBaseShake( ); mFollow.AddShake( 1.0F ); %} transition ThirdPerson { SetTargetMsg_NewTargetAcquired } %{ // ok I'm being rejuvenated... this must be a multiplayer game and // the player is being respawned without the level restarting. // so I'll transition back to the world of the living... and then // repost this message! receiveMessage( $$(MESSAGE) ); %} } transition CameraDead { CameraMsg_CameraDead } %{ %} state start CameraAlive { transition { CameraMsg_AddShake } %{ cFloatMsg* pMsg = (cFloatMsg*)( $$(MESSAGE DATA) ); mFollow.AddShake( pMsg->mParam ); %} transition { CameraMsg_SetBaseShake } %{ cFloatMsg* pMsg = (cFloatMsg*)( $$(MESSAGE DATA) ); mFollow.SetBaseShake( pMsg->mParam ); %} state LockedInThird { } state start NotLocked { state start ThirdPerson { } state FollowingClose { entry code %{ /* switch to first person mode */ IAIGame* pTarget = SMGetAIGame( this, $$(MSG:SetTargetMsg_PollTarget) ); if ( pTarget != NULL ) { cCZMsg msg( IAIGAME, $$(MSG:ModelMsg_SwitchToFirstPerson) ); pTarget->ReceiveMessage( &msg, sizeof( msg )); } mFollow.ZoomIn( mFirstPersonLagTime, mFirstPersonOffsetPos, mFirstPersonSwitchTime ); CAMERA_SWITCH_TIME_UPDATE; %} exit code %{ /* switch to back to third person */ IAIGame* pTarget = SMGetAIGame( this, $$(MSG:SetTargetMsg_PollTarget) ); if ( pTarget != NULL ) { cCZMsg msg( IAIGAME, $$(MSG:ModelMsg_SwitchToThirdPerson) ); pTarget->ReceiveMessage( &msg, sizeof( msg )); } mFollow.ZoomIn( mFollowLagTime, mFollowOffsetPos, mFollowSwitchTime ); CAMERA_SWITCH_TIME_UPDATE; %} state history Sniper { } state start history FirstPerson { transition Sniper { InputMsg_CamerasUp } %{ %} transition ThirdPerson { InputMsg_CamerasDown } %{ CAMERA_SWITCH_TIME_PROTECT; REMEMBER_THIRD_PERSON; %} transition { CameraMsg_FirstPerson } %{ REMEMBER_FIRST_PERSON; %} } state history Sniper { entry code %{ /* narrow fov */ mZoom.Stop(); mZoom.ZoomIn( mSniperMagnification, mSniperZoomTime ); mZoom.Start(); %} exit code %{ /* widen fov */ mZoom.Stop(); mZoom.ZoomIn( (1.0F/mSniperMagnification), mSniperZoomTime ); mZoom.Start(); %} transition FirstPerson { InputMsg_CamerasDown } %{ CAMERA_SWITCH_TIME_PROTECT; REMEMBER_FIRST_PERSON; %} transition { CameraMsg_ZoomDone } %{ mZoom.Stop(); %} } transition { CameraMsg_IsFirstPerson } %{ cBoolMsg* msg = (cBoolMsg*)( $$(MESSAGE DATA) ); msg->mBool = true; %} } state start history ThirdPerson { transition FirstPerson { InputMsg_CamerasUp } %{ CAMERA_SWITCH_TIME_PROTECT; REMEMBER_FIRST_PERSON; %} } transition ThirdPerson { CameraMsg_ThirdPerson } %{ CAMERA_SWITCH_TIME_PROTECT; REMEMBER_THIRD_PERSON; %} transition FirstPerson { CameraMsg_FirstPerson } %{ CAMERA_SWITCH_TIME_PROTECT; REMEMBER_FIRST_PERSON; %} transition Sniper { CameraMsg_Sniper } %{ %} transition LockedInThird { CameraMsg_Lock } %{ %} } state LockedInThird { transition NotLocked { CameraMsg_Unlock } %{ %} } transition { CameraMsg_Upgrade } %{ // change what we are following cCZReplaceMsg* pMsg = (cCZReplaceMsg*)( $$(MESSAGE DATA) ); IAIGame* pTarget = SMGetAIGame( this, $$(MSG:SetTargetMsg_PollTarget) ); if ( pMsg->mpTarget1 == pTarget ) { cCZTargetMsg msg( IAIGAME, $$(MSG:SetTargetMsg_Acquire), pMsg->mpTarget2 ); receiveMessage( $$(MSG:SetTargetMsg_Acquire), (tKlownMsgData)(&msg), sizeof( msg )); } %} transition { SetTargetMsg_NewTargetAcquired } %{ IAIGame* pOldTarget = mFollow.GetTarget(); IAIGame* pTarget = SMGetAIGame( this, $$(MSG:SetTargetMsg_PollTarget) ); if ( pTarget != pOldTarget ) { if ( pOldTarget != NULL ) { // old target should go to third person.. cCZMsg msg( IAIGAME, $$(MSG:ModelMsg_SwitchToThirdPerson) ); mFollow.GetTarget()->ReceiveMessage( &msg, sizeof( msg )); } if ( pTarget != NULL ) { mFollow.SetTarget( pTarget ); mFollow.Cut(); mFollow.Start(); // get my target's desired follow position cCZPositionMsg posMsg( IAIGAME, $$(MSG:PCamMsg_PollFirstOffset), mFirstPersonOffsetPos ); pTarget->PollMessage( &posMsg ); mFirstPersonOffsetPos = posMsg.mVector; cCZPositionMsg posMsg2( IAIGAME, $$(MSG:PCamMsg_PollThirdOffset), mFollowOffsetPos ); pTarget->PollMessage( &posMsg2 ); mFollowOffsetPos = posMsg2.mVector; if ( $$(STATEOF:CameraControl) == $$(STATE:FollowingClose) ) { mFollow.ZoomIn( mFirstPersonLagTime, mFirstPersonOffsetPos, mFirstPersonSwitchTime ); } else { mFollow.ZoomIn( mFollowLagTime, mFollowOffsetPos, mFollowSwitchTime ); } // hook myself into the new target's match engines so i can shake // when the thrusters fire cPtrMsg ptrMsg(IAIGAME, $$(MSG:EngineMsg_FollowMotion), &mFollow); pTarget->ReceiveMessage( &ptrMsg, sizeof( ptrMsg )); } else { mFollow.SetTarget(); } } // get user's preferred view cEnvironment* pEnv = GetAIGlobal( IAIGAME )->GetEnvironment(); int preferFirstPerson = pEnv->GetVariable( "UseFirstPerson", 0 ); // switch to first person if that is the preferred. if ( preferFirstPerson ) receiveMessage( $$(MSG:CameraMsg_FirstPerson) ); %} } } }