Skip to content

Player Controls With Finite State Machine Using C# in Unity

Player Controls With Finite State Machine Using C# in Unity

In this tutorial, we will implement Player Controls With Finite State Machine Using C# in Unity. We will use the Finite State Machine created in the earlier tutorial to control a player game object.


Part 1 introduces a Finite State Machine and implements a generic Finite State Machine in C#.

Part 2 uses the Finite State Machine created in part 1 and applies to a Unity project in a simple, straightforward UI implementation of a Splash Screen.

This tutorial is Part 3 of our Finite State Machine tutorial. This tutorial demonstrates the use of the same Finite State Machine and applies to a complex Unity project which handles multiple animation states of a 3D animated character.

Part 4 uses delegates to define the functionality of the state and create the Finite State Machine. After that, we will demonstrate the use of the Finite State Machine by creating a simple key press based application in Unity.

In part 5 of the tutorial, we will move further by demonstrating an enemy NPC behaviour, which handles multiple animation states of a 3D animated character, using the delegate based FSM created in Part 4 of the tutorial.


Section 1 – Configure the Player and the scene

Our first step in creating Player Controls With Finite State Machine Using C# in Unity will be configuring the character and the sample scene. Refer to A Configurable Third-Person Camera in Unity to create the sample scene, the player and the third-person camera control necessary for this tutorial. Complete all the sections and then proceed from below.

Once you have completed, you will notice that the Animator comprises only the Movement motions. We will now add some other motions to make the player controls.

Attack Animations

We will add three attacks (in this case, shooting) animations for this character, viz., Attack1, Attack2 and Attack3. The asset comes with:

* Shoot_AutoShot_AR.fbx,
* Shoot_BurstShot_AR.fbx, and
* Shoot_SingleShot_AR.fbxCode language: CSS (css)

animations. We will use these three animations and tie them to Fire1 (left mouse button or the left ctrl key), Fire2 (the left alt key) and Fire3 (the left shift key). Do note that in this tutorial, we will target PC, MAC & Linux Standalone Build. We will not target this demo for the Android build for now.

Go ahead and open the PlayerAnim animator in Unity.

Add three Boolean parameters called Attack1, Attack2 and Attack3.

Add the three animations of shooting into the Animator. We will create the Transitions later.

Reload and Die Animations

Add two new motions called Reload and Die into the Animator. The asset comes with Reload.fbx and Die.fbx motion files. After that, add two new Trigger parameters called Reload and Die.

Jump and Crouch Animations

Finally, we added two animations in the Animator called Jump and Crouch with the Jump.fbx and Idle_Ducking_ar.fbx. We will also need to add a new Boolean parameter called Crouch.

Animation Transitions

Die Animation Transition

Now we are ready to create the Animation Transition. Die animation can be transitioned from Any State by calling the Trigger Die.

Disable Has Exit Time.

Ensure that Die animation is not looping. If it is looping, then select the Die animation and deselect the Loop Time shown below. Click on Apply to apply the changes.

Attack Animation Transitions

All attack animations can be transitioned from the Ground Movement blend state.

Attack1, Attack2, orAttack3 animations are transitioned based on setting the Boolean parameters Attack1, Attack2 or Attack3 as true.

When set to false the animation transitions back to the Ground Movement blend state. Ensure that Has Exit Time is disabled for both transitions.

Reload Animation Transition

The Reload animation transition happens from all Attack animations when Reload trigger is executed. The Reload animation is played and then transits to Ground Movement blend state animation.

Ensure to check the Has Exit Time checkbox as you want this animation to exit after one complete animation run. Also, disable looping for this motion sequence.

Crouch and Jump Animations

Finally, add the Crouch using the Idle_Ducking_ar.fbx and Jump using the Jump.fbx animations. Make the transition to and from the Ground Movement blend state. For crouch, you will add a Boolean parameter called Crouch which, when true, will show the Crouch animation, and when false, show the Ground Movement animations. Look below for the final Animator.

Section 2 – Implement the Finite State Machine

Select Player from the Hierarchy and add a new Script called Player.cs.

Open Player.cs in Visual Studio or your favourite editor. Add using Patterns to have FSM in your namespace.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Patterns; // Add Patters for FSM
Code language: C# (cs)

Next, we will add the different state types for our Player FSM.

