Skip to content

Chapter 2. The Basics

Antonio Maiorano edited this page Jul 9, 2018 · 4 revisions

Introduction

This chapter will walk you through the basics of using HSM. To keep things simple, we will learn how to author single-hierarchy (aka flat) state machines here, and will cover hierarchical state machines in the next chapter.

The Simplest State Machine

Let's start with the simplest state machine we can write.

// simplest_state_machine.cpp

#include "hsm/statemachine.h"

struct First : hsm::State
{
};

int main()
{
	hsm::StateMachine stateMachine;
	stateMachine.Initialize<First>();
	stateMachine.ProcessStateTransitions();
}

First we include hsm/statemachine.h, which brings in the entire HSM library.

We declare a state named First. States are structs or classes that inherit from hsm::State.

NOTE: We prefer to use structs over classes as they derive publicly by default, so there is no need to specify the 'public' keyword.

In main, we initialize a StateMachine object, telling it that First is its initial state. All StateMachine's must have an initial state to start with.

We then call stateMachine.ProcessStateTransitions, which will evaluate any transitions that must be made, and perform them. In this case, as we have only one state, and it doesn't do anything, this call does nothing.

This is about as simple as it gets. Now let's make this state machine actually do something.

States and Transitions

Let's add some states and transitions.

// states_and_transitions.cpp

#include "hsm/statemachine.h"

using namespace hsm;

struct Third : State
{
	virtual Transition GetTransition()
	{
		return NoTransition();
	}
};

struct Second : State
{
	virtual Transition GetTransition()
	{
		return SiblingTransition<Third>();
	}
};

struct First : State
{
	virtual Transition GetTransition()
	{
		return SiblingTransition<Second>();
	}
};

int main()
{
	StateMachine stateMachine;
	stateMachine.Initialize<First>();
	stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
	stateMachine.ProcessStateTransitions();
}

Let's look at what's new in this code:

We've brought in the hsm namespace. In general, we recommend doing this in cpp files where state machines are implemented as it drastically reduces the 'hsm::' prefix noise.

