I recommend you the chapter on State-Driven Agent Design Programming AI by Example, By Matt Buckland, since a general approach of Finite State Machines is shown there. It proposes a singleton generic FiniteStateMachine System, which
handles the state updating and transitions of all the agents. The states, by the way they are designed (generally and in the book) are also of the kind of singleton classes, but you could make it Component-wise, so they have their reference to their own agents, so you can handle each agent separately (not applied on my example, but it would be just a matter to add a reference to their Agents, and delete the agent parameter on their methods).
Here is a very generic example, the ECS would be separated by Agents (Entitites), States (Components) and a Finite State Machine per Agent Type (System):
/// Entity
public abstract class FSMAgent<T> : MonoBehaviour where T : MonoBehaviour
{
public StateComponent<T> currentState, previousState;
private void Update()
{
FiniteStateMachine<T>.UpdateState(this);
}
}
/// Component
public abstract class StateComponent<T> where T : FSMAgent<T>
{
/// I put it virtual, since maybe there'd be transition actions that you'd like to avoid in certain states.
public virtual void Exit(T agent) { /*...*/ }
public virtual void Enter(T agent) { /*...*/ }
public virtual void Execute(T agent) { /*...*/ }
}
/// System
public class FiniteStateMachine<T> where T : FSMAgent<T>
{
public static void ChangeState(T agent, StateComponent<T> state)
{
/// Store and execute exit of actual state. Execute new state's entrance.
if(agent.currentState != null)
{
agent.previousState = agent.currentState;
agent.previousState.Exit(agent);
}
agent.currentState = state;
agent.currentState.Enter(agent);
}
public static void UpdateState(T agent)
{
/// Execute state and check for conditions and transitions...
if(agent.currentState != null) agent.currentState.Execute(agent);
}
}
So you just have to define classes that inherit from the base, I'll take the example with Halo's Agents, since I read "Brutes" on another answer:
public class Grunt : FSMAgent<Grunt> { /*...*/ }
public class Elite: FSMAgent<Elite> { /*...*/ }
public class Jackal : FSMAgent<Jackal> { /*...*/ }
public class GruntIdle : StateComponent<Grunt> { /*...*/ }
public class EliteIdle : StateComponent<Elite> { /*...*/ }
public class JackalIdle : StateComponent<Jackal> { /*...*/ }
/// And so on...
And they just would call the static reference of the Finite State Machine of their own kind.
Hope it helps.