public enum PlayerFSMStateType
{
    MOVEMENT = 0,
    CROUCH,
    ATTACK,
    RELOAD,
    TAKE_DAMAGE, // we won't use this as we do not have an animation for this state.
    DEAD,
}
Code language: C# (cs)

For convenience, we will derive PlayerFSMState from State and PlayerFSM from FSM.

public class PlayerFSMState : State
{
    // For convenience we will keep the ID for a State.
    // This ID represents the key
    public PlayerFSMStateType ID { get { return _id; } }

    protected Player _player = null;
    protected PlayerFSMStateType _id;

    public PlayerFSMState(FSM fsm, Player player) : base(fsm)
    {
        _player = player;
    }

    // A convenience constructor with just Player
    public PlayerFSMState(Player player) : base()
    {
        _player = player;
        m_fsm = _player.playerFSM;
    }

    // The following are the normal methods from the State base class.
    public override void Enter()
    {
        base.Enter();
    }
    public override void Exit()
    {
        base.Exit();
    }
    public override void Update()
    {
        base.Update();
    }
    public override void FixedUpdate()
    {
        base.FixedUpdate();
    }
}
Code language: C# (cs)

The PlayerFSM has three helper methods that interact with the PlayerFSMStateType and the new PlayerFSMState. These are just convenience methods that allow easier access to our base FSM.

public class PlayerFSM : FSM
{
    public PlayerFSM() : base()
    {
    }

    public void Add(PlayerFSMState state)
    {
        m_states.Add((int)state.ID, state);
    }

    public PlayerFSMState GetState(PlayerFSMStateType key)
    {
        return (PlayerFSMState)GetState((int)key);
    }

    public void SetCurrentState(PlayerFSMStateType stateKey)
    {
        State state = m_states[(int)stateKey];
        if (state != null)
        {
            SetCurrentState(state);
        }
    }
}
Code language: C# (cs)

Player Class

We implement the Player class with the bare minimum functionality. Essentially, our Player class only handles the FSM associated with the Player. However, it will have access to the Animator, the PlayerMovement (implemented in our previous tutorial on Third Person Camera Control) and our newly created PlayerFSM.

public class Player : MonoBehaviour
{
    public PlayerMovement playerMovement;
    public Animator playerAnimator;
    public PlayerFSM playerFSM = null;

    // Start is called before the first frame update
    void Start()
    {

        playerFSM = new PlayerFSM();

        // create the FSM.
    }

    // Update is called once per frame
    void Update()
    {
        playerFSM.Update();
    }
}
Code language: C# (cs)

MOVEMENT State

The movement state handles the normal movement of the Player. Our previous tutorial shows that the movement is dealt with by the PlayerMovement.cs class. So, we disable the calling of Move from Update in PlayerMovement.cs.

    void Update()
    {
        //Move();
    }
Code language: C# (cs)

We will also amend the Move function in PlayerMovement.cs to handle the Jump.

    public void Move()
    {
#if UNITY_STANDALONE
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
#endif
#if UNITY_ANDROID
        float h = mJoystick.Horizontal;
        float v = mJoystick.Vertical;
#endif
#if UNITY_STANDALONE
        float speed = mWalkSpeed;
        if (Input.GetKey(KeyCode.LeftShift))
        {
            speed = mRunSpeed;
        }
#endif
#if UNITY_ANDROID
        float speed = mRunSpeed;
#endif
        if (mFollowCameraForward)
        {
            // Only allow aligning of player's direction when there is a movement.
            if (v > 0.1 || v < -0.1 || h > 0.1 || h < -0.1)
            {
                // rotate player towards the camera forward.
                Vector3 eu = Camera.main.transform.rotation.eulerAngles;
                transform.rotation = Quaternion.RotateTowards(
                    transform.rotation,
                    Quaternion.Euler(0.0f, eu.y, 0.0f),
                    mTurnRate * Time.deltaTime);
            }
        }
        else
        {
            transform.Rotate(0.0f, h * mRotationSpeed * Time.deltaTime, 0.0f);
        }
        mCharacterController.Move(transform.forward * v * speed * Time.deltaTime);
        // Move forward / backward
        Vector3 forward = transform.TransformDirection(Vector3.forward).normalized;
        forward.y = 0.0f;
        Vector3 right = transform.TransformDirection(Vector3.right).normalized;
        right.y = 0.0f;
        if (mAnimator != null)
        {
            if (mFollowCameraForward)
            {
                mCharacterController.Move(forward * v * speed * Time.deltaTime + right * h * speed * Time.deltaTime);
                mAnimator.SetFloat("PosX", h * speed / mRunSpeed);
                mAnimator.SetFloat("PosZ", v * speed / mRunSpeed);
            }
            else
            {
                mCharacterController.Move(forward * v * speed * Time.deltaTime);
                mAnimator.SetFloat("PosX", 0);
                mAnimator.SetFloat("PosZ", v * speed / mRunSpeed);
            }
        }
        // apply gravity.
        mVelocity.y += mGravity * Time.deltaTime;
        mCharacterController.Move(mVelocity * Time.deltaTime);
        if (mCharacterController.isGrounded && mVelocity.y < 0)
            mVelocity.y = 0f;
        if (Input.GetKey(KeyCode.Space))
        {
            Jump();
        }
    }Code language: C# (cs)