We've added 2 more states: Third, and Second. We've also implemented the virtual GetTransition function in all 3 states. This function is where a state returns what transition it wishes to make when StateMachine::ProcessStateTransition gets called. In this case, all 3 states are siblings, meaning they are all at the same hierarchical level (we'll get into the hierarchical part later), and First transitions to Second, which in turn transitions to Third.

In main, we've added a call to stateMachine.SetDebugInfo to give our state machine a name and verbosity level for debugging purposes.

NOTE: The TraceLevel enum supports three values: None, Basic, Diagnostic. We recommend using Basic while authoring your state machines, and Diagnostic when debugging the internals of the library.

Finally, we call stateMachine.ProcessStateTransitions as before. Since we set the debug level to 1, we get the following output:

HSM_1_TestHsm: Init    : struct First
HSM_1_TestHsm: Sibling : struct Second
HSM_1_TestHsm: Sibling : struct Third

The debug output displays the transitions being made. The initial transition to First is followed by two sibling transitions, First to Second, and Second to Third.

Let's also take a look at the plotHsm output for this state machine:

states_and_transitions.cpp.dot.png

The plot of this state machines shows our three states, with dotted arrows signifying sibling transitions that can be made: First can transition to Second, and Second to Third.

NOTE: The plots for the examples in this chapter are too simple to be useful; however, in the next chapter, we make extensive use of plotHsm to better understand the hierarhical state machines presented.

Pretty simple so far, right? Obviously there are many details missing, but we'll get to that soon enough!

Improving Readability

You may have noticed in the previous example that states First, Second, and Third were defined in reverse order; that is: Third, then Second, and finally First. This is typical of C/C++ code as you must always define, or at least declare, a type before it is used; in our case, Second references Third in its GetTransition implementation, and similarly First references Second:

struct Third : State
{
	virtual Transition GetTransition()
	{
		return NoTransition();
	}
};

struct Second : State
{
	virtual Transition GetTransition()
	{
		return SiblingTransition<Third>(); //*** Here
	}
};

struct First : State
{
	virtual Transition GetTransition()
	{
		return SiblingTransition<Second>(); //*** And here
	}
};

It would be nice to order the states any way we'd like; in this case, it would be easier to understand the state machine if First came before Second, and Second before Third. We might be able to do it with some forward declarations, but it's also nice having to declare our states only once. Well, as it turns out, we can have our cake and eat it too, by nesting our states within a struct:

// improving_readability.cpp

#include "hsm/statemachine.h"

using namespace hsm;

struct MyStates
{
	struct First : State
	{
		virtual Transition GetTransition()
		{
			return SiblingTransition<Second>();
		}
	};

	struct Second : State
	{
		virtual Transition GetTransition()
		{
			return SiblingTransition<Third>();
		}
	};

	struct Third : State
	{
		virtual Transition GetTransition()
		{
			return NoTransition();
		}
	};
};

int main()
{
	StateMachine stateMachine;
	stateMachine.Initialize<MyStates::First>();
	stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
	stateMachine.ProcessStateTransitions();
}

Notice how we've added a struct named MyStates and have nested our three states in a different order within it. We also modified the stateMachine.Initialize call to fully qualify the initial state name (MyStates::First).

The reason this works is because of how template argument dependent name lookup (ADL) works when those names are nested in C++. Without getting into too much detail, when a template function parameter is a nested type, even if it's defined after the template function call, it will be resolved correctly. In our case, SiblingTransition is a template function, and we can pass it the name of a state even though it is defined later because it is nested in the MyStates struct.

NOTE: Later on, we'll show yet another advantage to nesting states in a struct: granting access to a state machine's owner's private members.

State OnEnter and OnExit

The base hsm::State provides two virtual hooks for when a state is entered and exited: OnEnter and OnExit respectively. These can be used to initialize or deinitalize data, systems, etc.

Here's our previous example code with OnEnter/OnExit pairs added to the three states:

// state_onenter_onexit.cpp

#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;

struct MyStates
{
	struct First : State
	{
		virtual void OnEnter()
		{
			printf("First::OnEnter\n");
		}

		virtual void OnExit()
		{
			printf("First::OnExit\n");
		}

		virtual Transition GetTransition()
		{
			return SiblingTransition<Second>();
		}
	};

	struct Second : State
	{
		virtual void OnEnter()
		{
			printf("Second::OnEnter\n");
		}

		virtual void OnExit()
		{
			printf("Second::OnExit\n");
		}

		virtual Transition GetTransition()
		{
			return SiblingTransition<Third>();
		}
	};

	struct Third : State
	{
		virtual void OnEnter()
		{
			printf("Third::OnEnter\n");
		}

		virtual void OnExit()
		{
			printf("Third::OnExit\n");
		}

		virtual Transition GetTransition()
		{
			return NoTransition();
		}
	};
};

int main()
{
	StateMachine stateMachine;
	stateMachine.Initialize<MyStates::First>();
	stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
	stateMachine.ProcessStateTransitions();
}

The output from running the program:

HSM_1_TestHsm: Init    : struct MyStates::First
First::OnEnter
First::OnExit
HSM_1_TestHsm: Sibling : struct MyStates::Second
Second::OnEnter
Second::OnExit
HSM_1_TestHsm: Sibling : struct MyStates::Third
Third::OnEnter

We can see that when a source state makes a sibling transition - or siblings - to a target state, the source state is first exited before the target state is entered.

OnEnter/OnExit vs Constructor/Destructor

Since states are just classes, why not use the constructor/destructor instead of the OnEnter/OnExit functions?

The main reason is that by the time OnEnter is called on a state, all its data has been initialized, including - and most importantly - the owning state machine instance. When using the default constructor, this data is not yet set, and cannot be used. Most functions available to a state depend on the state machine pointer being valid, so these can only be called within OnEnter, not in the constructor.

As for OnExit, there isn't much difference between using it and the destructor; however, we recommend using it for consistency.

NOTE: Another reason for using OnEnter is that it allows for the optional use of StateArgs, a feature we will cover later on.

ProcessStateTransitions

In the examples so far, we've glossed over the details of the stateMachine.ProcessStateTransitions call. In this section, we'll take a look a closer look at this function, starting with some pseudo-code for how it works:

done = false
while (!done)
	transition = currState.GetTransition()
	if (transition != NoTransition)
		currState.OnExit()
		currState = transition.GetTargetState()
		currState.OnEnter()
	else
		done = true

NOTE: This pseudo-code will be expanded in the next chapter to deal with hierarchical state transitions. For now, what we present here is accurate for flat state machines (i.e. that only perform sibling transitions between states).

The important part is to note that the function will keep transitioning between states until there are no more transitions to be made. The following example shows how this works:

// process_state_transitions.cpp

#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;

bool gStartOver = false;

struct MyStates
{
	struct First : State
	{
		virtual void OnEnter()
		{
			gStartOver = false;
		}

		virtual Transition GetTransition()
		{
			return SiblingTransition<Second>();
		}
	};

	struct Second : State
	{
		virtual Transition GetTransition()
		{
			return SiblingTransition<Third>();
		}
	};

	struct Third : State
	{
		virtual Transition GetTransition()
		{
			if (gStartOver)
				return SiblingTransition<First>();

			return NoTransition();
		}
	};
};

int main()
{
	StateMachine stateMachine;
	stateMachine.Initialize<MyStates::First>();
	stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
	
	printf(">>> First ProcessStateTransitions\n");
	stateMachine.ProcessStateTransitions();

	printf(">>> Second ProcessStateTransitions\n");
	stateMachine.ProcessStateTransitions();

	gStartOver = true;
	
	printf(">>> Third ProcessStateTransitions\n");
	stateMachine.ProcessStateTransitions();

	printf(">>> Fourth ProcessStateTransitions\n");
	stateMachine.ProcessStateTransitions();
}

As before, First siblings to Second, and Second siblings to Third; but state Third will only transition back to First if global variable gStartOver is true; otherwise it remains in its state. Here's the output from this program:

>>> First ProcessStateTransitions
HSM_1_TestHsm: Init    : struct MyStates::First
HSM_1_TestHsm: Sibling : struct MyStates::Second
HSM_1_TestHsm: Sibling : struct MyStates::Third
>>> Second ProcessStateTransitions
>>> Third ProcessStateTransitions
HSM_1_TestHsm: Sibling : struct MyStates::First
HSM_1_TestHsm: Sibling : struct MyStates::Second
HSM_1_TestHsm: Sibling : struct MyStates::Third
>>> Fourth ProcessStateTransitions

We can see that the second call to ProcessStateTransitions does nothing. This is because we are in state Third and gStartOver is false, so it returns NoTransition. After that, we set gStartOver = true, and the third call to ProcessStateTransitions has Third sibling to First, First to Second, and Second back to Third where it stops again. Why does it stop in Third again? The reason is that First::OnEnter always resets gStartOver to false, so by the time it reaches Third again, it will not transition back to First. Indeed, if we remove First::OnEnter, ProcessStateTransitions would end up in an infinite loop of sibling transitions: Third -> First -> Second -> Third -> First -> etc.

NOTE: HSM triggers an assertion when infinite transitions are detected.

So now we see how changing some data in between calls to ProcessStateTransitions can result in different transitions being made. In this example, the data is a global variable that is modified outside the state machine; however, often the data changes are made from the states themselves.

At what frequency should ProcessStateTransitions be called? That depends on your application, but here are couple of examples:

  • In games or real time simulations, you would likely call ProcessStateTransitions every frame, knowing that the state of the world, inputs from the player, or other data may have changed since the last frame.

  • In event-based systems, such as UI, you'd want to call ProcessStateTransition after an event modifies some data.

A final note on State::GetTransition: this function's role is simply to return what transition to make, not to perform any state-specific logic. Instead, you can use State::Update for this purpose, which is covered in the next section.

UpdateStates

When you need a state to perform some actions while in that state, you can implement the virtual Update function. This function will be called on the current state when StateMachine::UpdateStates is called:

// update_states.cpp

#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;

bool gPlaySequence = false;

struct MyStates
{
	struct First : State
	{
		virtual Transition GetTransition()
		{
			if (gPlaySequence)
				return SiblingTransition<Second>();
			
			return NoTransition();
		}

		virtual void Update()
		{
			printf("First::Update\n");
		}
	};

	struct Second : State
	{
		virtual Transition GetTransition()
		{
			if (gPlaySequence)
				return SiblingTransition<Third>();
			
			return NoTransition();
		}

		virtual void Update()
		{
			printf("Second::Update\n");
		}
	};

	struct Third : State
	{
		virtual Transition GetTransition()
		{
			return NoTransition();
		}

		virtual void Update()
		{
			printf("Third::Update\n");
		}
	};
};

int main()
{
	StateMachine stateMachine;
	stateMachine.Initialize<MyStates::First>();
	stateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);

	stateMachine.ProcessStateTransitions();
	stateMachine.UpdateStates();

	stateMachine.ProcessStateTransitions();
	stateMachine.UpdateStates();

	gPlaySequence = true;

	stateMachine.ProcessStateTransitions();
	stateMachine.UpdateStates();

	stateMachine.ProcessStateTransitions();
	stateMachine.UpdateStates();
}

