I've got a state machine implementation in Unity that I'm working on (C#), the plan being that it will be mostly used for AI related things.
I'm not sure how I should deal with various "inputs" / how it should interact with knowledge from the outside environment. Two approaches I've considered and tried so far:
1, I have a dedicated "Query" class that holds various bools. At the end of the Tick() method in each state I make some checks like
if (queries.JumpUp) { SetState(JumpState); }
that take care of switching states. To change states, I simply set the bool to true. This seems to work fine, creates a very loose relationship between the "query" and the resulting behaviour, and lets me place pretty much all the transition logic in a dedicated method (my base Tick() method calls an CheckForTransitions() method at its end, and it's this method that I override and put all the transition logic in). It works fine so far, but I'm a bit worried that this type of logic might be almost a bit too loose. I think it might be somewhat similar to the blackboard design pattern. It also feels a bit like an observer pattern, which could be useful - I might have multiple different types of state machines active at the same time. A "Query" class that has booleans like I mentioned seems like a very natural way of implementing a general interface layer that will allow for loose "communication" between the layers etc.
2, Create virtual methods for all possible "events" I would like to be handled.
public override void TryJump() { SetState(JumpState); }
To change states, I explicitly call the method above (or some wrapper around it, implemented inside the StateMachine).
This also seems to work fine. Some slight negatives I can see compared to 1: With approach 1, there's no issues if I choose to update my state machine independently of the game loop. With approach 2, there could be multiple calls that result in a transition between two state machine ticks/updates. This could be fixed by the making calls like "TryJump()" change some buffer variable that would just hold the desired next state, and the actual transition would perhaps happen at the end/start of the state machine update (rather than "TryJump()" causing an immediate change in states). But at that point I'm getting very close to the approach used in 1, so I'd think I might as well just use that.
I don't have a single nice place in the code where I can check exactly what sort transitions can happen and what their conditions are. I will have to have tons of virtual methods for each state, for each "TryJump" type event. If I don't want to work with a direct reference to the state machine, I will have to also write wrappers, that will call those TryJump events the currently active StateMachines - so something like say:
public void TryJump()
{
foreach (StateMachine stateMachine in ActiveStateMachines)
stateMachine.currentState.TryJump();
}
Despite that, something just feels a bit off about the method in 1, - using booleans like that just seems a bit weird. I'm also not terribly comfortable with event based approaches, and it feels a bit "wrong" to use a special layer with booleans etc., when I could just make a direct call that does what I want using approach 2.
EDIT - adding some example code to clarify how I'm doing things so far.
class StateMachine
{
private State currentState;
public void SetState(SimpleState newState)
{
state = newState;
}
public void Tick()
{
state.Tick();
}
}
abstract class State
{
public parentStateMachine;
protected void SetState(State newState) {parentStateMachine.SetState(newState);}
public void Tick()
{
Update();
Transition();
}
protected virtual void Update(){}
protected virtual void Transition(){}
}
the state machine tick is then called for example as:
public void Update() // game loop update inside Unity3D
{
stateMachine.Tick();
queries.Clear(); // Clears all the bool values inside queries
}
an example of how a Transition override could look when using approach 1:
protected override void Transition()
{
if (queries.PickupAProp && currentProp == null)
{
SetState(PickupPropState);
return;
}
if (2secondsPassedSinceEnteringThisState() && queries.WalkToTarget)
{
SetState(WalkState);
return;
}
}
in other words, all the logic concerning decision of what state to choose next is implemented mostly in the state extension itself. "queries" really just work as something that holds various data / signals. It shouldn't contain any complicated logic and in my current implementation it's mostly booleans.
The alternative approach 2, could then replace certain "checks" like so:
public override void TryPickupProp()
{
if (currentProp == null)
{
SetState(PickupPropState);
return;
}
}
In places where I did queries.PickupAProp = true; I could now instead just do stateMachine.state.TryPickupProp();
But Transition() should still contain the check:
if (2secondsPassedSinceEnteringThisState() && queries.WalkToTarget)
{
SetState(WalkState);
return;
}
As that's a purely "internal" check. But the TryPickupProp() query could now just be called directly.