And then, we add the Jump method.

    void Jump()
    {
        if (mCharacterController.isGrounded)
        {
            mAnimator.SetTrigger("Jump");
            mVelocity.y += Mathf.Sqrt(mJumpHeight * -2f * mGravity);
        }
    }
Code language: C# (cs)

We now add the class for handling the MOVEMENT state type.

public class PlayerFSMState_MOVEMENT : PlayerFSMState
{
    public PlayerFSMState_MOVEMENT(Player player) : base(player)
    {
        _id = PlayerFSMStateType.MOVEMENT;
    }

    public override void Enter()
    {
        base.Enter();
    }

    public override void Update()
    {
        base.Update();

        // call PlayerMovement's Move method.
        _player.playerMovement.Move();

        //_player.playerEffects.Aim();
        if (Input.GetButton("Fire1")) // the left CTRL or the left mouse button.
        {
            PlayerFSMState_ATTACK attackState = (PlayerFSMState_ATTACK)_player.playerFSM.GetState(PlayerFSMStateType.ATTACK);
            attackState.AttackId = 0;
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.ATTACK);
        }
        if (Input.GetButton("Fire2")) // the left ALT key
        {
            PlayerFSMState_ATTACK attackState = (PlayerFSMState_ATTACK)_player.playerFSM.GetState(PlayerFSMStateType.ATTACK);
            attackState.AttackId = 1;
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.ATTACK);
        }
        if (Input.GetButton("Fire3")) // the left SHIFT key
        {
            PlayerFSMState_ATTACK attackState = (PlayerFSMState_ATTACK)_player.playerFSM.GetState(PlayerFSMStateType.ATTACK);
            attackState.AttackId = 2;
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.ATTACK);
        }
        if (Input.GetButton("Crouch")) // The TAB key.
        {
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.CROUCH);
        }

    }
    public override void FixedUpdate()
    {
        base.FixedUpdate();
    }
}
Code language: C# (cs)

Do note that for Crouch, I have added the key Tab to the Input Manager for Unity which can be accessed through Edit->Project Settings.

CROUCH State

public class PlayerFSMState_CROUCH : PlayerFSMState
{
    public PlayerFSMState_CROUCH(Player player) : base(player)
    {
        _id = PlayerFSMStateType.CROUCH;
    }

    public override void Enter()
    {
        _player.playerAnimator.SetBool("Crouch", true);
    }
    public override void Exit()
    {
        _player.playerAnimator.SetBool("Crouch", false);
    }
    public override void Update()
    {
        if (Input.GetButton("Crouch"))
        {
        }
        else
        {
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
        }
    }
    public override void FixedUpdate() { }
}
Code language: C# (cs)

ATTACK State

We will also introduce 4 new variables in the Player class to allow Reload handling ammunition (bullets) count.

public class Player : MonoBehaviour
{
    public PlayerMovement playerMovement;
    public Animator playerAnimator;
    public PlayerFSM playerFSM = null;

    public int maxAmunitionBeforeReload = 40;
    public int totalAmunitionCount = 100;
    [HideInInspector]
    public int bulletsInMagazine = 40;
    public float roundsPerSecond = 10;

********
Code language: C# (cs)

We have three different Attack animations so that we will use three different Attack types. We introduce a variable called _attackID that will represent the type of Attack animation to use.

public class PlayerFSMState_ATTACK : PlayerFSMState
{
    private float m_elaspedTime;

