Scalable Hierarchical State Machine for In-Game Characters (Player and AI)

Hey everyone,

I’ve been pondering over possible implementations of a Hierarchical State Machine for in-game entities (controlled by either players or AI) that could scale well for character/entity systems of increasing complexity.

[NOTE]
I understand this post is long and winding, so I’d be happy to clarify any points or questions. Heck, I’ll even make pictures if it’d help!
I’m especially prone to overthinking things and haven’t personally witnessed the performance impact of my code at a true “game”-level scale (I guess think Metroidvania scale for starters?). I respect KISS and YAGNI, but I believe this system could come in handy. Any guidance, advice WELCOME!!!

In terms of features and optimization, I’ve been shooting for the following:

  • Avoid garbage collection whenever possible, if not always. Inspired by Playdead’s (Limbo, INSIDE devs) Unite 2016 talk on optimization
    https://www.youtube.com/watch?v=mQ2KTRn4BMI

  • Check for transition conditions per frame-tick ONLY when necessary.

  • I think checking for ground contact in a humanoid GameObject’s locomotion states every fixed update can be reasonable if the entity functions like a platformer character (Megaman X, Hollow Knight, Guacamelee) with abilities like wall-jumping, air-dashing, etc.), or if they move along uneven terrain and calculate their velocity based on the ground’s normals (Sonic with its curves, ramps, and loops).

  • I think checking for damage or other character interactions every tick is excessive.

  • Check for transition conditions by events whenever possible

  • Given one typically initiates damage-damageable interactions is via callbacks such as a MonoBehaviour’s OnCollisionEnter method, it’d seem reasonable to implement a state callback that fires only when the entity receives the damage event and handles it on a state-by-state basis.

  • For instance, consider a character with multiple top-level state machines, one dedicated to the character’s “vulnerability” (vulnerable, invulnerable due to invincibility frame post-damage) and the other to handling movement, attacks, and calling for their corresponding animations. Some script managing both state machines would subscribe to the Dameagable component’s OnDamageReceived callback, and feed any damage results to each state machine, Vulnerability first and General second. If the character is briefly in the “invulnerable” state, Vulnerability could “consume” the event and prevent it from propagating to General, as General should only need to worry about triggering recoil from actual damage events.

My design approaches so far

Common setups

Represent a state with a State class/object. To start, all states would share the methods OnEnter and OnExit.
The StateMachine class extends State itself, so that one may layer states without explicit class inheritance.

States are not MonoBehaviours. They don’t have FixedUpdate or Update functions magically cached by Unity. Instead, an external MonoBehaviour will contain the State Machines and handle ticking them (if necessary, more details in approaches below) as well as passing events to them.

States machines contain a pool of States, so states themselves do not initialize new state objects, only for them to be discarded between transitions.

Approach A: Coroutine-Driven

During OnEnter(), a State could start a Coroutine which in yields to Coroutines that execute in-line with Update and/or FixedUpdate, by yielding to WaitOnUpdate() and WaitOnFixedUpdate() respectively (Via the Flyweight pattern, I cache these values within a static CoroutineHelper class to reduce/eliminate garbage collection, rather than instantiating new objects each yield). The State would stop these Coroutines during OnExit().

During its execution, the State machine would run a Coroutine which should simply yield for the completion of the current state’s execution coroutine. It would feed events into the states via delegates.

While I enjoy the concept of individual states handling whether they include an Update and/or FixedUpdate-based tick, it seems very hard to manage correctly. On the other hand, I’d likely drive animation through Animancer, a great code-based add-on that allows easily yielding for an animation clip to complete rather than checking whether it has completed via a Tick method, so I guess a guy can dream.

Also, I haven’t explicitly profiled the overhead of stopping and starting coroutines on a state-by-state basis as opposed to settling with empty TickUpdate() and TickFixedUpdate() methods in certain states.

Approach B: Tick-Driven

Expand the state interface to include TickUpdate() and TickFixedUpdate(). Unfortunately, the State Machine would then tick states that may only require events; these states would have empty TickUpdate() and TickFixedUpdate() methods.

Other additions

