게임 프로그래밍 패턴 7장
상태
상태 패턴은 객체의 상태에 따라 행동이 달라지는 상황에서, 상태를 객체화하여 스스로 행동할 수 있도록 하는 패턴이다. 유한상태기계(FSM : Finite State Machine)로 구현한다.
객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보입니다. (GoF의 디자인 패턴 395쪽)
플랫포머 게임에서 플레이어를 구현한다고 해보자. B 버튼을 누르면 플레이어는 점프한다.
# Player.cs
public class Player : MonoBehaviour
{
public void HandleInput()
{
if (Input.GetKeyDown(KeyCode.B))
{
yVelocity = JUMP_VELOCITY;
setGraphics(IMAGE_JUMP);
}
}
}
이때 점프 중인지 확인하지 않으면 공중에서도 무한대로 점프할 수 있다. isJumping 플래그를 하나 넣어 방지한다.
public class Player : MonoBehaviour
{
public void HandleInput()
{
if (Input.GetKeyDown(KeyCode.B))
{
if (isJumping == false)
{
isJumping = true;
yVelocity = JUMP_VELOCITY;
setGraphics(IMAGE_JUMP);
}
}
}
}
이번엔 땅에 있을 때 아래 버튼을 누르면 엎드리고, 버튼을 떼면 다시 일어나는 기능을 추가한다.
public class Player : MonoBehaviour
{
public void HandleInput()
{
if (Input.GetKeyDown(KeyCode.B))
{
if (isJumping == false)
{
isJumping = true;
yVelocity = JUMP_VELOCITY;
setGraphics(IMAGE_JUMP);
}
}
else if (Input.GetKeyDown(KeyCode.DownArrow))
{
if (isJumping == false)
{
setGraphics(IMAGE_DUCK);
}
}
else if (Input.GetKeyUp(KeyCode.DownArrow))
{
setGraphics(IMAGE_STAND);
}
}
}
이때 점프 중 아래 버튼을 떼면 서 있는 모습으로 보이는 버그가 있다.
이번에도 플래그를 하나 추가하여 막을 수 있겠지만, 이런 식으론 끝이 없다.
FSM
- 가질 수 있는 '상태'가 한정된다. - 위 예시에서는 서기, 점프, 엎드리기 상태에 해당한다.
- 한 번에 '한 가지' 상태만 될 수 있다. - 플레이어는 점프와 동시에 서 있을 수 없다. FSM에서는 동시에 두 가지 상태가 될 수 없다.
- '입력'이나 '이벤트'가 기계에 전달된다. - 위 예시에서는 버튼 누르기와 버튼 뗴기에 해당한다.
- 각 상태에는 입력에 따라 다음 상태로 바뀌는 '전이'가 있다. - 입력이 들어왔을 때 현재 상태에 전이가 있다면 전이에 따라 다음 상태로 전환된다.
위 예제를 FSM으로 바꿔보자.
# StateType.cs
public enum StateType
{
STADING,
JUMPING,
DUCKING,
}
여러 상태 중 한 가지 상태만 갖기 때문에 열거형(enum)을 사용한다.
# PlayerState.cs
public class PlayerState
{
public virtual void HandleInput() { }
public virtual void Update() {}
}
public class StandingState : PlayerState
{
public override void HandleInput(Player player)
{
if (Input.GetKeyDown(KeyCode.DownArrow))
{
// 엎드린 상태로 바꾼다.
player.SetState(StateType.DUCKING);
}
}
public override void Update(Player player)
{
}
}
public class JumpingState : PlayerState
{
...
}
...
플레이어의 상태를 나타내는 객체. 각각의 state는 PlayerState를 상속 받는다.
State 객체 내 HandleInput에서 다른 키 입력이 들어왔을 경우 player의 상태를 바꾼다.
# Player.cs
public class Player : MonoBehaviour
{
private PlayerState m_state;
private StandingState m_standingState;
private JumpingState m_jumpinState;
private DuckingState m_duckingState;
private void Update()
{
m_state.Update(this);
}
private void HandleInput()
{
m_state.HandleInput(this);
}
public void SetState(StateType type)
{
PlayerState newState = null;
switch (type)
{
case StateType.STADING:
newState = m_standingState;
break;
case StateType.JUMPING:
newState = m_jumpinState;
break;
case StateType.DUCKING:
newState = m_duckingState;
break;
}
if (newState != m_state)
{
m_state = newState;
}
}
}
플레이어는 현재 상태를 나타내는 PlayerState를 갖고 있다.
m_state에서 SetState플레이어의 현재 상태와 새로운 상태가 다를 경우 새로운 상태로 현재 상태를 바꿔준다.
입장과 퇴장
플레이어의 상태가 변경될 때 이미지 변경 등 기능이 추가적으로 필요하다고 해보자.
public class StandingState : PlayerState
{
public override void HandleInput(Player player)
{
if (Input.GetKeyDown(KeyCode.DownArrow))
{
// 엎드린 상태로 바꾼다.
player.SetState(StateType.DUCKING);
// 이미지를 바꾼다.
player.SetImage(IMAGE_DUCKING);
}
}
...
}
이런 식으로 스테이트가 바뀌는 부분에서 기능을 넣어줄 수 있겠지만, FSM에 입장/퇴장 기능을 넣어 좀 더 상태머신처럼 만들 수 있다.
# PlayerState.cs
public class PlayerState
{
public virtual void HandleInput() { }
public virtual void Update() {}
public virtual void EnterState() {}
public virtual void ExitState() {}
}
스테이트의 Enter와 Exit 시점에 호출하는 EnterState와 ExitState를 각 자식 스테이트에서 오버라이드한다.
# Player.cs
public class Player : MonoBehaviour
{
...
public void SetState(StateType type)
{
...
if (newState != null &&
newState != m_state)
{
m_state.ExitState();
m_state = newState;
m_state.EnterState();
}
}
}
Player의 스테이트가 바뀌는 시점에 현재 스테이트의 ExitState를 호출하고,
현재 스테이트를 새 스테이트로 바꾸고 EnterState를 호출한다.
PlayerState를 상속받는 각각의 자식 클래스에서는 상황에 맞는 기능을 작성하면 된다.
public class StandingState : PlayerState
{
...
public override void EnterState()
{
player.SetImage(IMAGE_STANDING);
}
public override void ExitState()
{
...
}
}
'Design Pattern' 카테고리의 다른 글
싱글턴 (Singleton) (0) | 2020.09.19 |
---|---|
프로토타입 (Prototype) (0) | 2020.07.19 |
관찰자 (Observer) (0) | 2020.07.05 |