    public GameObject AttackGameObject { get; set; } = null;

    public PlayerFSMState_ATTACK(Player player) : base(player)
    {
        _id = PlayerFSMStateType.ATTACK;
    }

    private int _attackID = 0;
    private string _attackName;

    public int AttackId
    {
        get
        {
            return _attackID;
        }
        set
        {
            _attackID = value;
            _attackName = "Attack" + (_attackID + 1).ToString();
        }
    }

    public override void Enter()
    {
        //Debug.Log("PlayerFSMState_ATTACK");
        _player.playerAnimator.SetBool(_attackName, true);
        m_elaspedTime = 0.0f;
    }
    public override void Exit()
    {
        //Debug.Log("PlayerFSMState_ATTACK - Exit");
        _player.playerAnimator.SetBool(_attackName, false);
    }
    public override void Update()
    {
        //Debug.Log("Ammo count: " + _player.totalAmunitionCount + ", In Magazine: " + _player.bulletsInMagazine);
        if (_player.bulletsInMagazine == 0 && _player.totalAmunitionCount > 0)
        {
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.RELOAD);
            return;
        }

        if (_player.totalAmunitionCount == 0)
        {
            //Debug.Log("No ammo");
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
            //_player.playerEffects.NoAmmo();
            return;
        }

        //_player.playerEffects.Aim();

        if (Input.GetButton("Fire1"))
        {
            _player.playerAnimator.SetBool(_attackName, true);
            if (m_elaspedTime == 0.0f)
            {
                Fire();
            }

            m_elaspedTime += Time.deltaTime;
            if (m_elaspedTime > 1.0f / _player.roundsPerSecond)
            {
                m_elaspedTime = 0.0f;
            }
        }
        else
        {
            m_elaspedTime = 0.0f;
            _player.playerAnimator.SetBool(_attackName, false);
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
        }
    }

    void Fire()
    {
        float secs = 1.0f / _player.roundsPerSecond;
        //_player.playerEffects.DelayedFire(secs);
        _player.bulletsInMagazine -= 1; ;
    }
}
Code language: C# (cs)

RELOAD State

public class PlayerFSMState_RELOAD : PlayerFSMState
{
    public float ReloadTime = 3.0f;
    float dt = 0.0f;
    public int previousState;
    public PlayerFSMState_RELOAD(Player player) : base(player)
    {
        _id = PlayerFSMStateType.RELOAD;
    }

    public override void Enter()
    {
        //Debug.Log("PlayerFSMState_RELOAD");
        _player.playerAnimator.SetTrigger("Reload");
        dt = 0.0f;
    }
    public override void Exit()
    {
        if (_player.totalAmunitionCount > _player.maxAmunitionBeforeReload)
        {
            _player.bulletsInMagazine += _player.maxAmunitionBeforeReload;
            _player.totalAmunitionCount -= _player.bulletsInMagazine;
        }
        else if (_player.totalAmunitionCount > 0 && _player.totalAmunitionCount < _player.maxAmunitionBeforeReload)
        {
            _player.bulletsInMagazine += _player.totalAmunitionCount;
            _player.totalAmunitionCount = 0;
        }
        //Debug.Log("PlayerFSMState_RELOAD - Exit");
    }
    public override void Update()
    {
        dt += Time.deltaTime;
        //_player.playerAnimator.SetTrigger("Reload");
        //_player.playerEffects.Reload();
        if (dt >= ReloadTime)
        {
            //Debug.Log("Reload complete in " + dt);
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
        }
    }
    public override void FixedUpdate() { }
}
Code language: C# (cs)

TAKE_DAMAGE State

We do not do anything with our TAKE_DAMAGE State as we do not have any animation for this state. However, we can implement this for other characters with taking hits or take damage animations.

public class PlayerFSMState_TAKE_DAMAGE : PlayerFSMState
{
    public PlayerFSMState_TAKE_DAMAGE(Player player) : base(player)
    {
        _id = PlayerFSMStateType.TAKE_DAMAGE;
    }

    public override void Enter() { }
    public override void Exit() { }
    public override void Update() { }
    public override void FixedUpdate() { }
}

DEAD State

public class PlayerFSMState_DEAD : PlayerFSMState
{
    public PlayerFSMState_DEAD(Player player) : base(player)
    {
        _id = PlayerFSMStateType.DEAD;
    }