We've added the Update function to states First, Second, and Third. We use global variable gPlaySequence to make First sibling to Second then to Third. In the main function, we now pair up the call to ProcessStateTransition with UpdateStates. Typically, we'd want to call these two functions in succession once per frame (or whenever the state machine needs to be updated). In this contrived example, we call this pair twice before modifying the global variable to show what happens when you remain in a state for multiple frames.

Here's the output from running the program:

HSM_1_TestHsm: Init    : struct MyStates::First
First::Update
First::Update
HSM_1_TestHsm: Sibling : struct MyStates::Second
HSM_1_TestHsm: Sibling : struct MyStates::Third
Third::Update
Third::Update

While we're in state First, First::Update gets called each time we invoke stateMachine.UpdateStates. After modifying our global variable, the next call to stateMachine.ProcessStateTransitions causes First to sibling to Second, and Second to Third. As we're in state Third, Third::Update gets called twice for each call to stateMachine.UpdateStates. The important thing to note here is that Second::Update is never called because we never ended up in that state at the end of ProcessStateTransitions. If we really wanted Second to do something when we pass through it, we could use OnEnter.

Some more things to note about UpdateStates:

  • This feature is, in fact, not actually necessary. However, in games and real-time simulations, it turns out we often want some type of Update function on our current state, so it was added to HSM as a convenience.

  • It is often useful to pass certain arguments to Update functions, such as the frame delta time. The HSM provides macros that can be modified to define the parameters to StateMachine::UpdateStates and State::Update:

    #define HSM_STATE_UPDATE_ARGS void
    #define HSM_STATE_UPDATE_ARGS_FORWARD
    

    By default, these functions take no arguments (void), but say you wanted to pass a float deltaTime parameter, you could modify the macros as follows:

    #define HSM_STATE_UPDATE_ARGS float deltaTime
    #define HSM_STATE_UPDATE_ARGS_FORWARD deltaTime
    

    Now when invoking stateMachine.UpdateStates you would pass it a float parameter, and make sure to add 'float deltaTime' to the overrides of State::Update in your own states.