States accept references to their State machine and a Monobehaviour via their method parameters. States would also also accept a reference to a Blackboard object that caches any relevant components on the GameObject.

I suppose, for the sake of easier testability, I should work to prevent states from calling any MonoBehaviour components directly; I could instead have more controllers to abstract these components.

  • EX: A GroundMovementController and AirMovementController with functions that accept directional vectors and handle moving the character via their Rigidbody component. Upon receiving input, a “ground-walk” state could send it to the GroundMovementController.

[HELP] My obstacles/shortcomings

Following object-oriented programming by using a State interface / abstract class, but also feeding custom events on a state-by-state basis.

Let’s theoretically add an OnEvent() function to State. The MonoBehaviour containing the State Machines (let’s call it CharacterController for now), would subscribe to events/delegates on other components, such as OnDamage() on the DamageableController. It would then pass these events on to the State Machines, which in turn pass them to the active states. Given the hierarchical nature of this state machine, each state can attempt to handle the event before passing it to the active sub-state. For instance, the Grounded state machine could handle damage events directly without passing them to the Idle or Run state.

Ideally, OnDamage() would also pass parameters related to the damage received-- consider OnDamage(DamageTypeEnum type, int amount). I’d like CharacterController to pass these parameters to the corresponding State Machines. However, different parameters would require further extending the State interface, hardcoding its possible events. Does there exist a means of wrapping these parameters without creating garbage?

I could redefine OnEvent() as OnEvent(string/enum eventType, IEventParams params), having all possible parameters extend from an empty interface IEventParams; individual states could then cast the parameter based on the eventType. In most cases, however, I’d imagine this method would generate garbage. I could attempt to cache/reuse a single instance for each type of event and change its values per event, but there’d be trouble if multiple damage events occured in a single frame.

Another approach would involve removing the IEventParams argument from OnEvent(), and instead storing event-based information in the Blackboard accessible by all states. I believe, however, that a specific implementation of the Blackboard interface/class would require making States/State Machines generic, restricting the sharing of states between characters with similar actions but slightly different Blackboards / event listeners. Having the Blackboard contain a Dictionary of component types mapped to objects would bypass the Generic type approach, but at the cost of (most likely) boxing value types.

I’d say this issue extends to input-driven events, both those by human players and AI; one could describe a button-based input (“jump”, “attack”) with a simple string, but axis-driven inputs would also require a value (a Vector2 containing the horizontal and vertical axes, perhaps).

[SIDE SPECULATION, ADVICE APPRECIATED] “Dream” Features beyond my mortal comprehension

Making state transitions extendable at runtime.

Say your player character begins with a basic jump ability, but obtains a double jump later on. Sure, one could manage a blackboard variable, like a boolean hasDoubleJump, and have the Jump state check it upon receiving a “jump” input event… but what if the Jump state had no knowledge of this transition prior to the character receiving this upgrade? Upon the player character receiving the upgrade, some script / ScriptableObject / whatnot could append the jump-to-double-jump transition to the character’s movement state machine.

[Heavy Speculation]

We could abstract transitions with objects, themselves, and pair each State with a dictionary of event names mapped to transitions, but now we run the risk of processing input event logic both inside and outside of States.

Consider the Walk state, passes input-based movement vectors to the GroundMovementController as they come along. If the vector’s squared magnitude falls below a certain value (into the deadzone), then the Walk transitions to Idle state. Now, would one handle this transition within the extendable transition dictionary/list/container, or within the state?

[More Possible Approach?]

Another route involves the State Machine’s pool of States. If a destination state does not exist in the pool, we simply don’t end the current state or transition to the destination state. In this case, we simply add the Double Jump state to the movement machine’s pool after the character obtains this upgrade.

1 Like

Some brief thoughts:

Simplicity is the key. Overengineering will be the constant devil on your shoulder as you design this.

For simplicity, check the current state’s transitions every frame, BUT only check blackboard values. Events should update blackboard values. For example, a state transition shouldn’t check for collisions. A collision event should set a blackboard value, and the transition can check the value on its tick.