    public override void Enter()
    {
        Debug.Log("Player dead");
        _player.playerAnimator.SetTrigger("Die");
    }
    public override void Exit() { }
    public override void Update() { }
    public override void FixedUpdate() { }
}
Code language: C# (cs)

Player Class

Finally, we instantiate the different states in the Player constructor and add them to the FSM.

    void Start()
    {

        playerFSM = new PlayerFSM();

        // create the FSM.
        playerFSM.Add(new PlayerFSMState_MOVEMENT(this));
        playerFSM.Add(new PlayerFSMState_ATTACK(this));
        playerFSM.Add(new PlayerFSMState_RELOAD(this));
        playerFSM.Add(new PlayerFSMState_TAKE_DAMAGE(this));
        playerFSM.Add(new PlayerFSMState_DEAD(this));
        playerFSM.Add(new PlayerFSMState_CROUCH(this));

        playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
    }
Code language: C# (cs)

The Completed Player.cs

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

public enum PlayerFSMStateType
{
    MOVEMENT = 0,
    CROUCH,
    ATTACK,
    RELOAD,
    TAKE_DAMAGE,
    DEAD,
}

public class PlayerFSMState : State
{
    public PlayerFSMStateType ID { get { return _id; } }

    protected Player _player = null;
    protected PlayerFSMStateType _id;
    public PlayerFSMState(FSM fsm, Player player) : base(fsm)
    {
        _player = player;
    }
    public PlayerFSMState(Player player) : base()
    {
        _player = player;
        m_fsm = _player.playerFSM;
    }

    public override void Enter()
    {
        base.Enter();
    }
    public override void Exit()
    {
        base.Exit();
    }
    public override void Update()
    {
        base.Update();
    }
    public override void FixedUpdate()
    {
        base.FixedUpdate();
    }
}

public class PlayerFSMState_MOVEMENT : PlayerFSMState
{
    public PlayerFSMState_MOVEMENT(Player player) : base(player)
    {
        _id = PlayerFSMStateType.MOVEMENT;
    }

    public override void Enter()
    {
        base.Enter();
    }

    public override void Update()
    {
        base.Update();
        _player.playerMovement.Move();

        //_player.playerEffects.Aim();
        if (Input.GetButton("Fire1"))
        {
            PlayerFSMState_ATTACK attackState = (PlayerFSMState_ATTACK)_player.playerFSM.GetState(PlayerFSMStateType.ATTACK);
            attackState.AttackId = 0;
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.ATTACK);
        }
        if (Input.GetButton("Fire2"))
        {
            PlayerFSMState_ATTACK attackState = (PlayerFSMState_ATTACK)_player.playerFSM.GetState(PlayerFSMStateType.ATTACK);
            attackState.AttackId = 1;
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.ATTACK);
        }
        if (Input.GetButton("Fire3"))
        {
            PlayerFSMState_ATTACK attackState = (PlayerFSMState_ATTACK)_player.playerFSM.GetState(PlayerFSMStateType.ATTACK);
            attackState.AttackId = 2;
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.ATTACK);
        }
        if (Input.GetButton("Crouch"))
        {
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.CROUCH);
        }

    }
    public override void FixedUpdate()
    {
        base.FixedUpdate();
    }
}

public class PlayerFSMState_CROUCH : PlayerFSMState
{
    public PlayerFSMState_CROUCH(Player player) : base(player)
    {
        _id = PlayerFSMStateType.CROUCH;
    }

    public override void Enter()
    {
        _player.playerAnimator.SetBool("Crouch", true);
    }
    public override void Exit()
    {
        _player.playerAnimator.SetBool("Crouch", false);
    }
    public override void Update()
    {
        if (Input.GetButton("Crouch"))
        {
        }
        else
        {
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
        }
    }
    public override void FixedUpdate() { }
}

public class PlayerFSMState_ATTACK : PlayerFSMState
{
    private float m_elaspedTime;

    public GameObject AttackGameObject { get; set; } = null;

    public PlayerFSMState_ATTACK(Player player) : base(player)
    {
        _id = PlayerFSMStateType.ATTACK;
    }

    private int _attackID = 0;
    private string _attackName;

    public int AttackId
    {
        get
        {
            return _attackID;
        }
        set
        {
            _attackID = value;
            _attackName = "Attack" + (_attackID + 1).ToString();
        }
    }