Ownership

Basic Usage

So far in our examples, we've created a single StateMachine instance directly in main, and have communicated with the states using global variables. In practice, a StateMachine would be a data member of a class - it's owner - and we'd want the states of that StateMachine to access members on this owner (its functions and data members).

Let's take a look at an example that is functionally equivalent to the one in the last section, except this time we've added an owner:

// ownership_basic_usage.cpp

#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;

class MyOwner
{
public:
	MyOwner();
	void UpdateStateMachine();
	void PlaySequence();
	bool GetPlaySequence() const;

private:
	StateMachine mStateMachine;
	bool mPlaySequence;
};

struct MyStates
{
	struct First : State
	{
		virtual Transition GetTransition()
		{
			MyOwner* owner = reinterpret_cast<MyOwner*>(GetStateMachine().GetOwner());

			if (owner->GetPlaySequence())
				return SiblingTransition<Second>();

			return NoTransition();
		}

		virtual void Update()
		{
			printf("First::Update\n");
		}
	};

	struct Second : State
	{
		virtual Transition GetTransition()
		{
			MyOwner* owner = reinterpret_cast<MyOwner*>(GetStateMachine().GetOwner());

			if (owner->GetPlaySequence())
				return SiblingTransition<Third>();

			return NoTransition();
		}

		virtual void Update()
		{
			printf("Second::Update\n");
		}
	};

	struct Third : State
	{
		virtual Transition GetTransition()
		{
			return NoTransition();
		}

