I was working on a state machine implementation today and faced with a scenario that I need to introduce lots of code for a one-time-check-only situation. It was
- Initialize the state machine object via init() method
- Set the new state via changeState() method.
The code was like this:
1 2 |
mStateMachine.initObject(1, this); mStateMachine.changeState(&mWanderingState); |
And I faced with the very famous null pointer exception in C++. Then, I checked the state machine code I wrote many months ago. The changeState() method was like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
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); } |
Very reasonable code for a method that I will call frequently. While checking changeState() method I remembered the constructors of the state machine:
1 2 3 4 5 6 7 |
StateMachine() :mOwner(nullptr), mCurrentState(nullptr), mPreviousState(nullptr), mGlobalState(nullptr) { } StateMachine(entity_type* pOwner) :mOwner(pOwner), mCurrentState(nullptr), mPreviousState(nullptr), mGlobalState(nullptr) { } |
It was obvious that mCurrentState would be NULL for the call I made right after initializing the state machine and I needed to write some if-then code for preventing this. The only problem, I knew that I would need this if-then code only once at the beginning of the execution, but it would remain with the code and perform this useless if-then check whenever I want to change the state. So, I was not very keen to go for if-then implementation.
Then, I remembered null object, an object that does not do anything except saving you handling NULL with if…else statement. So, I added an extra class to my project:
1 2 3 4 5 6 7 8 9 |
class NullState : public State<entity_type> { public: virtual ~NullState() {} void onEnter(entity_type*) {} void onExecute(entity_type*) {} void onExit(entity_type*) {} }; |
and replaced my constructors as:
1 2 3 4 5 6 7 |
StateMachine() :mOwner(nullptr), mCurrentState(&mNullState), mPreviousState(&mNullState), mGlobalState(&mNullState) { } StateMachine(entity_type* pOwner) :mOwner(pOwner), mCurrentState(&mNullState), mPreviousState(&mNullState), mGlobalState(&mNullState) { } |
Thanks to null object pattern I was able to save myself from null pointer exception without changing changeState() method. It also helped me to turn these lines below
1 2 3 4 5 6 7 8 9 10 11 12 |
void callOnFrame() override { if(mGlobalState) { mGlobalState->onExecute(mOwner); } if(mCurrentState) { mCurrentState->onExecute(mOwner); } } |
to these ones:
1 2 3 4 5 6 |
void callOnFrame() override { // Global state is a state you need to call every frame -if you need one. mGlobalState->onExecute(mOwner); mCurrentState->onExecute(mOwner); } |
I believe this is cleaner, less error prone, and easier to read.
Here is the whole code of State and StateMachine classes in case you would like to see:
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 */ |
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 */ |