    public override void Enter()
    {
        //Debug.Log("PlayerFSMState_ATTACK");
        _player.playerAnimator.SetBool(_attackName, true);
        m_elaspedTime = 0.0f;
    }
    public override void Exit()
    {
        //Debug.Log("PlayerFSMState_ATTACK - Exit");
        _player.playerAnimator.SetBool(_attackName, false);
    }
    public override void Update()
    {
        //Debug.Log("Ammo count: " + _player.totalAmunitionCount + ", In Magazine: " + _player.bulletsInMagazine);
        if (_player.bulletsInMagazine == 0 && _player.totalAmunitionCount > 0)
        {
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.RELOAD);
            return;
        }

        if (_player.totalAmunitionCount == 0)
        {
            //Debug.Log("No ammo");
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
            //_player.playerEffects.NoAmmo();
            return;
        }

        //_player.playerEffects.Aim();

        if (Input.GetButton("Fire1"))
        {
            _player.playerAnimator.SetBool(_attackName, true);
            if (m_elaspedTime == 0.0f)
            {
                Fire();
            }

            m_elaspedTime += Time.deltaTime;
            if (m_elaspedTime > 1.0f / _player.roundsPerSecond)
            {
                m_elaspedTime = 0.0f;
            }
        }
        else
        {
            m_elaspedTime = 0.0f;
            _player.playerAnimator.SetBool(_attackName, false);
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
        }
    }

    void Fire()
    {
        float secs = 1.0f / _player.roundsPerSecond;
        //_player.playerEffects.DelayedFire(secs);
        _player.bulletsInMagazine -= 1; ;
    }
}

public class PlayerFSMState_RELOAD : PlayerFSMState
{
    public float ReloadTime = 3.0f;
    float dt = 0.0f;
    public int previousState;
    public PlayerFSMState_RELOAD(Player player) : base(player)
    {
        _id = PlayerFSMStateType.RELOAD;
    }

    public override void Enter()
    {
        //Debug.Log("PlayerFSMState_RELOAD");
        _player.playerAnimator.SetTrigger("Reload");
        dt = 0.0f;
    }
    public override void Exit()
    {
        if (_player.totalAmunitionCount > _player.maxAmunitionBeforeReload)
        {
            _player.bulletsInMagazine += _player.maxAmunitionBeforeReload;
            _player.totalAmunitionCount -= _player.bulletsInMagazine;
        }
        else if (_player.totalAmunitionCount > 0 && _player.totalAmunitionCount < _player.maxAmunitionBeforeReload)
        {
            _player.bulletsInMagazine += _player.totalAmunitionCount;
            _player.totalAmunitionCount = 0;
        }
        //Debug.Log("PlayerFSMState_RELOAD - Exit");
    }
    public override void Update()
    {
        dt += Time.deltaTime;
        //_player.playerAnimator.SetTrigger("Reload");
        //_player.playerEffects.Reload();
        if (dt >= ReloadTime)
        {
            //Debug.Log("Reload complete in " + dt);
            _player.playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
        }
    }
    public override void FixedUpdate() { }
}

public class PlayerFSMState_TAKE_DAMAGE : PlayerFSMState
{
    public PlayerFSMState_TAKE_DAMAGE(Player player) : base(player)
    {
        _id = PlayerFSMStateType.TAKE_DAMAGE;
    }

    public override void Enter() { }
    public override void Exit() { }
    public override void Update() { }
    public override void FixedUpdate() { }
}

public class PlayerFSMState_DEAD : PlayerFSMState
{
    public PlayerFSMState_DEAD(Player player) : base(player)
    {
        _id = PlayerFSMStateType.DEAD;
    }

    public override void Enter()
    {
        Debug.Log("Player dead");
        _player.playerAnimator.SetTrigger("Die");
    }
    public override void Exit() { }
    public override void Update() { }
    public override void FixedUpdate() { }
}

public class PlayerFSM : FSM
{
    public PlayerFSM() : base()
    {
    }

    public void Add(PlayerFSMState state)
    {
        m_states.Add((int)state.ID, state);
    }

    public PlayerFSMState GetState(PlayerFSMStateType key)
    {
        return (PlayerFSMState)GetState((int)key);
    }

    public void SetCurrentState(PlayerFSMStateType stateKey)
    {
        State state = m_states[(int)stateKey];
        if (state != null)
        {
            SetCurrentState(state);
        }
    }
}

