Finite State Machine with Multiple Playable Characters

Hello everyone, it’s my first time posting here.
I’m working on a simple 2D fighting game, so I have multiple characters with different moves and abilities.
Before I used a type of state machine using Enum, but now I’m trying to implement a finite state machine that uses multiple scripts, I have a StateMachine script that takes care of changing state and everything else, a MovementSM script that inherits (?) from StateMachine and sets up all the states, and the Moving and Idle states.

StateMachine

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class StateMachine : MonoBehaviour
{
    protected BaseState currentState;

    public string characterName;
    public float atk, def, speed, fric;
    public float currentSpeed, topSpeed;
    public Vector3 m_Velocity = Vector3.zero;

    public Rigidbody2D rb;

    public Controls controls;
    public Vector2 movement;

    private void OnEnable()
    {
        controls.Enable();
    }

    private void OnDisable()
    {
        controls.Disable();
    }

    void Start()
    {
        currentState = GetInitialState();
        if (currentState != null)
            currentState.Enter();
    }

    void Update()
    {
        if (currentState != null)
            currentState.Update();
    }

    void FixedUpdate()
    {
        Vector3 targetVelocity = new Vector2(MoveInput() * speed, rb.velocity.y);
        rb.velocity = Vector3.SmoothDamp(rb.velocity, targetVelocity, ref m_Velocity, fric);

        if (currentState != null)
            currentState.FixedUpdate();
    }

    public void ChangeState(BaseState newState)
    {
        currentState.Exit();

        currentState = newState;
        currentState.Enter();
    }

    protected virtual BaseState GetInitialState()
    {
        return null;
    }

    public float MoveInput()
    {
        movement = new Vector2(controls.Gameplay.Move.ReadValue<Vector2>().x, controls.Gameplay.Move.ReadValue<Vector2>().y);
        return movement.x;
    }

    private void OnGUI()
    {
        string state = currentState != null ? currentState.name : "no current state";
        string content = characterName + ": " + atk + " - " + def + " - " + speed + " - " + state;
        GUILayout.Label($"<color='black'><size=40>{content}</size></color>");
    }
}

MovementSM

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class MovementSM : StateMachine
{
    public Idle idleState;
    public Moving movingState;

    public PlayableCharacter playableCharacter;

    private void Awake()
    {
        idleState = new Idle(this);
        movingState = new Moving(this);

        characterName = playableCharacter != null ? playableCharacter.charName : "no object attached";

        atk = playableCharacter != null ? playableCharacter.charAtk : 0;
        def = playableCharacter != null ? playableCharacter.charDef : 0;
        speed = playableCharacter != null ? playableCharacter.charSpeed : 0;
        fric = playableCharacter != null ? playableCharacter.charFric : 0;

        controls = new Controls();
    }

    protected override BaseState GetInitialState()
    {
        return idleState;
    }
}

Idle State

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Idle : BaseState
{
    public Idle(MovementSM stateMachine) : base("Idle", stateMachine) { }

    public override void Enter()
    {
        base.Enter();
        stateMachine.topSpeed = 0;
    }

    public override void Update()
    {
        base.Update();
        if (Mathf.Abs(stateMachine.MoveInput()) >= Mathf.Epsilon)
            stateMachine.ChangeState(((MovementSM)stateMachine).movingState);
    }
}

Moving State

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class Moving : BaseState
{
    public Moving(MovementSM stateMachine) : base("Moving", stateMachine) { }

    public override void Enter()
    {
        base.Enter();
        stateMachine.topSpeed = stateMachine.speed;
    }

    public override void Update()
    {
        base.Update();
        if (Mathf.Abs(stateMachine.MoveInput()) < Mathf.Epsilon)
            stateMachine.ChangeState(((MovementSM)stateMachine).idleState);
    }
}

As you can see to initialize a state I refer to the name of the class (Idle, Movement, etc.), and to change state I refer to the MovementSM class.
So now my question would be, how do I implement both “general” states that will be the same for every character like these ones, and specific states for each character that represent their abilities and attacks?
I ask this because I don’t think making the same state for each character would be wise, but I also don’t know how can I tell, for example, the Idle state to call the attack state of a specific character when the attack button is pressed.
The solution may be very simple and I’m being really dumb, but I’m trying to learn more advanced programming skills so bear with me :smile:.

Alas, this is exactly why I stand by my position on generic state machines: note how your basic problem still remains unsolved.

This is my position on generically-written finite state machines (FSMs):

https://discussions.unity.com/t/834040/6

I’m kind of more of a “get it working first” guy.

Your mileage may vary.

So your suggestion would be to not use this method for my state machine and just go back to the old Enum one?

For your specific problem I think I would make each behavior its own “installable” feature for a given character.

That way all characters would have punch and kick, driven by the Punch and Kick scripts, but more-special behaviors would go only on specific characters.

I just find that easier to reason about.

The scripts would probably all connect to some kind of lockout mechanism so that when you are kicking, the kick must complete (or be interrupted by another attack) before the next one can happen.

This lockout mechanism could also control if you can continue moving during a particular attack. High-kick might force you to stop, but punching might let you keep advancing / retreating, but its effect might be modified by the motion of the character.

Considering that each move is unique to a specific character (it’s a very simple game, you have a melee attack, a special ability and pressing both attack and special performs either a grab if you’re near an opponent, or a projectile attack) I might as well go back to my previous system, I had a basic abstract script with all the basic movement, and a series of abstract functions connected to each type of attack that then I override in a character-specific inherited class that I attach to the prefab of that character.

The driving reason to change / refactor is pain. If you have identified pain in your earlier way of using enums, make sure that you are addressing that pain with your change(s). Personally I find that the specific case of a general purpose FSM never actually fixes everything, and at the same time makes debugging FAR harder forever in to the future.

There is a good state machine here you can use: GitHub - Real-Serious-Games/Fluent-State-Machine: Fluent API for creating state machines in C#

I’m not sure what you mean by “tell the Idle state to call the attack state of a specific character”. Why would the movement idle state care about attacking? It should only have movement or idle states.

Input would be handled outside, then when the attack button is pressed, change your MovementSM state to Disabled, then set your AttackSM to Attacking state.

Better state machines have a transition event, so that you’re not setting the states directly (forcing). It allows the current state to decide where it can go.

1 Like

The idea was to check for the button press during a certain state that actually lets you attack (in my case, moving or standing still).
Anyway thanks for the response and the link, I’ll check it out.