디자인 패턴) 상태패턴 State Pattern _ Unity C#
상태패턴의 구조
- 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)를 고려하자 - 유한 상태 기계
유한 상태 기계는 특정 입력 트리거를 기반으로 하는, 유한 상태 간 전환에 더 깊이 관여한다. 자동 기계 같은 시스템을 구현하는데 더 적합하다. - 메멘토
상태 패턴과 비슷하지만, 개체가 이전 상태로 돌아갈 수 있는 기능을 제공한다. 자체적으로 변경된 것으로 되돌리는 기능이 필요한 시스템에 유용하다.