Finite state machines have been used for a long time to create illusion of intelligence for game agents. A finite state machine (FSM) is a computational model used to simulate sequential logic. It has a finite number of states a game agent can be in at any given time, make transitions from one to another, and can operate. There is one simple rule though, the state machine can only be in one state at any moment.
To apply this idea to game AI development, we decompose our agent’s behaviors into easily manageable states and construct a state machine based on some transition rules.
1 2 3 4 5 6 |
Do not consider FSMs only for AI development. It is a very useful tooll that you can apply to many different scenarios, such as your game menus. So in general, when you face with a structure that you can divide into several states, consider using a state machine or at least applying state design pattern. |
For example the ghosts in Pac-man has two states: Chase and evade. The agents start in their specialized state and if the player (Pac-man or Ms. Pac-man) eats one of the power pills, then they all transit to the evade state. When the power pill timer is done, they transit to the chase state.
For a bit more complex example, you can consider FPS/TPS bots. Usually, these bots have states, such as FindArmor, FindHealth, SeekCover, and so on. They move between one of these states depending on their self-condition.
Advantages and Disadvantages of FSMs
There are many advantages of FSM based game agent implementation. That’s also why FSM-based approach still exists after many years.
First of all, FSMs are quick and simple to code. They are also easy to debug since the behavior of the game agent is broken down into the easily manageable chunks (states). Since FSMs essentially follow hard-coded rules, they have little computational overhead. Easily manageable chunks also make it easy for you to discuss the design of your AI with non-programmers, such as level designers, game producers, and so on. Finally, FSMs let you add new states and rules. So, you can easily adjust, tweak, or expand the scope of the agent’s behavior. Finally, FSMs provide a solid backbone with which you can combine other techniques, such as fuzzy logic or neural networks.
The biggest disadvantage of FSMs comes from its nature. Because, FSMs naturally can only be in one state at any moment and there is no real thinking involved other than if-this-then-that sort of process, developing complex behaviors can be painful and requires you to introduce many states or hierarchical state machines.
FSMs also tend to become unmanageable easily and they cannot capture similar behaviors and benefit from behavior inheritance. Thus, they end up repeating the same behavior in many states.
Implementing a FSM
Even if there are many ways to implement a FSM (including an if-else based approach), it is better to use an approach based on state design pattern. In this way, you embed the rules into the states for the state transitions. So, you monitor the conditions and instruct to switch between the states. You can also easily control enter and exit actions. Thus, each state becomes a self-contained unit aware of the existence of any other states, and does not rely on any external logic for the state transitions. As a consequence, adding new states or replacing a set of states with a new set become straightforward.
So, each of a game agent’s states is implemented as a unique class and each agent holds a pointer (or reference for C#) to an instance of its current state. The logic for determining any state transitions is contained within each state and an agent implements a changeState() method to facilitate the state transition.
If your design and requirements let you, you can consider to make your states singleton for increasing the efficiency. In this way, you can share your state machine among the agents. However, if your states access some external data that may be different for each agent, you would require some extra logic to run your state machine properly. You may even consider to dispose the singleton design. But “friends don’t let friends create singletons“, anyway.
One of the last important points are making the base State class as reusable as possible. Not surprisingly the key thing for this is implementing the base class as template (or generic for C#).
So, here some code:
State.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#ifndef State_hpp #define State_hpp // State of a state machine template <class entity_type> class State { public: virtual ~State() {} // When we enter to the state virtual void onEnter(entity_type*) = 0; // When it is time to run the update virtual void onExecute(entity_type*) = 0; // When we leave the state virtual void onExit(entity_type*) = 0; }; #endif /* State_hpp */ |
StateMachine.hpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 |
#ifndef StateMachine_hpp #define StateMachine_hpp #include "State.hpp" // IGameObject is an interface that declares the necessary abstract methods for a GameObject #include "IGameObject.hpp" // A state machine template <class entity_type> class StateMachine : public IGameObject { public: StateMachine() :mOwner(nullptr), mCurrentState(&mNullState), mPreviousState(&mNullState), mGlobalState(&mNullState) { } StateMachine(entity_type* pOwner) :mOwner(pOwner), mCurrentState(&mNullState), mPreviousState(&mNullState), mGlobalState(&mNullState) { } virtual ~StateMachine() {} State<entity_type>* getGlobalState() const {return mGlobalState;} State<entity_type>* getCurrentState() const {return mCurrentState;} State<entity_type>* getPreviousState() const {return mPreviousState;} void setGlobalState(State<entity_type>* pState) { mGlobalState = pState; } void setCurrentState(State<entity_type>* pState) { mCurrentState = pState; } void setPreviousState(State<entity_type>* pState) { mPreviousState = pState; } void callOnFrame() override { // Global state is a state you need to call every frame -if you need one. mGlobalState->onExecute(mOwner); mCurrentState->onExecute(mOwner); } void changeState(State<entity_type>* pNewState) { // Keep a record of the previous state mPreviousState = mCurrentState; // Call the exit method of the existing state mCurrentState->onExit(mOwner); // Change state to the new state mCurrentState = pNewState; // Call the entry method of the new state mCurrentState->onEnter(mOwner); } // Change state back to the previous state void revertToPreviousState() { ChangeState(mPreviousState); } // Check if the current state and the given state are the same bool isInState(const State<entity_type>& pState) const { return typeid(*mCurrentState) == typeid(pState); } private: // Null object pattern: // // Object of this class will assign to global, current, and previous states by default. // This will save us from constant null checks and one-time null preventing logics, such // as very first call for changeState() in the late-initialization step. class NullState : public State<entity_type> { public: virtual ~NullState() {} void onEnter(entity_type*) {} void onExecute(entity_type*) {} void onExit(entity_type*) {} }; void performInit(const int pArgNumber, va_list args) override { // TODO Solve this mOwner = va_arg(args, entity_type*); } // A pointer to the owner -not ownership entity_type* mOwner; NullState mNullState; State<entity_type>* mGlobalState; // The global state is called every time the FSM is updated State<entity_type>* mCurrentState; State<entity_type>* mPreviousState; }; #endif /* StateMachine_hpp */ |
If you have some questions about null object pattern, please refer to this page for the details.
An improvement on the state class I would make would be applying template method pattern. Template method pattern intents to separate the interface and implementation as much as possible for making future changes and refactors easier. Virtual functions are considered as data members. Protected and private virtuals define the class’ customizable behavior, and there is no reason to make them public. A public virtual method would define both interface and a customization point, a duality that could reflect weak design. The class’ interface must remain consistent in all derived classes.
So, I would suggest to change the State.hpp as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#ifndef State_hpp #define State_hpp // State of a state machine template <class entity_type> class State { public: // Destructor is a different story virtual ~State() {} void onEnter(entity_type* entity) { performOnEnter(entity); } void onExecute(entity_type* entity) { performOnEnter(entity); } void onExit(entity_type* entity) { performOnEnter(entity); } private: // Private virtual, because there is no reason to // call parent's version // // If this was C#, we would be forced to define it // as protected // When we enter to the state virtual void performOnEnter(entity_type*) = 0; // When it is time to run the update virtual void performOnExecute(entity_type*) = 0; // When we leave the state virtual void performOnExit(entity_type*) = 0; }; #endif /* State_hpp */ |
An Example of FSM Based Agent
I will use the enemy state machine I developed for Hands-On Game AI Development video course by Packt as an example.
In this example, our game agents have three states:
- Wander around
- Go to the alarm
- Go to the player
As a design decision, the agents own the states and the state machine.
The default state is the wandering state and if the agent gets close enough to the player, then it transitions to GoToAlarm alarm state. After reaching the alarm or alarmed by the other agents, it transitions to GoToPlayer state. When the player gets caught, the agents go back to the wandering state.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
#ifndef PLG_EnemyAiStates_hpp #define PLG_EnemyAiStates_hpp #include "State.hpp" class PLG_EnemyAi; class PLG_EnemyWanderingState : public State<PLG_EnemyAi> { public: virtual ~PLG_EnemyWanderingState() { } void onEnter(PLG_EnemyAi* pAgent) override { } void onExecute(PLG_EnemyAi* pAgent) override; void onExit(PLG_EnemyAi* pAgent) override { } }; class PLG_EnemyAlarmOthersState : public State<PLG_EnemyAi> { public: virtual ~PLG_EnemyAlarmOthersState() { } void onEnter(PLG_EnemyAi* pAgent) override; void onExecute(PLG_EnemyAi* pAgent) override; void onExit(PLG_EnemyAi* pAgent) override { } }; class PLG_EnemySearchPlayerState : public State<PLG_EnemyAi> { public: virtual ~PLG_EnemySearchPlayerState() { } void onEnter(PLG_EnemyAi* pAgent) override; void onExecute(PLG_EnemyAi* pAgent) override; void onExit(PLG_EnemyAi* pAgent) override { } }; #endif /* PLG_EnemyAiStates_hpp */ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
#include "PLG_EnemyAiStates.hpp" #include "PLG_EnemyAi.hpp" #include "PLG_GameWorldInfo.hpp" void PLG_EnemyWanderingState::onExecute(PLG_EnemyAi *pAgent) { if(pAgent->isAlarmed()) { pAgent->changeState(PLG_EnemyAi::eENEMY_STATES::eES_SearchPlayer); } if( pAgent->isPlayerSeen() ) { pAgent->changeState(PLG_EnemyAi::eENEMY_STATES::eES_AlarmOthers); } if( !pAgent->hasPositionTarget() ) { pAgent->chooseNewRandomPath(); } } void PLG_EnemyAlarmOthersState::onEnter(PLG_EnemyAi *pAgent) { pAgent->setPathToAlarm(); } void PLG_EnemyAlarmOthersState::onExecute(PLG_EnemyAi *pAgent) { if( PLG_GameInfo.isPlayerDown ) { pAgent->changeState(PLG_EnemyAi::eENEMY_STATES::eES_WanderAround); } if( !pAgent->hasPositionTarget() ) { pAgent->changeState(PLG_EnemyAi::eENEMY_STATES::eES_SearchPlayer); } } void PLG_EnemySearchPlayerState::onEnter(PLG_EnemyAi* pAgent) { pAgent->setPathToPlayer(); } void PLG_EnemySearchPlayerState::onExecute(PLG_EnemyAi* pAgent) { if( PLG_GameInfo.isPlayerDown ) { pAgent->changeState(PLG_EnemyAi::eENEMY_STATES::eES_WanderAround); } if( pAgent->isPlayerClose() ) { pAgent->attack(); } if( !pAgent->hasPositionTarget() ) { pAgent->setPathToPlayer(); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
#ifndef PLG_EnemyAi_hpp #define PLG_EnemyAi_hpp #include "StateMachine.hpp" #include "PLG_EnemyAiStates.hpp" // OMITTED CODE class PLG_EnemyAi : public GameCharacterController { public: enum eENEMY_STATES{ eES_SearchPlayer, eES_AlarmOthers, eES_WanderAround }; // OMITTED CODE void changeState(const eENEMY_STATES pNewStateRequest); // OMITTED CODE private: // OMITTED CODE PLG_EnemyWanderingState mWanderingState; PLG_EnemyAlarmOthersState mAlarmOthersState; PLG_EnemySearchPlayerState mSearchPlayerState; // OMITTED CODE StateMachine<PLG_EnemyAi> mStateMachine; }; #endif /* PLG_EnemyAi_hpp */ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void PLG_EnemyAi::changeState(const eENEMY_STATES pNewStateRequest) { switch (pNewStateRequest) { case eENEMY_STATES::eES_WanderAround: mStateMachine.changeState(&mWanderingState); break; case eENEMY_STATES::eES_SearchPlayer: mStateMachine.changeState(&mSearchPlayerState); break; case eENEMY_STATES::eES_AlarmOthers: mStateMachine.changeState(&mAlarmOthersState); break; } } |