public class Player : MonoBehaviour
{
    public PlayerMovement playerMovement;
    public Animator playerAnimator;
    public PlayerFSM playerFSM = null;

    public int maxAmunitionBeforeReload = 40;
    public int totalAmunitionCount = 100;
    [HideInInspector]
    public int bulletsInMagazine = 40;
    public float roundsPerSecond = 10;

    // Start is called before the first frame update
    void Start()
    {

        playerFSM = new PlayerFSM();

        // create the FSM.
        playerFSM.Add(new PlayerFSMState_MOVEMENT(this));
        playerFSM.Add(new PlayerFSMState_ATTACK(this));
        playerFSM.Add(new PlayerFSMState_RELOAD(this));
        playerFSM.Add(new PlayerFSMState_TAKE_DAMAGE(this));
        playerFSM.Add(new PlayerFSMState_DEAD(this));
        playerFSM.Add(new PlayerFSMState_CROUCH(this));

        playerFSM.SetCurrentState(PlayerFSMStateType.MOVEMENT);
    }

    // Update is called once per frame
    void Update()
    {
        playerFSM.Update();
    }
}

Code language: C# (cs)

In the Unity Editor, drag and drop Player into the Player Movement field and HPCharacter into the Player Animator field. Set the values for the Max Ammunition Before Reload, Total Ammunition Count and Rounds Per Second values.

Select the main camera from the Hierarchy and ensure the setting is set as shown in the diagram.

Click Play and play the game. The video below shows the Player controls using the State Machine.

https://faramira.com/wp-content/uploads/2020/08/ThirdPersonShooterCamera-SampleScene-PC-Mac-Linux-Standalone-Unity-2019.4.4f1-Personal-_DX11_-2020-08-01-11-53-52.mp4

In our next tutorial, we will add Sound effects to our Player.

Read My Other Tutorials

  1. Implement Constant Size Sprite in Unity2D
  2. Implement Camera Pan and Zoom Controls in Unity2D
  3. Implement Drag and Drop Item in Unity
  4. Graph-Based Pathfinding Using C# in Unity
  5. 2D Grid-Based Pathfinding Using C# and Unity
  6. 8-Puzzle Problem Using A* in C# and Unity
  7. Create a Jigsaw Puzzle Game in Unity
  8. Implement a Generic Pathfinder in Unity using C#
  9. Create a Jigsaw Puzzle Game in Unity
  10. Generic Finite State Machine Using C#
  11. Implement Bezier Curve using C# in Unity
  12. Create a Jigsaw Tile from an Existing Image
  13. Create a Jigsaw Board from an Existing Image
  14. Solving 8 puzzle problem using A* star search
  15. A Configurable Third-Person Camera in Unity
  16. Player Controls With Finite State Machine Using C# in Unity
  17. Finite State Machine Using C# Delegates in Unity
  18. Enemy Behaviour With Finite State Machine Using C# Delegates in Unity
  19. Augmented Reality – Fire Effect using Vuforia and Unity
  20. Implementing a Finite State Machine Using C# in Unity
  21. Solving 8 puzzle problem using A* star search in C++
  22. What Are C# Delegates And How To Use Them
  23. How to Generate Mazes Using Depth-First Algorithm

References

  1. https://en.wikipedia.org/wiki/Finite-state_machine
  2. https://gameprogrammingpatterns.com/state.html
  3. https://gamedevelopment.tutsplus.com/tutorials/finite-state-machines-theory-and-implementation–gamedev-11867

1 thought on “Player Controls With Finite State Machine Using C# in Unity”

  1. Hello,

    I fixed the error.

    In State, I add:

    public State() {}

    I think this is a simple version of FSM. There are a lot to be “fixed”.

    For example, when I pressed jump, and then I pressed tab in the air, the animation crouch will take over, and the character will be stuck in the air, not up nor down. Of course when I realised crouch, it falls back to ground.

    To solve this, do I need a different layer of FSM or should I add in different conditions e.g. to stop crouch to be pressed while in the air?

    Also, since Jump and Move are separated, I can only Jump vertically and if I want to jump forward, it seems I have to combine run and jump, which actually defeats the purposes of FSM, right?

    If so, I need to add horizontal movement into jump (without touching Move State).

    Thank you again for the tutorial.

Leave a Reply

Your email address will not be published. Required fields are marked *