관리 메뉴

기억을 위한 기록들

[GPP]상태 패턴 본문

디자인 패턴 ( Design Pattern )/게임프로그래밍 패턴

[GPP]상태 패턴

에드윈H 2023. 2. 2. 22:13

 

"객체의 내부 상태에 따라 스스로 행동을 변경할 수 있게 허가하는 패턴으로, 이렇게 하면 객체는 마치 자신의 클래스를 바꾸는 것처럼 보입니다." GoF의 디자인 패턴 35쪽

 

간단한 횡스크롤 플랫포머게임을 만든다고 쳐보자. 사용자가 B 버튼을 누르면 점프를 해야 한다.

캐릭터 Heroine이라는 클래스가 있다고 가정한다.

 

하지만 위 코드엔 버그가 있다.  바로 '공중점프' 즉, 무한 점프가 가능하다.

이 버그는 점프 상태를 확인하는 불리언 변수를 추가해 간단히 고칠 수 있다.

 

다음으로, 주인공이 땅에 있을 때 아래 버튼을 누르면 엎드리고, 떼면 다시 일어서는 기능을 추가했다.

하지만 이번에도 이번에도 버그가 있다.

 

1. 엎드리기 위해 아래 버튼을 누른 뒤,

2. B 버튼을 눌러 엎드린 상태에서 점프하고

3. 공중 자세 아래 버튼을 떼면

 

점프 중인데도 땅에 서 있는 모습으로 보인다....

이러면 또 각각 엎드린 상태, 서있는 상태 불리언 변수를 추가해준다 하더라도, 만약에

점프 중에 아래버튼을 눌러 내려찍기 공격이 추가가 된다면?... 끔찍하다 이런 식이면 버그에 파묻혀서 구현을 못 끝낼 것이다.

 

여기서 FSM(유한 상태 기계)가 나온다. 

FSM의 요점은

  • 가질 수 있는 '상태'가 한정된다.
  • 한 번에 '한 가지' 상태만 될 수 있다.
  • '입력'이나 '이벤트'가 기계에 전달된다.
  • 각 상태에는 입력에 따라 다음 상태로 바뀌는 전이(transiton)가 있다.

우선 간단한 방법으로는 열거형(enum)과 다중 선택문이 있다.

여기 예제에서는 FSM상태를 열거형으로 정의할 수 있다.

 

이제 Heroin에는 불리언 변수 여러 개 대신 State 변수 필드 하나만 있으면 된다.

분기 순서도 바뀐다. 이전에는 입력에 따라 먼저 분기한 뒤에 상태에 따라 분기했다.

따라서 하나의 버튼 입력에 대한 코드는 모아둘 수 있었으나 하나의 상태에 대한 코드는 흩어져 있었다.

상태 관련 코드를 한 곳에 모아두기 위해 먼저 상태에 따라 분기하게 하자.

 

 

분기문을 다 없애진 못했지만, 변수를 하나로 줄였고, 하나의 상태를 관리하는 코드는 깔끔하게 한곳에 모았다. 

이 정도만으로 충분할 때도 있다.

 

하지만 열거 형만으로 부족할 수도 있다. 이동을 구현하되,

엎드려 있으면 기가 모여서 놓는 순간 특수 공격을 쏠 수 있게(?) 만든다고 해보자. 엎드려서 기를 모으는 시간을 기록해야 한다.

 

매 프레임마다 호출되는 update()함수는 있었다고 치고, chargeTime_ 시간을 계산하는 변수가 있다.

 

엎드릴 때마다 시간을 초기화해야 하나 handleInput()을 바꿔보자.

기 모으기 공격을 추가하기 위해 함수 두 개를 수정하고, 엎드리기 상태(STATE_DUCKING)에서만 의미 있는 chargeTime_필드를 Heroine에 추가해야 했다... 이 모든 코드와 데이터를 한 곳에 모아둘 수 있는 게 낫다!. GoF가 나설 차례다

 

 

상태 패턴 등장

 

상태 인터페이스를 정의하자. 상태에 의존하는 코드, 즉 handleInput()과 update()가 해당된다.

 

상태별 클래스 만들기 

엎드려있는 상태 클래스이다.

chargeTime_ 변수도 옮겼다.

chargeTime_은 엎드리기 상태에서만 의미 있다는 점을 객체 모델링을 통해서 분명하게 보여준다는 점에서 훨씬 개선되었다.

 

동작을 상태에 위임하기

