개인 공부/디자인패턴

유니티 FSM

smallship 2024. 7. 17. 14:38

FSM( Finite State Machine )이란 한정된 상태들과 그 상태 간의 전이를 정의하여 게임 오브젝트의 동작을 제어하는 패턴이다. 게임에서 오브젝트가 여러 상태를 가지고 있고, 각 상태에서는 특정 동작을 수행하거나 다른 상태로 전환할 수 있는 구조이다. 유니티에서는 Animator Controller를 생각하면 이해하기 쉽다. 한 State에 머무르며 행동을 하다 특정 조건을 만족하면 다른 State로 Transition을 하는 시스템이다.

 

플레이어가 있다고 가정을 해보자. 플레이어의 상태를 3가지로 구분할 수 있다.

  1. Idle : 아무것도 하지 않는 상태
  2. Move : 움직이는 상태
  3. Stun : 경직에 걸려 움직이지 못하는 상태

 

각 상태와 전이 조건을 정리한 그림

 

유니티에서 간단하게 FSM을 구현해보자.

 

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 OwnerStateMachine;
    
    // HSFM 이용 할 시
    public  StateMachine HSFM_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()
    {
        // 1번 이거는 성능이 직접 컴포넌트 가져오는 방식 대비 비싸다.
        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);
            }
        }    
        
        // 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();
        }
    }
}

StateMachine 스크립트이다.

 

using UnityEngine;

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

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

    public override void ExitState()
    {
    }
}
using UnityEngine;

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

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

    public override void ExitState()
    {
    }
}
using System.Collections;
using UnityEngine;

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

    public override void ExcuteState()
    {
    }

    public override void ExitState()
    {
    }
}

순서대로 Idle,Move,Stun 상태 스크립트이다.

 

큐브를 생성하고 스크립트들을 적용시켜준다.

 

Idle 상태에서 W키를 누르면 Move상태로 전이되어 움직이고 F키를 누르면 Stun상태로 전이 되는것을 확인할 수 있다.

 


 

FSM은 게임 개발에서 NPC나 AI를 구현하는 데 유용한 패턴이다. 구조가 단순하고 직관적이어서 초기 단계에서는 유용하지만 상태의 수가 많아지고 행동 패턴이 복잡해질수록 관리와 유지보수가 어려워질 수 있다. 따라서 사용 전에 프로젝트의 요구사항과 AI의 복잡성을 신중하게 분석하고 사용해야 한다.