State Pattern

State Pattern 이란?

유한한 상태를 관리할 때 사용하는 패턴입니다.
구현해야하는 상태가 많을 수록 불안정해지는 경우가 많습니다.

잘못된 플로우로 흘러가는 경우 원하는 상태로 도달하지 못하는 상황도 발생합니다.

또 상태를 구현하다보면 다양한 코드가 작성되어 어떤 상태인지 정확하게 파악하기 힘들어지기도 합니다.

State Pattern을 이용하여 상태를 캡슐화하고 클래스를 간소화하여 유지관리를 용이하게 할 수 있습니다.


State Pattern의 구성

State Pattern Diagram

  1. Context 클래스는 클라이언트가 객체의 내부 상태를 변경할 수 있도록 요청하는 인터페이스인 IState를 정의합니다.

  2. Context 클래스는 현재 상태에 대한 정보를 가집니다.

  3. IState인터페이스는 상태 클래스인 ConcreteState 클래스가 상속받아 구현합니다.

  4. 클라이언트는 객체의 상태를 업데이트할 때 Context 객체를 활용하여 원하는 상태로 설정할 것을 요청합니다.


State Pattern의 구현

State Pattern을 구현하기에 앞서 상태들에 대한 정의가 필요합니다.

저는 캐릭터의 상태로 정지, 걷기, 점프 3가지의 상태를 생각해두었습니다.

[IPlayerState 인터페이스] : IState

1
2
3
4
public interface IPlayerState
{
void Handle(PlayerController controller);
}

[PlayerController 클래스] : Context에 상태 전달

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class PlayerController : MonoBehaviour
{
public float maxSpeed = 2f;
public float jumpPower = 2f;

public float CurrentSpeed { get; set; }
public Vector3 CurrentDirection { get; set; }
public bool CurrentJump { get; set; }

//현재 상태 정보를 위해 각 상태 별로 생성
private IPlayerState _walkState, _stopState, _jumpState;
private PlayerStateContext _playerStateContext;

private void Start()
{
_playerStateContext = new PlayerStateContext(this);

//각 상태들을 정의
_walkState = gameObject.AddComponent<PlayerWalkState>();
_stopState = gameObject.AddComponent<PlayerStopState>();
_jumpState = gameObject.AddComponent<PlayerJumpState>();

_playerStateContext.Transition(_stopState);
}

#region 상태 전이 메서드
public void WalkPlayer(Vector3 direction)
{
CurrentDirection = direction;
_playerStateContext.Transition(_walkState);
}
public void StopPlayer()
{
_playerStateContext.Transition(_stopState);
}
public void JumpPlayer()
{
_playerStateContext.Transition(_jumpState);
}
#endregion
}

[PlayerStateContext 클래스] : Context

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class PlayerStateContext
{
public IPlayerState CurrentState
{
get; set;
}

private readonly PlayerController _playerController;

public PlayerStateContext(PlayerController playerController)
{
_playerController = playerController;
}
public void Transition()
{
CurrentState.Handle(_playerController);
}
public void Transition(IPlayerState state)
{
CurrentState = state;
CurrentState.Handle(_playerController);
}
}

[PlayerWalkState 클래스] : ConcreteState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class PlayerWalkState : MonoBehaviour, IPlayerState
{
private PlayerController _playerController;

public void Handle(PlayerController playerController)
{
if (!_playerController)
{
_playerController = playerController;
}
_playerController.CurrentSpeed = _playerController.maxSpeed;
}

void Update()
{
if (_playerController)
{
if (_playerController.CurrentSpeed > 0)
{
_playerController.transform.Translate(
Time.deltaTime * _playerController.CurrentSpeed * _playerController.CurrentDirection);
}
}
}
}

[ClientState 클래스]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
using UnityEngine;

public class ClientState : MonoBehaviour
{
private PlayerController _playerController;
private float _inputHorizontal;
private float _inputVertical;
private Vector3 _direction;

private void Start()
{
_playerController = GetComponent<PlayerController>();
}

private void Update()
{
_inputHorizontal = Input.GetAxis("Horizontal");
_inputVertical = Input.GetAxis("Vertical");

_direction = new Vector3(_inputHorizontal, 0, _inputVertical);

//조건에 따라 상태 전이 메서드 호출
if(_direction == Vector3.zero){
_playerController.StopPlayer();
}
if(_direction != Vector3.zero) {
_playerController.WalkPlayer(_direction);
}
if(Input.GetKeyDown(KeyCode.Space)) {
_playerController.JumpPlayer();
}
}
}

WalkState를 제외하고 Stop, Jump 도 작성을 해두었지만 WalkState 클래스만 예시로 보여드렸습니다.

IPlayerState 인터페이스를 생성하고

WalkState는 IPlayerState를 상속받아 Handle메서드를 구현해두었습니다.

PlayerController는 클라이언트가 요청한 상태 값을 PlayerStateContext에게 전달,
PlayerStateContext는 현재 상태 정보를 보관하도록 작성해두었습니다.

이렇게 작성해두니 상태별로 관리하기가 수월해진 것이 느껴집니다.

상태를 수정하려거든 각 상태 클래스 또는 상태를 호출하는 ClientState 클래스의 상태 호출 조건을 수정하면 됩니다.

상태를 추가하려거든 특정 상태 클래스를 추가, PlayerController클래스에서 전이 메서드 추가,
전이 메서드 호출하는 코드 추가를 하면 됩니다.

이렇게 정리해보니 새로운 상태를 추가하거나, 기존 상태에 대한 변경점이 발생한다면 어느 부분을 추가/수정해야하는지 명확해졌습니다.


State Pattern의 장단점

장점

  1. 상태별로 클래스 관리가 가능합니다.
  2. 유지관리가 수월해집니다.

단점

  1. 추가할 때마다 상태 클래스를 추가해야하므로 관리해야하는 클래스의 수가 증가합니다.
  2. 상태 전환이 빈번하게 발생한다면 확인 조건이 늘어나므로 코드가 방대해지고 수행 시간이 길어지게 됩니다.

유니티를 이용하여 상태를 구현하다보면 상태 변경으로 인해 코드가 늘어나고 관리하기 힘들어질 때가 있습니다.

유니티 내에서는 애니메이터 상태에 추상 클래스인 StateMachineBehaviour 클래스를 상속받은 상태 정의 클래스를 컴포넌트로 추가하여 활용할 수 있습니다.


[참조]