본문 바로가기
STUDY/디자인패턴

FSM(Finite-State-Machine) ★★★

by 램플릿 2024. 7. 17.

현업에서도 많이 쓰이는 중요한  패턴

 유한상태기계

 

상태를 명확하게 나눔으로써 내 행동을 클래스단위로 나눌 수 있고 이를 통해 버그를 방지할 수 있다.

상태를 체크할 때 bool을 사용하지 않아도 된다.

 

데이터를 제어하려면 스크립터블 오브젝트를 사용하거나 MonoBehaviour를 상속받아서 이를 컴포넌트화 시켜야한다.

(MonoBehaviour는 new로 생성하지 않고 Awake나 Start에서 초기화함

 

주체가 되는 State Machine

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public enum EMyState
{
    IdleMyState,
    MoveMyState,
    StunMyState
}

public interface IMyState
{
    void EnterState();
    void ExcuteState();
    void ExitState();
}

public class IdleMyState : IMyState
{
    public void EnterState()
    {
    }

    public void ExcuteState()
    {
    }

    public void ExitState()
    {
    }
}

public class MoveMyState : IMyState
{
    public void EnterState()
    {
    }

    public void ExcuteState()
    {
    }

    public void ExitState()
    {
    }
}

public class StunMyState : IMyState
{
    public void EnterState()
    {
    }

    public void ExcuteState()
    {
    }

    public void ExitState()
    {
    }
}



public class StateMachine : MonoBehaviour
{
    [SerializeField] private EMyState defaultState;
    
    private IMyState _currentMyState;
    private Dictionary<EMyState, IMyState> _states = new();

    private void ChangeState_Internal(IMyState newMyState)
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExitState();
        }

        _currentMyState = newMyState;
        _currentMyState.EnterState();
    }

    public void ChangeState(EMyState state)
    {
        ChangeState_Internal(_states[state]);
    }
    
    // Start is called before the first frame update
    void Start()
    {
        _states.Add(EMyState.IdleMyState, new IdleMyState());
        _states.Add(EMyState.MoveMyState, new MoveMyState());
        _states.Add(EMyState.StunMyState, new StunMyState());

        // DefaultState
        ChangeState(EMyState.IdleMyState);
    }

    // Update is called once per frame
    void Update()
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExcuteState();
        }
    }
}

 

구현 FSM 기초코드

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public enum EMyState
{
    IdleMyState,
    MoveMyState,
    StunMyState
}

public interface IMyState
{
    void EnterState();
    void ExcuteState();
    void ExitState();
}

public abstract class VMyState : IMyState
{
    public StateMachine StateMachine;
    public abstract void  EnterState();

    public abstract void ExcuteState();

    public abstract void ExitState();
}

public class IdleMyState : VMyState
{
    public override void EnterState()
    {
    }

    public override void ExcuteState()
    {
        if (Input.GetKey(KeyCode.W))
        {
            StateMachine.ChangeState(EMyState.MoveMyState);
        }
        else if (Input.GetKey(KeyCode.F))
        {
            StateMachine.ChangeState(EMyState.StunMyState);
        }
    }

    public override void ExitState()
    {
    }
}

public class MoveMyState : VMyState
{
    public override void EnterState()
    {
    }

    public override void ExcuteState()
    {
        if (Input.GetKey(KeyCode.W))
        {
            StateMachine.transform.position += StateMachine.transform.forward * (Time.deltaTime * 10);
        }
        else if (Input.GetKey(KeyCode.S))
        {
            StateMachine.transform.position -= StateMachine.transform.forward * (Time.deltaTime * 10);
        }
        else if (Input.GetKey(KeyCode.F))
        {
            StateMachine.ChangeState(EMyState.StunMyState);
        }
        else
        {
            StateMachine.ChangeState(EMyState.IdleMyState);
        }
    }

    public override void ExitState()
    {
    }
}

public class StunMyState : VMyState
{
    IEnumerator Stun()
    {
        yield return new WaitForSeconds(3.0f);
        StateMachine.ChangeState(EMyState.IdleMyState);
    }
    
    public override void EnterState()
    {
        StateMachine.StartCoroutine(Stun());
    }

    public override void ExcuteState()
    {
    }

    public override void ExitState()
    {
    }
}

public class StateMachine : MonoBehaviour
{
    [SerializeField] private EMyState defaultState;
    
    private IMyState _currentMyState;
    private Dictionary<EMyState, IMyState> _states = new();

