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) );
%}
}
}
}