Heroine 클래스에 자신의 현재 상태 객체 포인터(HeroineState* state_)를 추가해, 거대한 다중 선택문은 제거하고,

 

상태 객체를 위임한다.

 

'상태를 바꾸려면' state_ 포인터에 HeroineState를 상속받는 다른 객체를 할당하기만 하면 된다.

 

이게 상태 패턴의 전부다.

 

 

상태 객체는 어디에 둬야할까?

1. 정적 객채

 

인스턴스 하나를 같이 사용하면 된다. (이런 게 경량 패턴)

각각의 정적 변수가 게임에서 사용하는 상태 인스턴스이다.

 

ex ) 서있는 상태에서 점프하게 하려면, 이렇게 한다.

2. 상태 객체 만들기 

정적 객체만으로는 부족할 때도 있다. 새로 상태를 할당했기 때문에 이전 상태를 해제해야 한다.

상태를 바꾸는 코드가 현재 상태 메서드에 있기 때문에 삭제할 때 this를 스스로 지우지 않도록 주의해야 한다.

이를 위해 handleinput()에서 상태가 바뀔 때에만 새로운 상태를 반환하고, 밖에서는 반환 값에 따라 예전 상태를 삭제하고 새로운 상태를 저장하도록 바꿔보자.

 

handleInput메서드가 새로운 상태를 반환하지 않는다면 현재 상태를 삭제하지 않는다. 서있기 상태에서 엎드리기 상태로 전이하려면 새로운 인스턴스를 생성해 반환해야 한다.

가능하다면 매번 상태 객체를 할당하기 위해 메모리와 CPU를 낭비하지 않아도 되는 정적 상태를 쓰려고 하는 편이다.

지금부터는 상태 패턴을 좀 더 "상태스럽게"만들 방법을 살펴본다.

 

입장과 퇴장

이렇게 하는 것보다 상태에서 그래픽까지 제어하는 게 바람직하다. 이를 위해 입장 기능을 추가하자.

 

Heroin클래스에서는 새로운 상태에 들어 있는 enter함수를 호출하도록 상태 변경 코드를 수정한다.

이제 엎드리기 코드를 더 단순하게 만들 수 있다.

 

 

병행 상태 기계

주인공이 총을 든다고 가정해보자. 총을 장착하고 나서도 이전에 할 수 있었던 행동들을 모두 할 수 있어야 한다.

그러면서 동시에 총을 쏠 수도 있어야 한다.  두 종류의 상태, 즉 무엇을 하는가와 무엇을 들고 있는가를 한 상태 기계에 욱여넣으면 코드는 약간 외에는 거의 같아서 중복이 많아진다.

 

모든 가능한 조합에 대해 모델링하려다 보니 모든 쌍에 대해 상태를 만들어야 한다. 해결법은 간단하다 상태 기계를 둘로 나누면 된다.

입력을 상태에 위임할 때에는 입력을 상태 기계 양쪽에 다 전달한다.

그리 바람직한 방법은 아니지만, 문제를 해결할 수는 있을 것이다.

 

 

 

계층형 상태 기계

상속으로 여러 상태가 코드를 공유할 수 있다. 점프와 엎드리기는 땅 위에 있는 상태로 정의한다.

'땅 위에 있는' 상태 클래스를 상속받아 고유 동작을 추가하면 된다.

이벤트가 들어올 때, 하위 상태에서 처리하지 않으면 상위 상태로 넘어간다. 말하자면 상속받은 메서드를 오버라이드 하는 것과 같다.

그다음 각각의 하위 상태가 상위 상태를 상속받는다.

 

계층형을 꼭 이렇게 구현해야 하는 건 아니다. 클래스를 사용하는 GoF 식 상태 패턴을 쓰지 않는다면 이런 구현이 불가능할 수 있다. 그럴 땐 주 클래스에 상태를 하나만 두지 않고, 상태 스택을 만들어 명시적으로 현재 상태의 상위 상태 연쇄를 모델링할 수도 있다.

 

현재 상태 = 스택 최상위

현재 상태의 상위 상태 = 그 하위 스택

... 등등 

'디자인 패턴 ( Design Pattern ) > 게임프로그래밍 패턴' 카테고리의 다른 글

[GPP]싱글턴  (0) 2023.02.02
[GPP]프로토타입 패턴  (0) 2023.02.02
[GPP]컴포넌트 패턴  (0) 2023.02.02
[GPP]경량 패턴  (0) 2023.01.29