Implementing Player Controls With Finite State Machine Using C# in Unity

Share the Post

In this tutorial, we will implement Player controls with Finite State Machines. We will use the Finite State Machine created in the earlier tutorial and apply it 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 is Part 3 of our Finite State Machine tutorial. This tutorial uses the same Finite State Machine and applies to a complex Unity project which handles multiple animation states of a 3D animated character.


Section 1 – Configure the Player and the scene

Our first step will be to configure 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 that is 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 attack (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.fbx

animations. We will use these three animations and tie them to Fire1 (left mouse button or the left crtl 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 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 add two new animations in the Animator called Jump and Crouch with the Jump.fbx and Idle_Ducking_ar.fbx respectively. 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 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 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

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,
}

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();
    }
}

The PlayerFSM has three helper methods that interact with the PlayerFSMStateType and the new PlayerFSMState. These are just convenience methods that allows 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);
        }
    }
}

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();
    }
}

MOVEMENT State

The movement state handles the normal movement of the Player. From our previous tutorial, we know that the movement is handled by the PlayerMovement.cs class. So, we disable the calling of Move from Update in PlayerMovement.cs.

    void Update()
    {
        //Move();
    }

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 &amp;&amp; mVelocity.y < 0)
            mVelocity.y = 0f;

        if (Input.GetKey(KeyCode.Space))
        {
            Jump();
        }
    }

And then we add the Jump method.

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

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();
    }
}

Do note, that for Crouch I have added the key Tab to the Input Manager for Unity. This 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() { }
}

ATTACK State

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

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;

********

We have three different Attack animations so we will use three different Attack types. For this, 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 &amp;&amp; _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; ;
    }
}

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 &amp;&amp; _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() { }
}

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 take hit 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() { }
}

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);
    }

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 &amp;&amp; _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 &amp;&amp; _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();
    }
}

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 Amunition Before Reload, Total Amunition Count and Rounds Per Second values.

Select the MainCamera from the Hierarchy and make sure the setting are set to 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.

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

Leave a Reply

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