Sunday, 21 November 2021

Unity Implementing Finite State Pattern (FSM)

 State patterns are very useful when organized.   The theory behind is grouping every behavior in separated  classes and making those classes loosely coupled to the main algorithm

all game object behaviors will be grouped into some classes  and can be maintained by a main Finite State Machine Manager class

State patterns can be implemented with "Observation" systems

in our example, there is a player  moving to the target point  and we will have 3 different states for player

1. Idle state

2. Search State

3. Found state

In our example, there is a cube and a plain  when you click anywhere on-screen player moves to this point and stops

when the game starts Player is at IdleState (there must be an initial state for FSM to work)

so SearchState when you clicked somewhere on-screen

and FoundState when cube reached finish position

 To set up this example create an empty scene and add a plane and add a cube 

and add an EmptyGameObject => Namethis TargetAssigner and add "TargetAssigner" component to EmptyGameObject

and Add "PlayerStateManager" to Cube Object





=> Start by Creating Our IState Interface

  public interface IState
{
// start executing state
    void Enter();
// update in state
    void Tick();
//fixed Update in state
    void FixedTick();
// exiting current sate
    void Exit();
}
 
=> And Base StateMachine Class "StateMachineBase"

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

public abstract class StateMachineBase : MonoBehaviour
{
    public IState CurrentState { get; private set; }
    public IState _previousState;
    bool _inTransition = false;
    public void ChangeState(IState newState)
    {
        if (CurrentState == newState || _inTransition)
            return;
        ChangeStateRoutine(newState);

    }
    public void RevertState()
    {
        if (_previousState != null)
            ChangeState(_previousState);
    }
    void ChangeStateRoutine(IState newState)
    {
        _inTransition = true;
        if (CurrentState != null)
            CurrentState.Exit();
        if (_previousState != null)
            _previousState = CurrentState;
        CurrentState = newState;
        CurrentState.Enter();
        _inTransition = false;
    }
    private void Update()
    {
        if (CurrentState != null && !_inTransition)
            CurrentState.Tick();
    }
    private void FixedUpdate()
    {
        if (CurrentState != null && !_inTransition)
            CurrentState.FixedTick();
    }

}

 
  
*** Those two classes can be used in any project as the main construction for FSM


=> For assigning Cube target there is our assigner (this is only for assigning target)

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

public class TargetAssigner : MonoBehaviour
{
    // delegate for moving player towards point
    public Action<Vector3> MoveCommand = delegate { };


    #region Fields
    Ray ray;
    RaycastHit hit;
    #endregion

    private void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            ray = Camera.main.ScreenPointToRay(Input.mousePosition);
            if (Physics.Raycast(ray, out hit, Mathf.Infinity))
            {
                MoveCommand.Invoke(hit.point);
               
            }
        }
    }

    private void OnDrawGizmos()
    {
        // to see target gizmo
        Gizmos.color = Color.red;
        Gizmos.DrawWireSphere(this.hit.point, 1F);
    }
}

=> Player State Manager

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


[RequireComponent(typeof(Rigidbody))]
public class PlayerStateManager : StateMachineBase
{
    #region Unity Fields
  
 
    [SerializeField]
    TargetAssigner target;
    [SerializeField]
    public  float RotateSpeed;
    [SerializeField]
    public float MoveSpeed;

    #endregion
    #region Public States
    public IdleState IdleState { get; private set; }
    public SearchState SearchState { get; private set; }
    public FoundState FoundState { get; private set; }
    #endregion
    public Vector3 TargetPosition { get;  set; }
    #region Fields
    Material stateMaterial;
    Rigidbody rbd;
    #endregion
    private void Awake()
    {
        rbd = this.GetComponent&ltRigidbody>();
        stateMaterial = this.GetComponent<MeshRenderer>().material;
        IdleState = new IdleState(this, stateMaterial, target);
        SearchState = new SearchState(this, stateMaterial, rbd);
        FoundState = new FoundState(this,stateMaterial,target);
    }
    private void Start()
    {
        ChangeState(IdleState);
    }





}


=> Idle State

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

public class IdleState : IState
{
    PlayerStateManager _playerSM;
    TargetAssigner _targetAssigner;
    Material _material;
    Color _stateColor=Color.yellow;

    public  IdleState(PlayerStateManager playerSM, Material material,TargetAssigner targetAssigner)
    {

        this._playerSM = playerSM;
        this._material = material;
        this._targetAssigner = targetAssigner;
    }

    public void Enter()
    {
        Debug.Log("Idle State Enter");
        _material.color = _stateColor;
        _targetAssigner.MoveCommand += OnNewTargetAcuired;
    }

    public void Exit()
    {
        _targetAssigner.MoveCommand -= OnNewTargetAcuired;
    }

    public void FixedTick()
    {
        
    }

    public void Tick()
    {
        //
        Debug.Log("Idle State Tick");
    }

    void OnNewTargetAcuired(Vector3 newPosition)
    {

        _playerSM.TargetPosition = newPosition;
        _playerSM.ChangeState(_playerSM.SearchState);
    }
    

    // Start is called before the first frame update

}

=> Searcing State

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

public class SearchState : IState
{
    PlayerStateManager _playerSM;
    Rigidbody _rbd;
    Material _material;
    Color _stateColor = Color.red;
    public SearchState(PlayerStateManager playerStateMan,Material material,Rigidbody rb)
    {
        _rbd = rb;
        _material = material;
        _playerSM = playerStateMan;
    }
    public void Enter()
    {
        Debug.Log($"STATE CHANE - Search");
        _material.color = _stateColor;
    }

    public void Exit()
    {
       
    }

    public void FixedTick()
    {
        float distanceFromtarget = Vector3.Distance(_playerSM.TargetPosition, _rbd.position);
        if (distanceFromtarget &lt .8F)
        {
            _playerSM.ChangeState(_playerSM.FoundState);
        }
        else
        {
            RotateTowardsTarget();
            MoveTowerdsTarget();
        }
    }
    void RotateTowardsTarget()
    {
        Quaternion lookrotation = Quaternion.LookRotation(_playerSM.TargetPosition - _rbd.position);
        lookrotation = Quaternion.Slerp(_rbd.rotation, lookrotation, _playerSM.RotateSpeed * Time.deltaTime);
        _rbd.MoveRotation(lookrotation);
    }
    void MoveTowerdsTarget()
    {
//        Vector3 moveoffset = _playerSM.transform.forward * _playerSM.MoveSpeed;
        _rbd.MovePosition(Vector3.Lerp(_playerSM.transform.position, _playerSM.TargetPosition,_playerSM.MoveSpeed));
    }

    public void Tick()
    {
   
    }
}

=> Found State

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

public class FoundState : IState
{

    PlayerStateManager _playerStateManager;
    Color foundColor = Color.green;
    Material _material;
    TargetAssigner _targetassigned;
    public FoundState(PlayerStateManager playerStateManager, Material material, TargetAssigner targetassigned)
    {
        this._playerStateManager = playerStateManager;
        this._material = material;
        this._targetassigned = targetassigned;
    }
    public void Enter()
    {

        Debug.Log("Player is at found state");
        _material.color = foundColor;
        _targetassigned.MoveCommand += NewMoveCommand;
    }
    public void Exit()
    {
        _targetassigned.MoveCommand -= NewMoveCommand;
    }

    private void NewMoveCommand(Vector3 obj)
    {
        _playerStateManager.TargetPosition = obj;
        _playerStateManager.ChangeState(_playerStateManager.SearchState);
    }
    public void FixedTick()
    {
    }

    public void Tick()
    {
    
    }
}

No comments:

Post a Comment