카테고리 없음

디자인 패턴) 상태패턴 State Pattern _ Unity C#

ChoiSW 2024. 9. 1. 22:50

상태패턴의 구조

  • Context 클래스
    Context 객체가 현재 상태를 유지하고, 상태에 따라 행동을 다르게 처리하는 역할을 합니다. 클라이언트가 객체의 내부 상태를 변경할 수 있도록 요청하는 인터페이스(IState)를 정의합니다. 또한 현재 상태에 대한 포인터(currentState : IState)를 보유합니다. Context의 내부 상태가 바뀜에 따라, 행동을 바꾸도록 할 수 있습니다.
  • IState 인터페이스
    구체적인(concrete) 상태클래스로 연결할 수 있도록 설정합니다.
  • ConcreteState 클래스
    IState 인터페이스를 구현하고, Context 오브젝트가 상태의 동작을 트리거하기 위해 호출하는 public method인 handle()을 노출합니다.

클라이언트는 Context 객체를 활용해, 원하는 상태로 설정하거나, 새로운 상태로 전환할 것을 요청합니다. 그리고 이 때, 구체적인 상태 클래스들을 알 필요는 없습니다. 또한, 코드 수정 없이 Context 클래스에 추가적인 상태 클래스들을 추가할 수 있습니다.


이해하기

상태를 별도의 클래스로 캡슐화한 후, 현재 상태를 나타내는 객체에게 행동을 위임합니다.

엔티티의 개별 상태와 상태 동작을 정의할 수 있는 패턴입니다.

동적으로 행동을 교체할 수 있습니다.

캐릭터의 유한 상태(finite state)를 관리하고자 할 때, 상태 패턴을 사용합니다.


장점

  • 캡슐화 : 개체의 상태별 행동을 구현할 수 있다. 상태가 변할 때, 개체에 동적으로 할당할 수 있다.
  • 유지 및 관리 : 긴 조건문이나 코드 수정 없이도, 쉽게 새로운 상태를 구현할 수 있다.

제한사항(한계)

  • 블렌딩 : 기본 형태에서 상태패턴은 애니메이션 블렌드를 제공하지 않는다.
  • 전환 : 상태 간 관계를 정의하지는 않았다. 관계와 조건에 따라 상태 간의 전환을 정의하고자 하면 더 많은 코드를 작성해야한다. ex) 대기상태 → 걷기상태로의 전환

위의 2가지 한계는 유니티의 애니메이션 시스템과 기본 상태 기계로 극복할 수 있다.


 

사용하는 경우

주로 객체나 시스템이 여러 가지 상태를 가질 수 있으며, 상태에 따라 다른 행동을 해야 할 때입니다. 

상태 전환이 명확하고 구조적으로 관리될 수 있어 코드의 복잡성을 줄이고 유지보수성을 높일 수 있습니다.

 

 

캐릭터 상태 관리

플레이어 또는 NPC 캐릭터가 여러 가지 상태(예: 걷기, 달리기, 점프, 공격, 피격, 사망 등)를 가질 수 있을 때 사용됩니다.

 

AI 행동 관리

적 AI가 다른 상태에 따라 행동을 다르게 해야 하는 경우에도 상태 패턴을 사용합니다. 예를 들어, 적 AI가 순찰(Patrol), 추적(Chase), 공격(Attack), 도주(Flee) 등 다양한 상태를 가질 수 있습니다.


 

구현하기

 

IBikeState

    public interface IBikeState
    {
        void Handle(BikeController bikeController);
    }

 

BikeStateContext

public class BikeStateContext
{
    public IBikeState CurrentState
    {
        get; set;
    }

    private readonly BikeController _bikeController; 

    public BikeStateContext(BikeController bikeController)
    {
        _bikeController = bikeController;
    }

    public void Transition()
    {
        CurrentState.Handle(_bikeController);
    }

    public void Transition(IBikeState state)
    {
        CurrentState = state;
        CurrentState.Handle(_bikeController);
    }
}