If you’re layering FSMs, keep each FSM simple and focused. This makes each FSM versatile and also easy to test in isolation. One layer shouldn’t control another layer. Instead, layer A can set blackboard values that layer B can detect to change its state.

Allow states to talk to MonoBehaviours, but keep the logic of states and transitions separate from whatever tasks the MonoBehaviours do.

There’s no reason why you can’t design the SM to insert states at runtime. If you’re concerned about GC, preallocate all states and leave unconnected ones sitting in memory until needed.

Keep in mind how to save and restore state machines (e.g., in saved games and scene changes).

Also implement a visual representation of the state machine, even if it’s rudimentary. It will help debugging immensely.

Thanks for nudging me towards simplicity; with my tendency to overthink things, over-engineering could really threaten my workflow.

Now, if a state would check the Blackboard’s values, then I would need to extend the Blackboard for varying implementations with different values (assuming no casting and object type assumptions made by States) and make States generic via State.

Do you find it reasonable that the Grounded state, for example, checks every tick whether the “underwater” Blackboard value has been changed to true? My personal approach, given a less-flexible Blackboard structure, would simply be to have the event send a key to the state, and only then have the state check the corresponding value. State would have OnEvent(string key); upon receiving the key “underwater”, the Grounded state would check the “underwater” Blackboard variable and transition accordingly if it is true.

This is just thought, however. I fear I’m severely overestimating the impact of per-tick operations as the number of co-existing entities exist (say, moving from smaller Metroidvania areas to larger Dark Souls-like areas); I understand that profiling could help find answers, but if I’m overthinking the event-vs-tick system with even just including keys, please let me know!

Thanks again!

You may be predesigning around a non-issue. I highly doubt a single state checking a few (or even a few dozen) blackboard values every frame will have any impact, especially if you keep your blackboard efficient. F.E.A.R.'s blackboard was just a set of bools (a bit array), and it did fine. You could probably cover all your blackboard needs with a dictionary of bools and a dictionary of ints.

Putting it all on the blackboard also makes it easier to see the current state, without having to trace through a mess of distributed events and keys. Also, it keeps everything even better decoupled than event registration.

This is extremely helpful to hear, thanks!

Adding to the Blackboard subject, would you ever store input-based info in there?