		virtual void Update()
		{
			printf("Third::Update\n");
		}
	};
};

MyOwner::MyOwner()
{
	mPlaySequence = false;
	mStateMachine.Initialize<MyStates::First>(this);
	mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}

void MyOwner::UpdateStateMachine()
{
	mStateMachine.ProcessStateTransitions();
	mStateMachine.UpdateStates();
}

void MyOwner::PlaySequence()
{
	mPlaySequence = true;
}

bool MyOwner::GetPlaySequence() const
{
	return mPlaySequence;
}

int main()
{
	MyOwner myOwner;

	myOwner.UpdateStateMachine();
	myOwner.UpdateStateMachine();

	myOwner.PlaySequence();

	myOwner.UpdateStateMachine();
	myOwner.UpdateStateMachine();
}

The output is exactly the same as before:

HSM_1_TestHsm: Init    : struct MyStates::First
First::Update
First::Update
HSM_1_TestHsm: Sibling : struct MyStates::Second
HSM_1_TestHsm: Sibling : struct MyStates::Third
Third::Update
Third::Update

Alright, let's break down this example to better understand the changes. First, we've introduced a new class MyOwner:

class MyOwner
{
public:
	MyOwner();
	void UpdateStateMachine();
	void PlaySequence();
	bool GetPlaySequence() const;

private:
	StateMachine mStateMachine;
	bool mPlaySequence;
};

This class contains the StateMachine instance as a member named mStateMachine. We've also moved the gPlaySequence global to this class as data member mPlaySequence, which is set and read by member functions PlaySequence and GetPlaySequence:

void MyOwner::PlaySequence()
{
	mPlaySequence = true;
}

bool MyOwner::GetPlaySequence() const
{
	return mPlaySequence;
}

The constructor is where we initialize mPlaySequence as well as mStateMachine. The important difference here is that we now pass an argument to mStateMachine.Initialize: 'this':

MyOwner::MyOwner()
{
	mPlaySequence = false;
	mStateMachine.Initialize<MyStates::First>(this); //*** Note that we pass 'this' as our owner
	mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}

The StateMachine::Initialize function accepts an optional pointer to the owner instance as it's first parameter. The pointer type is void*, so any type can be passed in here. Before we see how this owner pointer is used, let's take a look at UpdateStateMachine, which we'd call whenever the state machine needs to be updated (e.g. once per frame in a game):

void MyOwner::UpdateStateMachine()
{
	mStateMachine.ProcessStateTransitions();
	mStateMachine.UpdateStates();
}

In main, we create the MyOwner instance, and simulate four frame updates, making sure to set PlaySequence after two of them:

int main()
{
	MyOwner myOwner;

	myOwner.UpdateStateMachine();
	myOwner.UpdateStateMachine();

	myOwner.PlaySequence();

	myOwner.UpdateStateMachine();
	myOwner.UpdateStateMachine();
}

Now let's take a look at our states. Previously, states First and Second would read the value of global variable gPlaySequence in their GetTransition functions to determine whether to sibling to the next state. Now, these states access their owner via GetStateMachine().GetOwner():

	struct First : State
	{
		virtual Transition GetTransition()
		{
			MyOwner* owner = reinterpret_cast<MyOwner*>(GetStateMachine().GetOwner());

			if (owner->GetPlaySequence())
				return SiblingTransition<Second>();

			return NoTransition();
		}		
		<snip>
	}

Since GetStateMachine().GetOwner() returns the void* pointer we set earlier via StateMachine::Initialize, we need to cast it to MyOwner* so that we can call owner->GetPlaySequence(). In the next section, we'll see how to get rid of this casting.

Easier Owner Access

As we saw in the last example, we need to reinterpret_cast the void* owner pointer in state code like so:

	struct First : State
	{
		virtual Transition GetTransition()
		{
			MyOwner* owner = reinterpret_cast<MyOwner*>(GetStateMachine().GetOwner());

			if (owner->GetPlaySequence())
				return SiblingTransition<Second>();

			return NoTransition();
		}
	};

Accessing owners happens a lot in state code, so having to do this gets tedious fast. Thankfully, HSM offers a nice solution: derive your states from hsm::StateWithOwner<OwnerType> instead of from hsm::State. For example, state First would become:

	struct First : StateWithOwner<MyOwner>
	{
		virtual Transition GetTransition()
		{
			if (Owner().GetPlaySequence())
				return SiblingTransition<Second>();

			return NoTransition();
		}

		virtual void Update()
		{
			printf("First::Update\n");
		}
	};

