본문 바로가기
유니티/멋쟁이사자처럼

[멋쟁이 사자처럼 부트 캠프 TIL 회고] Unity 게임 개발 3기 - FSM 상태 패턴

by 몰캉이이 2025. 1. 2.

 

 

FSM - 상태 패턴

 

목차

     

    1. 상태 패턴 이란?

    • 객체의 상태에 따라 객체의 행동이 변하는 패턴
    • 객체의 상태를 클래스로 캡슐화하고, 상태 전환을 객체 내에서 관리 할 수 있게 만듬

    2. 핵심 개념

    1. 상태 캡슐화
      • 상태를 독립적인 클래스로 정의
      • 각 상태는 특정 행동을 구현
    2. 상태 전환
      • 현재 상태에 따라 동작을 결정
    3. 유연성
      • 상태별 행동은 독립적으로 존재

    3. 장.단점

    • 장점
      • 조건문을 줄이고 가독성을 높임
      • 새로운 상태 추가가 쉬움
      • 유지 보수가 용이
    • 단점
      • 상태가 많으면 복잡해 짐

     

    4. 상태 패턴 구현 방법들

    1. enum

    enum은 가장 단순하고 직관적인 방법입니다. 상태를 열거형으로 정의하고, 이를 기반으로 분기처리합니다.

    • 장점:
      • 구현이 간단하고 코드가 직관적임.
      • 상태 값이 제한적일 경우 적합.
    • 단점:
      • 상태별로 동작을 분리하기 어려움. 분기문(if, switch)의 남용으로 코드가 비대해질 수 있음.
      • 상태와 행동이 서로 강하게 결합됨.

    2. 인터페이스

    상태를 독립된 인터페이스(IState)로 정의하고, 각 상태별로 인터페이스를 구현한 클래스를 만듭니다.

    • 장점:
      • 상태와 행동을 분리하여 유연하게 관리 가능.
      • 상태 추가/확장이 용이.
      • 객체지향적인 설계.
    • 단점:
      • 상태별로 클래스를 많이 만들어야 할 수 있음.
      • 복잡성이 증가할 가능성이 있음.

    3. 클래스

    상태를 독립적인 클래스로 분리하고, 이를 조합해 객체의 상태를 전환합니다. 여기에는 상태 전환 로직을 컨텍스트(Context)에 넣을 수도 있고, 상태 자체에서 관리할 수도 있습니다.

    • 장점:
      • 상태별 행동을 캡슐화하여 응집도가 높아짐.
      • 상태 전환 로직을 관리하기 쉽고 확장성이 높음.
    • 단점:
      • 상태를 구현하는 클래스가 많아질 가능성이 있음.
      • 설계와 구현이 다소 복잡할 수 있음.

    4. 함수

    상태를 함수(또는 델리게이트, 람다)로 정의하여 동작을 할당합니다. 유니티에서는 상태를 업데이트하는 방식으로 델리게이트를 사용하는 경우가 많습니다.

    • 장점:
      • 상태 전환과 행동 구현이 간단.
      • 상태별로 별도의 클래스를 정의하지 않아도 됨.
    • 단점:
      • 상태가 많아질수록 코드가 관리하기 어려워질 수 있음.
      • 상태 전환이 명시적으로 보이지 않을 수 있음.

    5. ScriptableObject

    유니티에서 ScriptableObject를 활용하면 상태를 데이터 자산으로 정의할 수 있습니다. 이는 상태와 데이터를 함께 캡슐화하는 방식입니다.

    • 장점:
      • 상태를 데이터와 함께 재사용 가능.
      • 상태를 에디터에서 시각적으로 관리할 수 있음.
      • 유니티에서 자주 사용되는 방식.
    • 단점:
      • 설계가 까다로울 수 있음.
      • 상태와 데이터의 관계를 잘 정의해야 함.

    6. 플래그

    비트 연산자를 이용해 상태를 비트로 표현합니다. 여러 상태를 동시에 표현하거나 체크해야 할 때 유용합니다.

    • 장점:
      • 메모리와 성능에 최적화.
      • 상태를 조합하거나 중첩된 상태를 관리하기 쉬움.
    • 단점:
      • 상태에 대한 직관적인 이해가 어렵고, 구현 난이도가 높을 수 있음.
      • 복잡한 상태 트랜지션에는 적합하지 않음.

    7. FSM

    유니티나 C#에는 FSM 라이브러리나 상태 전환을 쉽게 관리할 수 있는 툴들이 있습니다. 대표적으로 UniRx, Stateless 등이 있습니다.

    • 장점:
      • 복잡한 상태 전환 로직을 손쉽게 관리 가능.
      • 재사용 가능하고 테스트하기 쉬움.
    • 단점:
      • 라이브러리를 이해하고 학습하는 시간이 필요함.

    8. 플로우 차트

    유니티에서는 Animator의 상태 머신을 활용하거나, Playmaker와 같은 시각적 스크립팅 툴을 사용해 상태를 관리할 수도 있습니다.

    • 장점:
      • 상태 전환이 직관적이고 시각적으로 표현됨.
      • 디자이너와 협업에 적합.
    • 단점:
      • 복잡한 상태일수록 관리가 어려워질 수 있음.
      • 추가적인 플러그인이나 툴 사용이 필요.

    9. Dictionary

    상태와 행동을 Dictionary(또는 해시맵)로 매핑하는 방식입니다. 상태 값(key)과 해당 행동(value)을 매핑해 동작을 결정합니다.

    • 장점:
      • 상태와 행동을 데이터적으로 관리 가능.
      • 상태 추가가 간단.
    • 단점:
      • 복잡한 상태 전환 로직은 별도로 구현해야 함.
      • 상태가 많아질 경우 관리가 어려울 수 있음.

    5. 코드

    1. enum

    더보기
    using UnityEngine;
    
    public class Player : MonoBehaviour
    {
        // 1. Enum 정의
        public enum PlayerState
        {
            Idle,
            Running,
            Jumping,
            Attacking
        }
    
        // 2. 현재 상태를 저장할 변수
        private PlayerState currentState;
    
        // 3. Unity의 Start 메서드에서 초기 상태를 설정
        private void Start()
        {
            SetState(PlayerState.Idle);
        }
    
        // 4. 상태에 따른 업데이트 처리
        private void Update()
        {
            switch (currentState)
            {
                case PlayerState.Idle:
                    HandleIdleState();
                    break;
    
                case PlayerState.Running:
                    HandleRunningState();
                    break;
    
                case PlayerState.Jumping:
                    HandleJumpingState();
                    break;
    
                case PlayerState.Attacking:
                    HandleAttackingState();
                    break;
            }
    
            // 예: 키 입력에 따라 상태 전환
            if (Input.GetKeyDown(KeyCode.Space))
            {
                SetState(PlayerState.Jumping);
            }
            else if (Input.GetKey(KeyCode.W))
            {
                SetState(PlayerState.Running);
            }
            else if (Input.GetMouseButtonDown(0))
            {
                SetState(PlayerState.Attacking);
            }
            else if (!Input.anyKey)
            {
                SetState(PlayerState.Idle);
            }
        }
    
        // 5. 상태 전환 메서드
        private void SetState(PlayerState newState)
        {
            // 상태가 변할 때만 변경
            if (currentState == newState) return;
    
            currentState = newState;
            Debug.Log($"State changed to: {currentState}");
        }
    
        // 6. 상태별 행동 정의
        private void HandleIdleState()
        {
            // Idle 상태에서 해야 할 행동
        }
    
        private void HandleRunningState()
        {
            // Running 상태에서 해야 할 행동
        }
    
        private void HandleJumpingState()
        {
            // Jumping 상태에서 해야 할 행동
        }
    
        private void HandleAttackingState()
        {
            // Attacking 상태에서 해야 할 행동
        }
    }

    2. 인터페이스

    더보기
    using UnityEngine;
    
    // 1. 상태를 정의하는 인터페이스
    public interface IPlayerState
    {
        void EnterState(Player player);   // 상태 진입 시 호출
        void UpdateState(Player player); // 매 프레임 호출
        void ExitState(Player player);   // 상태 종료 시 호출
    }
    
    // 2. Player 클래스 (컨텍스트)
    public class Player : MonoBehaviour
    {
        private IPlayerState currentState; // 현재 상태
    
        private void Start()
        {
            // 초기 상태 설정
            SetState(new IdleState());
        }
    
        private void Update()
        {
            // 현재 상태의 UpdateState 호출
            currentState?.UpdateState(this);
        }
    
        // 상태 전환 메서드
        public void SetState(IPlayerState newState)
        {
            // 기존 상태의 ExitState 호출
            currentState?.ExitState(this);
    
            // 새 상태로 변경
            currentState = newState;
    
            // 새 상태의 EnterState 호출
            currentState?.EnterState(this);
        }
    
        // 예시로 사용할 행동 메서드
        public void Move()
        {
            Debug.Log("Player is moving...");
        }
    
        public void Jump()
        {
            Debug.Log("Player is jumping...");
        }
    
        public void Attack()
        {
            Debug.Log("Player is attacking...");
        }
    }
    
    // 3. Idle 상태
    public class IdleState : IPlayerState
    {
        public void EnterState(Player player)
        {
            Debug.Log("Entering Idle State");
        }
    
        public void UpdateState(Player player)
        {
            if (Input.GetKey(KeyCode.W))
            {
                player.SetState(new RunningState());
            }
            else if (Input.GetKeyDown(KeyCode.Space))
            {
                player.SetState(new JumpingState());
            }
            else if (Input.GetMouseButtonDown(0))
            {
                player.SetState(new AttackingState());
            }
        }
    
        public void ExitState(Player player)
        {
            Debug.Log("Exiting Idle State");
        }
    }
    
    // 4. Running 상태
    public class RunningState : IPlayerState
    {
        public void EnterState(Player player)
        {
            Debug.Log("Entering Running State");
            player.Move();
        }
    
        public void UpdateState(Player player)
        {
            if (!Input.GetKey(KeyCode.W))
            {
                player.SetState(new IdleState());
            }
        }
    
        public void ExitState(Player player)
        {
            Debug.Log("Exiting Running State");
        }
    }
    
    // 5. Jumping 상태
    public class JumpingState : IPlayerState
    {
        public void EnterState(Player player)
        {
            Debug.Log("Entering Jumping State");
            player.Jump();
        }
    
        public void UpdateState(Player player)
        {
            // 점프 중인 상태에서 특정 조건(착지 등)이 만족되면 Idle 상태로 전환
            if (Input.GetKeyDown(KeyCode.W))
            {
                player.SetState(new RunningState());
            }
        }
    
        public void ExitState(Player player)
        {
            Debug.Log("Exiting Jumping State");
        }
    }
    
    // 6. Attacking 상태
    public class AttackingState : IPlayerState
    {
        public void EnterState(Player player)
        {
            Debug.Log("Entering Attacking State");
            player.Attack();
        }
    
        public void UpdateState(Player player)
        {
            // 공격이 끝나면 Idle 상태로 복귀
            if (!Input.GetMouseButton(0))
            {
                player.SetState(new IdleState());
            }
        }
    
        public void ExitState(Player player)
        {
            Debug.Log("Exiting Attacking State");
        }
    }