적용하기 (ConcreteState)

BikeStartState

public class BikeStartState : MonoBehaviour, IBikeState
{
    BikeController _bikeController;
    public void Handle(BikeController bikeController)
    {
        if(!_bikeController)
            _bikeController = bikeController;
        _bikeController.CurrentSpeed = _bikeController.maxSpeed;
    }

    void Update()
    {
        if (_bikeController)
        {
            if(_bikeController.CurrentSpeed > 0)
            {
                _bikeController.transform.Translate(
                    Vector3.forward *(_bikeController.CurrentSpeed * Time.deltaTime)
                );
            }
        }
    }
}

 

BikeStopState

public class BikeStopState : MonoBehaviour, IBikeState
{
    BikeController _bikeController;
    public void Handle(BikeController bikeController)
    {
        if (!_bikeController)
            _bikeController = bikeController;
        _bikeController.CurrentSpeed = 0;
    }
}

 

BiketurnState

public class BiketurnState : MonoBehaviour, IBikeState
{
    Vector3 _turnDirection;
    BikeController _bikeController;

    public void Handle(BikeController bikeController)
    {
        if(!_bikeController)
            _bikeController = bikeController;

        _turnDirection.x = (float)_bikeController.CurrentTurnDirection;

        if(_bikeController.CurrentSpeed > 0)
            transform.Translate(_turnDirection * _bikeController.turnDistance);
    }
}

 

BikeController

public enum Direction
{
    Left = -1,
    Right = 1
}

public class BikeController : MonoBehaviour
{
    public float maxSpeed = 2.0f;
    public float turnDistance = 2.0f;
    public float CurrentSpeed { get; set; }
    public Direction CurrentTurnDirection
    {
        get; private set;
    }

    private IBikeState _startState, _stopState, _turnState;

    private BikeStateContext _bikestateContext;

    private void Start()
    {
        _bikestateContext = new BikeStateContext(this);
        _startState = gameObject.AddComponent<BikeStartState>();
        _stopState = gameObject.AddComponent<BikeStopState>();
        _turnState = gameObject.AddComponent<BiketurnState>();

        _bikestateContext.Transition(_stopState);
    }

    public void StartBike()
    {
        _bikestateContext.Transition(_startState);
    }

    public void StopBike()
    {
        _bikestateContext.Transition(_stopState);
    }

    public void TurnBike(Direction direction)
    {
        CurrentTurnDirection = direction;
        _bikestateContext.Transition(_turnState);
    }
}

Client

public class Client : MonoBehaviour
{
    private BikeController _bikeController;

    private void Start()
    {
        _bikeController = FindObjectOfType<BikeController>();
    }

    private void OnGUI()
    {
        if (GUILayout.Button("Start"))
            _bikeController.StartBike();
        if (GUILayout.Button("Turn Left"))
            _bikeController.TurnBike(Direction.Left);
        if (GUILayout.Button("Turn Right"))
            _bikeController.TurnBike(Direction.Right);
        if (GUILayout.Button("Stop"))
            _bikeController.StopBike();
    }
}


 

추가내용

상태패턴의 대안

  • 블랙보드/행동 트리
    NPC 캐릭터의 복잡한 AI 동작을 구현하려고 한다면, 블랙보드 같은 패턴 혹은 행동 트리(behaviour tree)를 고려하자
  • 유한 상태 기계
    유한 상태 기계는 특정 입력 트리거를 기반으로 하는, 유한 상태 간 전환에 더 깊이 관여한다. 자동 기계 같은 시스템을 구현하는데 더 적합하다.
  • 메멘토
    상태 패턴과 비슷하지만, 개체가 이전 상태로 돌아갈 수 있는 기능을 제공한다. 자체적으로 변경된 것으로 되돌리는 기능이 필요한 시스템에 유용하다.

소스코드 링크 : https://github.com/PacktPublishing/Game-Development-Patterns-with-Unity-2021-Second-Edition/tree/main