Notice that First now derives from StateWithOwner<MyOwner>. This template class simply introduces a new function, Owner, that returns the specified owner type by reference: MyOwner& in this case. This simplifies the GetTransition code as it can now just call Owner() and access it's public members directly. We'll see in the next section how to access the Owner's private members as well.

Although deriving each state class from StateWithOwner<> works, we recommend creating a new base class that derives from StateWithOwner<>, and having each state derive from that class. This not only makes the code a little less verbose, but having a base class for your states is useful for adding shared functionality. Here's the full example using a base state class:

// ownership_easier_owner_access.cpp

#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;

class MyOwner
{
public:
	MyOwner();
	void UpdateStateMachine();
	void PlaySequence();
	bool GetPlaySequence() const;

private:
	StateMachine mStateMachine;
	bool mPlaySequence;
};

struct MyStates
{
	struct BaseState : StateWithOwner<MyOwner>
	{
	};

	struct First : BaseState
	{
		virtual Transition GetTransition()
		{
			if (Owner().GetPlaySequence())
				return SiblingTransition<Second>();

			return NoTransition();
		}

		virtual void Update()
		{
			printf("First::Update\n");
		}
	};

	struct Second : BaseState
	{
		virtual Transition GetTransition()
		{
			if (Owner().GetPlaySequence())
				return SiblingTransition<Third>();

			return NoTransition();
		}

		virtual void Update()
		{
			printf("Second::Update\n");
		}
	};

	struct Third : BaseState
	{
		virtual Transition GetTransition()
		{
			return NoTransition();
		}

		virtual void Update()
		{
			printf("Third::Update\n");
		}
	};
};

MyOwner::MyOwner()
{
	mPlaySequence = false;
	mStateMachine.Initialize<MyStates::First>(this);
	mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}

void MyOwner::UpdateStateMachine()
{
	mStateMachine.ProcessStateTransitions();
	mStateMachine.UpdateStates();
}

void MyOwner::PlaySequence()
{
	mPlaySequence = true;
}

bool MyOwner::GetPlaySequence() const
{
	return mPlaySequence;
}

int main()
{
	MyOwner myOwner;

	myOwner.UpdateStateMachine();
	myOwner.UpdateStateMachine();

	myOwner.PlaySequence();

	myOwner.UpdateStateMachine();
	myOwner.UpdateStateMachine();
}

Accessing Owner's Private Members

So far, we've seen how states can access their owner quite easily; however, because the owner is an unrelated class, states can only access its public members. This type of encapsulation is usually a good thing; however, in the case of state machines, the logic in states are often highly coupled with the data and functions in the owner class. Indeed, without using HSM, the state machine logic would usually be written directly in the owner class's member functions, thus having full access to its private members.

How can we make our states gain private access to the owner's private members? There are a few different ways:

  • Make each state a friend of the owner class. The problem with this approach is that every time a state is added/renamed/deleted, you need to modify the owner's class declaration.

  • Make the base state class - StateBase in the previous example - a friend of the owner class, and write forwarding functions in the base state class that accesses the private members of the owner. The derived states then use these forwarding functions. The advantage of this approach is that you don't need to modify the owner's class declaration when states are added/renamed/deleted. On the other hand, you may often need to add new forwarding functions to the base state class, introducing a lot of redundancy.

It would be great if states could just access their owner's privates directly without any fuss. Well, it turns out we can do this. As you may recall, we decided to nest all our states within a struct - MyStates in our examples so far - to avoid worrying about the order in which states are declared. Well, if we declare this struct a friend of the owner, then all states within the struct will be able to access the private members of the owner! This works because in C++, member access is inherited by nested types.

Here's our previous example modified to use this technique:

// ownership_access_owner_privates.cpp

#include <cstdio>
#include "hsm/statemachine.h"
using namespace hsm;

class MyOwner
{
public:
	MyOwner();
	void UpdateStateMachine();

	void PlaySequence();

private:
	friend struct MyStates; //*** All states can access MyOwner's private members