    private void ChangeState_Internal(IMyState newMyState)
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExitState();
        }

        _currentMyState = newMyState;
        _currentMyState.EnterState();
    }

    public void ChangeState(EMyState state)
    {
        ChangeState_Internal(_states[state]);
    }
    
    // Start is called before the first frame update
    void Start()
    {
        _states.Add(EMyState.IdleMyState, new IdleMyState() {StateMachine = this});
        _states.Add(EMyState.MoveMyState, new MoveMyState() {StateMachine = this});
        _states.Add(EMyState.StunMyState, new StunMyState() {StateMachine = this});

        // DefaultState
        ChangeState(EMyState.IdleMyState);
    }

    // Update is called once per frame
    void Update()
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExcuteState();
        }
    }
}

 

 

유니티식 FSM

StateMachine.cs

using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public enum EMyState
{
    IdleMyState,
    MoveMyState,
    StunMyState
}

public interface IMyState
{
    void EnterState();
    void ExcuteState();
    void ExitState();
}

public abstract class VMyState : MonoBehaviour, IMyState
{
    public StateMachine StateMachine;
    public abstract void  EnterState();

    public abstract void ExcuteState();

    public abstract void ExitState();
}

public class StateMachine : MonoBehaviour
{
    [SerializeField] private EMyState defaultState;
    
    private IMyState _currentMyState;
    private Dictionary<EMyState, IMyState> _states = new();

    private void ChangeState_Internal(IMyState newMyState)
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExitState();
        }

        _currentMyState = newMyState;
        _currentMyState.EnterState();
    }

    public void ChangeState(EMyState state)
    {
        ChangeState_Internal(_states[state]);
    }
    
    // Start is called before the first frame update
    void Start()
    {
        _states.Add(EMyState.IdleMyState, new IdleMyState() {StateMachine = this});
        _states.Add(EMyState.MoveMyState, new MoveMyState() {StateMachine = this});
        _states.Add(EMyState.StunMyState, new StunMyState() {StateMachine = this});

        // DefaultState
        ChangeState(EMyState.IdleMyState);
    }

    // Update is called once per frame
    void Update()
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExcuteState();
        }
    }
}

 

IdleMyState.cs

using UnityEngine;

public class IdleMyState : VMyState
{
    public override void EnterState()
    {
    }

    public override void ExcuteState()
    {
        if (Input.GetKey(KeyCode.W))
        {
            StateMachine.ChangeState(EMyState.MoveMyState);
        }
        else if (Input.GetKey(KeyCode.F))
        {
            StateMachine.ChangeState(EMyState.StunMyState);
        }
    }

    public override void ExitState()
    {
    }
}

 

MoveMyState.cs

using UnityEngine;

public class MoveMyState : VMyState
{
    public override void EnterState()
    {
    }

    public override void ExcuteState()
    {
        if (Input.GetKey(KeyCode.W))
        {
            StateMachine.transform.position += StateMachine.transform.forward * (Time.deltaTime * 10);
        }
        else if (Input.GetKey(KeyCode.S))
        {
            StateMachine.transform.position -= StateMachine.transform.forward * (Time.deltaTime * 10);
        }
        else if (Input.GetKey(KeyCode.F))
        {
            StateMachine.ChangeState(EMyState.StunMyState);
        }
        else
        {
            StateMachine.ChangeState(EMyState.IdleMyState);
        }
    }

    public override void ExitState()
    {
    }
}

 

 

StunMyState.cs

using System.Collections;
using UnityEngine;

public class StunMyState : VMyState
{
    IEnumerator Stun()
    {
        yield return new WaitForSeconds(3.0f);
        StateMachine.ChangeState(EMyState.IdleMyState);
    }
    
    public override void EnterState()
    {
        StateMachine.StartCoroutine(Stun());
    }

    public override void ExcuteState()
    {
    }

    public override void ExitState()
    {
    }
}

 

 

유니티에서 스테이트를 컴포넌트 화 시킨다음에 자동 등록 시키는 방법 코드

using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public enum EMyState
{
    IdleMyState,
    MoveMyState,
    StunMyState
}

public interface IMyState   
{
    void EnterState();
    void ExcuteState();
    void ExitState();
}

public abstract class VMyState : MonoBehaviour, IMyState
{
    [NonSerialized]public StateMachine StateMachine;
    public abstract void  EnterState();

    public abstract void ExcuteState();

    public abstract void ExitState();
}

public class StateMachine : MonoBehaviour
{
    [SerializeField] private EMyState defaultState;
    
    private IMyState _currentMyState;
    private Dictionary<EMyState, IMyState> _states = new();

