So I've been on a team that has been working on a game for quite a few months now and we're hitting some really nasty bloat problems with our character actions.
Question in bold. Accompanying information below it.
For complex character actions with concurrency of other actions, how would I end up refactoring our current system (see below) using a HSM. As what I've done so far poses concurrency issues.
We currently have these 'states/actions' for the characters.
- Idle
- Running/Movement
- Attacking
- Jumping
- Falling
- Attacking While Jumping
- Attacking While Falling
- Movement While Jumping
- Movement While Falling
- and more...
We started using a FSM to keep track of all the states for the character, but it's become massive and unmanageable. When we create a new action that can only execute based on some pre-requisites and/or causes current states to be changed, we have to make sure we check all the states.
Here is a small piece of code from our movement code that tries to determine if the character should move into an idle state.
if (CurrentPosition == PreviousPosition &&
animator != null &&
mState.CurrentHas(StateFlags.Landed) == false &&
mState.CurrentHas(StateFlags.InAir) == false &&
mState.CurrentHas(StateFlags.Attacking) == false &&
mState.CurrentHas(StateFlags.Stunned) == false &&
mState.CurrentHas(StateFlags.Hurting) == false &&
mState.CurrentHas(StateFlags.Falling) == false &&
mState.CurrentHas(StateFlags.FallingNonJump) == false &&
mState.CurrentHas(StateFlags.Charging) == false &&
mState.CurrentHas(StateFlags.Idle) == false)
{
mState.Add(StateFlags.Idle);
animator.AddToQueue(idleAnimation);
}
Clearly this is smelly, ugly, and I know there are better ways. I've been trying to refactor into a HSM (which I don't believe I am doing correctly).
Here is an excerpt.
public interface UnitState
{
void Update(UnitStateBehavior usb);
void Enter(UnitStateBehavior usb);
void Exit(UnitStateBehavior usb);
}
public class IdleState : UnitState
{
public IdleState()
{
}
public void Update(UnitStateBehavior usb)
{
// Can run while idle (move)
// Can attack while idle
// Can jump while idle
}
public void Enter(UnitStateBehavior usb)
{
}
public void Exit(UnitStateBehavior usb)
{
}
}
I feel like I'll start to run into concurrency issues with other actions, such as in the above mentioned list. If I am jumping and start to attack, I'll play my attack animation and do other appropriate things that deal with now being in an attack state. However when the attack is over, what then? I could still be 'jumping' or increasing in Y, or 'falling' decreasing in Y. What if I've hit the ground while in the middle of an attack, I'd need to then go into an idle state and play its animation among other things. Nearly any action and be combined with another at any time dependent on user input. From all I've read about HSM I thought it would be the best decision, but I'm lacking in knowledge of proper implementation.
My implementation based off Aludor's Conversation
Instead of thinking of a stack of states as a literal stack, I bent it to be more of a collection of states, but still sticking mostly to a stack implementation. First in, last out.
I have an Actor class which keeps a reference to a StateCollection class. This StateCollection class keeps a list of states, with various methods for manipulating the list. Peek, Pop, Push, Remove, Replace, Clear. In-addition to manipulating the list, it offers methods for updating the states. FixedUpdate, Update, LateUpdate, for the various states of the game loop.
Each state derives from an abstract class UnitState. The UnitState holds methods for FixedUpdate, Update, LateUpdate, Enter, and Exit. This base class also keeps a reference to the Actor which owns it. The Actor also keeps a list of valid states for that particular Actor type.
Now when input is received by the Actor it is passed onto the StateCollection. This input is then traversed 'down' the stack. The input is consumed by the first state that uses it, so conflicting states do not act on the same input.
Each time a state is pushed to or removed from the stack the appropriate Enter/Exit method is called. Overall, this allows me to fully remove states, replace states (such as turning a jump into a fall), and allows states to run concurrently.