	StateMachine mStateMachine;
	bool mPlaySequence;
};

struct MyStates
{
	struct BaseState : StateWithOwner<MyOwner>
	{
	};

	struct First : BaseState
	{
		virtual Transition GetTransition()
		{
			if (Owner().mPlaySequence) //*** Access one of owner's private members
				return SiblingTransition<Second>();

			return NoTransition();
		}

		virtual void Update()
		{
			printf("First::Update\n");
		}
	};

	struct Second : BaseState
	{
		virtual Transition GetTransition()
		{
			if (Owner().mPlaySequence) //*** Access one of owner's private members
				return SiblingTransition<Third>();

			return NoTransition();
		}

		virtual void Update()
		{
			printf("Second::Update\n");
		}
	};

	struct Third : BaseState
	{
		virtual Transition GetTransition()
		{
			return NoTransition();
		}

		virtual void Update()
		{
			printf("Third::Update\n");
		}
	};
};

MyOwner::MyOwner()
{
	mPlaySequence = false;
	mStateMachine.Initialize<MyStates::First>(this);
	mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}

void MyOwner::UpdateStateMachine()
{
	mStateMachine.ProcessStateTransitions();
	mStateMachine.UpdateStates();
}

void MyOwner::PlaySequence()
{
	mPlaySequence = true;
}

int main()
{
	MyOwner myOwner;

	myOwner.UpdateStateMachine();
	myOwner.UpdateStateMachine();

	myOwner.PlaySequence();

	myOwner.UpdateStateMachine();
	myOwner.UpdateStateMachine();
}

The main differences are:

  • We've added the friend declaration to MyOwner:
  friend struct MyStates; //*** All states can access MyOwner's private members
  • The public Owner::GetPlaySequence function has been removed since our states can now read the private mPlaySequence member directly, so the code in states First and Second looks like:
			if (Owner().mPlaySequence) //*** Access one of owner's private members
				return SiblingTransition<Third>();

Storing Data

So far, we've seen how states can access data stored on their owner instance. This is perfect for any data that must persist across state changes. You can also store data on a state directly - after all, it's just a class. Naturally, data members of a state will be created when the state is entered, and destroyed when the state is exited.

This example shows how we can store data on a state, and how its lifetime is managed:

// storing_data.cpp

#include <cstdio>
#include <string>
#include "hsm/statemachine.h"
using namespace hsm;

class MyOwner
{
public:
	MyOwner();
	void UpdateStateMachine();

private:
	friend struct MyStates; //*** All states can access MyOwner's private members
	StateMachine mStateMachine;
};

struct Foo
{
	Foo() { printf(">>> Foo created\n"); }
	~Foo() { printf(">>>Foo destroyed\n"); }
};

struct MyStates
{
	struct BaseState : StateWithOwner<MyOwner>
	{
	};

	struct First : BaseState
	{
		virtual Transition GetTransition()
		{
			return SiblingTransition<Second>();
		}

		Foo mFoo; //*** State data member
	};

	struct Second : BaseState
	{
		virtual Transition GetTransition()
		{
			return NoTransition();
		}
	};
};

MyOwner::MyOwner()
{
	mStateMachine.Initialize<MyStates::First>(this);
	mStateMachine.SetDebugInfo("TestHsm", TraceLevel::Basic);
}

void MyOwner::UpdateStateMachine()
{
	mStateMachine.ProcessStateTransitions();
	mStateMachine.UpdateStates();
}

int main()
{
	MyOwner myOwner;
	myOwner.UpdateStateMachine();
}

We've added a simple class Foo that prints when it's created and destroyed, and have added an instance of Foo to state First. Here's the output from the program:

>>> Foo created
HSM_1_TestHsm: Init    : struct MyStates::First
>>>Foo destroyed
HSM_1_TestHsm: Sibling : struct MyStates::Second

We can see that the Foo instance in First gets created when entering First, and destroyed when sibling from First to Second.

When it comes to storing data for your state machine, you should try to reduce its scope to where its needed. If you only need data on one state, make it data members of that state. If it needs to be accessed by all (or most) states, it makes sense to store it on the owner. What about scoping data to a subset of states? This will be covered in the next chapter when we introduce the concept of hierarchical state machines.

Prev (Chapter 1) | Next (Chapter 3)