Consider a Dash state which can be reached from Walk state. Dash uses the movement Vector2 provided by inputs during these states to determine the dash direction (Say, the Walk state would transition on a button being pressed (abstracted by the Input as “dash”) and also take the current movement vector. Should it set this Vector2 value in the Blackboard?

That would probably work fine. Your input component (fed either by human player input or an AI) could set various values on the blackboard, and the states and transitions could check those values.

When you get your system implemented, please consider posting your thoughts on it here. We get a lot of armchair designs on the forum, and it’s always great when it’s backed up by an analysis of the actual implementation.

Sorry for the delay; would you suggest states consume these blackboard changes by resetting them, e.g. changing “damaged” back to false upon handling it?

Sure, I’d be happy to!

1 Like

(Bumping because I also asked another question before my “Sure…” post, but here’s another query too) Given the division of tasks between Update and FixedUpdate, how would you personally handle logic affected by FixedUpdate checks? Say, checking if the character is grounded. Would the raycasting in FixedUpdate trigger a transition directly, or set the Blackboard variable to be seen and handled by the state in Update, or set a variable within the state to be handled in Update?

That should work. Just try to maintain a consistent set of rules, as strictly as possible, for who’s responsible for changing blackboard values. Otherwise down the road you’ll be scratching your head trying to figure out why a value is changing.

If possible, don’t let MonoBehaviour methods (e.g., FixedUpdate) trigger transitions. Keep a strict separation of duties. For example:

  • MonoBehaviours can set blackboard values, but can’t read blackboard values or have any knowledge of states or transitions whatsoever.
  • States can read and write to the blackboard, and query and command MonoBehaviours. Ideally, they just perform logic, and leave the actual work to MonoBehaviours.
  • Transitions only occur due to blackboard values.

Ah, so this was my train of thought for a setup throughout the conversation:

  • States are Non-MonoBehaviour instances.
  • States are ticked by the State Machine, which is ticked by some encapsulating MonoBehaviour, which likely also serves as a means of referencing MonoBehaviour components and providing them to the State Machine.
  • The MonoBehaviour components, themselves, do not have Update or FixedUpdate functions called by Unity. States query them as neccessary. For instance, a State could query the GroundDetector to determine whether the entity is currently grounded. Given physics-sensitive ticking, the State would query the GroundDetector via TickFixedUpdate(), which is ultimately ticked by the encapsulating MonoBehaviour/Unity’s FixedUpdate().
  • External event data is fed into the Blackboard, likely through the encapsulating MonoBehaviour. E.g. the entity’s Dameagable component has OnDamage(), which is called by some other object’s (likely a hurtbox) Damager component via OnCollisionEnter(). The MonoBehaviour has a callback to the Damageable’s OnDamage(), and modifies the corresponding Blackboard value. The active State checks these values every TickUpdate(), and responds accordingly.

Through this system, the States and just the encapsulating MonoBehaviour may modify the Blackboard values. Only the encapsulator directly performs GetComponent<>() and such on setup, and other MonoBehaviours perform work like raycasts and moving rigidbodies; the States call upon the MonoBehaviours to perform both work (play an uppercut animation, position hurtboxes, launch the entity’s rigidbody upwards) and checks (check whether a downwards raycast intersects with a Ground-tagged object).

What troubles me with this setup is that the States would juggle certain commands between the two Ticks. They’d check inputs in the Update tick, and command position transformations in FixedUpdate. Under my structure, restricting transitions to Blackboard values would mean caching all inputs within the Blackboard; e.g. if the State checks for the controller’s abstracted attack command and see’s it’s been issued (via button press), it would then set that command value in the Blackboard, and transition to the Attack state upon reading that value from the Blackboard?

Just want to clarify upon suggested changes!

There’s an advantage to caching abstracted inputs within the blackboard. The states/transitions that read the inputs from the blackboard don’t have to care where the input is coming from. The input could come from the player, or it could come from an AI simulating gamepad presses – and it could even switch if, for example, the player takes control of an NPC.

There’s no getting around handling some things in Update and others in FixedUpdate. I think the key is to just keep everything as decoupled as possible.

Thank you, I think the picture is getting clearer. I definitely seek to decouple input sources from their destination by abstracting the commands themselves. Initially didn’t think of it following the command pattern, as I would have had states query some input interface for the commands or their values; the controller interface would have been injected into the state machine via reference such that input sources (human, ai brain-controlled controllers) could be swapped seemlessly. However, delegating inputs to changes in Blackboard values through commands does seem like a useful path to take, as it would remove input queries from the states altogether.

Thanks for all your help and patience so far!I I’lljust have to consider the handling of states driven by smart objects/interactions, and most importantly, experiment.

1 Like

Sorry for the delay! My two current approaches take inspiration from Rockstar’s MARPO methodology, creating an Input variable-holding struct on the stack every frame, passing it to a controller for modification, and storing it in the Blackboard for reading by individual states (under the assumption only the controller will modify the struct).

Here’s one implementation relying on Types rather than strings as keys.

State
```csharp
*public abstract class State where T : IBlackboard

{
    public enum TransitionType
    {
        PopSelfPushNew,
        PushNew,
        PopSelf
    }

    public abstract void Enter(StateMachine<T> stateMachine,
        T blackboard, MonoBehaviour host);

    public abstract void Exit(StateMachine<T> stateMachine,
        T blackboard, MonoBehaviour host);

    public abstract void TickUpdate(StateMachine<T> stateMachine,
        T blackboard, MonoBehaviour host);

    public abstract void TickFixedUpdate(StateMachine<T> stateMachine,
        T blackboard, MonoBehaviour host);
}*

```

I’ll likely remove the Blackboard interface and allow for any class, as anything could carry the relevant Controller/Yoke.

State Machine

using PollHSM.Blackboards;
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace PollHSM.States
{
    public class StateMachine<T> : State<T> where T : IBlackboard
    {
        protected Dictionary<Type, State<T>> _statePool;

        protected Stack<State<T>> _priorStates;

        protected State<T> _currentState;

        protected State<T> _startState;

        public StateMachine(State<T> startState)
        {
            _statePool = new Dictionary<Type, State<T>>();
            _startState = startState;
            _priorStates = new Stack<State<T>>(10);
        }

        public StateMachine<T> AddState<E>() where E : State<T>, new()
        {
            // Get state type
            Type stateType = typeof(E);
            // Add new state
            _statePool[stateType] = new E();

            return this;
        }

        public StateMachine<T> AddState(State<T> state)
        {
            // Get state type
            Type stateType = state.GetType();
            // Add state
            _statePool[stateType] = state;

            return this;
        }

        public StateMachine<T> RemoveState<E>() where E : State<T>
        {
            // Get state type
            Type stateType = typeof(E);
            // Remove state
            _statePool.Remove(stateType);

            return this;
        }

        public bool Transition<E>(T blackboard, MonoBehaviour host,
            TransitionType transType) where E : State<T>
        {
            // Get state type
            Type stateType = typeof(E);

            var success = false;

            if (transType == TransitionType.PopSelf
                && _priorStates.Count > 0)
            {
                // Get last state from stack
                _currentState = _priorStates.Pop();
                // Enter last state
                _currentState.Enter(this, blackboard, host);

                // Successfully transitioned
                success = true;
            }
            else if (_statePool.TryGetValue(stateType, out State<T> nextState))
            {
                if (transType == TransitionType.PopSelfPushNew)
                {
                    // Exit current state
                    _currentState.Exit(this, blackboard, host);
                    // Set new state
                    _currentState = nextState;
                    // Enter new state
                    _currentState.Enter(this, blackboard, host);
                }
                else // Push new state
                {
                    // Exit current state
                    _currentState.Exit(this, blackboard, host);
                    // Add current state to stack
                    _priorStates.Push(_currentState);
                    // Set new state
                    _currentState = nextState;
                    // Enter new state
                    _currentState.Enter(this, blackboard, host);
                }

                // Successfully transitioned
                success = true;
            }

            // Transition succeeded?
            return success;
        }

        public override void Enter(StateMachine<T> stateMachine, T blackboard,
            MonoBehaviour host)
        {
            // Set current state
            _currentState = _startState;
            // Enter state
            _currentState.Enter(this, blackboard, host);
        }

        public override void Exit(StateMachine<T> stateMachine, T blackboard,
            MonoBehaviour host)
        {
            // Exit current state
            _currentState.Exit(this, blackboard, host);
        }

        public override void TickFixedUpdate(StateMachine<T> stateMachine,
            T blackboard, MonoBehaviour host)
        {
            _currentState.TickFixedUpdate(this, blackboard, host);
        }

        public override void TickUpdate(StateMachine<T> stateMachine,
            T blackboard, MonoBehaviour host)
        {
            _currentState.TickUpdate(this, blackboard, host);
        }
    }
}

Example Blackboard
```csharp

  • public class BasicBlackboard : IBlackboard
{
    public struct Yoke
    {
        public Vector2 movementVector;
        public bool jumpPressed;
        public bool primaryPressed;
        public bool secondaryPressed;
    }

    public Yoke yoke;

    public GroundDetectionController groundDetectionController;
    public GroundMoveController groundMovementController;
    public FallController fallController;
    public JumpController jumpController;

    public float groundMoveSpeed = 5;
    public float airMoveSpeed = 5;
    public float jumpHeight = 4;
}

}*
```
Example MonoBehaviour

 public class BasicActorController : MonoBehaviour
    {
        public IInputController<BasicBlackboard> _inputController;
      
        private StateMachine<BasicBlackboard> _stateMachine;
        private BasicBlackboard _blackboard;

        private void Awake()
        {
            // Feed in an InputController somehow

            // Add initially-available states
            _stateMachine = new StateMachine<BasicBlackboard>(new Stand())
                .AddState(Jump()).AddState(Fall());

           // Set MonoBehaviour controllers, etc.
            _blackboard = new BasicBlackboard
            {
                groundDetectionController =
                    GetComponent<GroundDetectionController>(),
                groundMovementController =
                    GetComponent<GroundMoveController>(),
                fallController =
                    GetComponent<FallController>(),
                jumpController =
                    GetComponent<JumpController>(),
            };
        }

        private void OnEnable()
        {
            _stateMachine.Enter(null, _blackboard, this);
        }

        private void Update()
        {
            // Fill in yoke
            _inputController.Handle(_blackboard);
            _stateMachine.TickUpdate(null, _blackboard, this);
        }

        private void FixedUpdate()
        {
            _stateMachine.TickFixedUpdate(null, _blackboard, this);
        }
    }

Thoughts on the general setup? I don’t like the current placement of the player/AI controller within the MonoBehaviour; I’d rather it get provided some other way, for a more flexible injection at startup and during gameplay-- that’s one reason I’m considering delegates/events for commands rather than polling as-is.

I’m also struggling to imagine easily swapping between different controller mappings with this setup, moreso with potential AI than players. Rockstar addressed this by simply including all possible actions within a Yoke, like gas-- if the character isn’t driving a vehicle, then they just won’t fill in the value, or they’ll just supply different yokes altogether. I suppose the AI brain must be aware of the entity’s state machines to handle cases such as vehicles as well.

As I implement the States, I’m also-also struggling to devise a way in which these states can be reused for different contexts. Initially, I sought to create a general state which takes an animation clip, plays the clip on start, and exists upon its completion. This interface state pool system, however, would force me to store clips within the Blackboard. Come to think of it, that could work well for certain systems, like if I referenced the ScriptableObject for the currently-equipped weapon or vehicle in the Blackboard. I could then have animations chain off one another by following a tree based on, say, weapon combo inputs. Still seems like it could bloat the base Blackboard, however.

Do you have anything playable yet? If so, what do you think of the implementation of individual states, both in terms of initially writing a state and then maintaining the code throughout the project’s lifetime? Easy? Cumbersome? Clear? Unclear?

Regarding controller mappings, your data’s going to end up somewhere. Throwing all the possible inputs onto the yoke is at least simple to implement, and it makes all the inputs apparent in a single place. You could probably devise a more sophisticated approach, but why bother? Especially if it makes things less clear.

I do have some test states implemented, yes; they currently implement my other State interface, which relies on string keys rather than Types.

Standing state

public class Stand : KeyPairState<BasicBlackboard>
    {
        public override void Enter(KeyPairStateMachine<BasicBlackboard> stateMachine,
            BasicBlackboard blackboard, MonoBehaviour host)
        {
            // TODO Enter idle animation
        }

        public override void Exit(KeyPairStateMachine<BasicBlackboard> stateMachine,
            BasicBlackboard blackboard, MonoBehaviour host)
        {
        }

        public override void TickFixedUpdate(
            KeyPairStateMachine<BasicBlackboard> stateMachine,
            BasicBlackboard blackboard, MonoBehaviour host)
        {
            if (blackboard.groundDetectionController
                .Detect(out RaycastHit hit))
            {
                // Move
                blackboard.groundMovementController.Move(hit.normal,
                    blackboard.groundMoveSpeed,
                    blackboard.yoke.movementVector);

                // Jumping
                if (blackboard.yoke.jumpPressed)
                {
                    if (stateMachine.Transition(blackboard, host,
                        TransitionType.PopSelfPushNew, "jump"))
                    {
                        return;
                    }
                }
            }
            else
            {
                // Falling
                if (stateMachine.Transition(blackboard, host,
                        TransitionType.PopSelfPushNew, "fall"))
                {
                    return;
                }
            }
        }

        public override void TickUpdate(
            KeyPairStateMachine<BasicBlackboard> stateMachine,
            BasicBlackboard blackboard, MonoBehaviour host)
        {
        }
    }

I’m quite inexperienced with several elements of Unity’s animation system, like Mechanim and blend trees. However, I’ve taken an interest in Animancer which allows for firing animation clips from code with more options (dynamically adding animation notifies, etc.).

I had initally intended to reference the abstracted animation controller in the Blackboard/Entity, allowing states to call upon it directly.

Example Attack State (Psuedocode)

public class Stand : KeyPairState<BasicBlackboard>
    {
        public override void Enter(KeyPairStateMachine<BasicBlackboard> stateMachine,
            BasicBlackboard blackboard, MonoBehaviour host)
        {
            /* Get current attack animation scriptable object from Blackboard. This could either be associated with a particular item, or a general combo. The object contains the corresponding animation clip, collision box positioning, and windup/damage/recovery frames. */
        }

        public override void Exit(KeyPairStateMachine<BasicBlackboard> stateMachine,
            BasicBlackboard blackboard, MonoBehaviour host)
        {
        }

        public override void TickFixedUpdate(
            KeyPairStateMachine<BasicBlackboard> stateMachine,
            BasicBlackboard blackboard, MonoBehaviour host)
        {
            /* Move entity in correspondance to certain animations. E.g. dashing punch requires forward movement. */
        }

        public override void TickUpdate(
            KeyPairStateMachine<BasicBlackboard> stateMachine,
            BasicBlackboard blackboard, MonoBehaviour host)
        {
            */ Check for interrupts (damage, falling, etc.) and animation events. /*
        }
    }

I’m likely thinking too big when it comes to scalability problems. Clearly Naughty Dawg and co. are onto something good with their script-driven state machines, but I’m no Naughty Dawg; I need to start thinking in terms of what I can do short of creating scripting languages for my non-existant design team.

How would you go about (de)coupling states with animations given attacks like those in Super Smash Bros, which have characters physically move in unique ways during their attacks?

What about shifting the specifics into the animations themselves, and using animation events? For example, say your yoke has inputs for 3 different types of attacks: attack_low, attack_mid, and attack_high. Your state can just tell Animancer to play the attack_low animation (or the Mecanim attack_low state if you use Mecanim). Each agent’s attack_low animation can have animation events that do things like activate hit boxes or spawn projectiles, specific to the agent.

Also take a look at how Jeff Orkin implemented AI in F.E.A.R. (The classic paper Three States and a Plan: The AI of F.E.A.R.) His agents’ state machine only has three states: Goto, Animate, and Use Smart Object. Simple to implement and debug, and very flexible since all the unique behavior is contained in the different animations and smart objects.

I’m coming to like the idea of storing logic/flow in the animations when possible. That way I can relay relevant information to the states without requiring them to know the exact clip playing.

I’m glad you brought up this article, as it’s definitely inspired me in terms of scalability. However, I’ve struggled to conceptualize such a simple machine in the context of entity body states, not simply decision-making. Where would basics such as Standing and Walking fall into this? Monolith decoupled goals from actions, so I would see Stand, Jump, Walk, Crouch as actions. Where would the transitions be managed, such that the entity can’t Jump from a Crouch? I’m currently still seeing these things as pieces of a state machine, in some sense.

As always, I appreciate your insight. I’ll continue to re-read the F.E.A.R. paper, hopefully change my perspective and understanding.

Sorry, my links and thoughts have been all over the place. And it looks like I’m going to continue to be all over the place. :slight_smile: To be honest, I forgot that the original topic was hierarchical/layered FSMs. Taking a step back, maybe you can think of the Mecanim animator controller (or equivalent) as the bottom FSM layer. Layered FSMs work best if you’re strict about communication. A lower layer can send info up to the layer above it. The layer above it can send commands down to the layer below it. Say you have these layered FSMs, from top to bottom:

  • Tactical (fight, flee, etc.)
  • Combat (shoot, reload, move to cover, etc.)
  • Weapon (fire, etc.) || Movement (stand, crouch, jump, etc.) [2 separate FSMs in this layer]
  • Animator

The Weapon FSM can command the Animator FSM to play a fire animation.

The Animator FSM can use an animation event to report to the Weapon FSM on the frame in which a bullet should spawn.

The Movement FSM can manage movement transitions (e.g., no jump from crouch). It can also command the Animator FSM (e.g., play crouch animation).

1 Like