    private void ChangeState_Internal(IMyState newMyState)
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExitState();
        }

        _currentMyState = newMyState;
        _currentMyState.EnterState();
    }

    public void ChangeState(EMyState state)
    {
        ChangeState_Internal(_states[state]);
    }
    
    void Start()
    {
        // 1번 이거는 성능이 직접 컴포넌트 가져오는 방식 대비 비싸다.
        VMyState[] stateArray = GetComponents<VMyState>();
        foreach (var state in stateArray)
        {
            state.StateMachine = this;
            EMyState outEnum;
            if (EMyState.TryParse(state.GetType().ToString(), out outEnum))
            {
                _states.Add(outEnum, state);
            }
        }    
        
        // 2번 아래의 방식이 좀 더 비용적으로 저렴하다.
        _states.Add(EMyState.IdleMyState, GetComponent<IdleMyState>());
        _states.Add(EMyState.MoveMyState, GetComponent<MoveMyState>());
        _states.Add(EMyState.StunMyState, GetComponent<StunMyState>());
        
        // DefaultState
        ChangeState(EMyState.IdleMyState);
    }

    // Update is called once per frame
    void Update()
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExcuteState();
        }
    }
}

 

 

컴포넌트 방식으로 바뀐 뒤에는 데이터를 가공하기가 편해진다.

 

 


상태 안에 상태가 존재하는 경우 HFSM(Hierarchy FSM)을 사용 수 있다.

public abstract class VMyState : MonoBehaviour, IMyState
{
    [NonSerialized]public StateMachine OwnerStateMachine;
    
    // HSFM 이용 할 시
    public  StateMachine HSFM_StateMachine;
    
    public abstract void  EnterState();

    public abstract void ExcuteState();

    public abstract void ExitState();
}

 

이렇게 해서 하위 객체에 StateMachine을 달고 State를 달면

무한 HFSM 된다.

 

 

 

 

HFSM 상위상태 변경 시 하위상태 정리 안되는문제 해결 버전 코드

GetSuperOwnerStateMachine

using System;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;

public enum EMyState
{
    NoneMyState,
    IdleMyState,
    MoveMyState,
    StunMyState,
}

public abstract class VMyState : MonoBehaviour
{
    [NonSerialized]public StateMachine OwnerStateMachine;

    // HSFM 이용 할 시
    public  StateMachine HSFM_StateMachine;

    public void EnterStateWrapper()
    {
        EnterState();
    }

    public void ExcuteStateWrapper()
    {
        ExcuteState();
    }

    public void ExitStateWrapper()
    {
        ExitState();
        if (HSFM_StateMachine)
        {
            HSFM_StateMachine.ChangeState(EMyState.NoneMyState);
        }
    }
    
    public abstract void  EnterState();

    public abstract void ExcuteState();

    public abstract void ExitState();
}

public class StateMachine : MonoBehaviour
{
    [SerializeField] private EMyState defaultState;
    
    private VMyState _currentMyState;
    private Dictionary<EMyState, VMyState> _states = new();
    
    StateMachine GetSuperOwnerStateMachile()
    {
        StateMachine stateMachine = GetComponentInParent<StateMachine>();
        if (stateMachine)
        {
            return stateMachine.GetSuperOwnerStateMachile();
        }

        return this;
    }
    
    private void ChangeState_Internal(VMyState newMyState)
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExitStateWrapper();
        }

        if (newMyState == null)
        {
            _currentMyState = null;
            return;
        }

        _currentMyState = newMyState;
        _currentMyState.EnterStateWrapper();
    }

    public void ChangeState(EMyState state)
    {
        if (state == EMyState.NoneMyState)
        {
            ChangeState_Internal(null);
            return;
        }
        
        ChangeState_Internal(_states[state]);
    }
    
    // Start is called before the first frame update
    void Start()
    {
        // 이거는 성능이 직접 컴포넌트 가져오는 방식 대비 비싸다.
        VMyState[] stateArray = GetComponents<VMyState>();
        foreach (var state in stateArray)
        {
            state.OwnerStateMachine = this;
            EMyState outEnum;
            if (EMyState.TryParse(state.GetType().ToString(), out outEnum))
            {
                _states.Add(outEnum, state);
            }
        }   
        
        // DefaultState
        ChangeState(defaultState);
    }

    // Update is called once per frame
    void Update()
    {
        if (_currentMyState != null)
        {
            _currentMyState.ExcuteStateWrapper();
        }
    }
}

'STUDY > 디자인패턴' 카테고리의 다른 글

Strategy 전략 패턴  (0) 2024.07.19
Observer 옵저버 패턴 ★★★  (0) 2024.07.19
Command 커맨드 패턴 ★★★  (0) 2024.07.16
chain of responsibility 책임연쇄 패턴  (0) 2024.07.16
Proxy 프록시 패턴  (0) 2024.07.15