프로토타입은 급하게 만든다고 코드가 못봐줄 지경이었죠.
Update
메소드 안에서 입력을 받으면 if-else if else… 로 이런저런 상태를 확인하고, 또 그 안에서 switch로 상태를 확인하고. 이래서는 알아보기 힘들죠.
public class PlayerController : MonoBehaviour
{
// Update is called once per frame
void Update ()
{
// 이게 터치 확인이었다면, phase를 확인하는 switch문이 있겠죠... 끔찍한데요?
if ( Input . GetKeyDown ( KeyCode . Space ))
{
if ( touching )
{
if ( OnGround )
{
Debug . Log ( "jump" );
rb . AddForce ( Vector2 . up * eff , ForceMode2D . Impulse );
}
else
{
// 붙어있는 나무의 충돌판정 비활성화
RaycastHit2D tree = Physics2D . BoxCast ( player . coll . bounds . center , player . coll . bounds . size , 0f , Vector2 . right , 0.01f , LayerMask . GetMask ( "Tree" ));
tree . transform . gameObject . GetComponent < Collider2D >(). enabled = false ;
Debug . Log ( "wall jump" );
rb . AddForce ( new Vector2 ( 0 , 3f ) * eff , ForceMode2D . Impulse );
}
}
}
if (! touching )
{
Vector2 pos = transform . position ;
pos . x += 0.1f ;
transform . position = pos ;
}
}
}
그래서 깔끔하게 State Machine 디자인패턴을 적용했습니다.
구조는 단순합니다. 우선 각 상태가 행해야 하는 행동양식을 정의해줍니다. Interface
로 말이죠.
public interface IState
{
IState HandleUpdate ();
IState HandleInput ();
}
매 프레임마다 호출되어야 하는 메소드는 두 개입니다. 우선 입력을 받고, 그 다음에 각 상태의 고유한 처리를 하는 것이죠.
플레이어에게 상태 필드를 선언해줍니다.
public partial class PlayerController : MonoBehaviour
{
private IState state ;
void Start ()
{
state = new StateRun ( this );
}
void Update ()
{
state = state . HandleInput ();
state = state . HandleUpdate ();
}
}
다람쥐 게임에서 플레이어가 가지는 상태는 딱 3가지입니다. 하이퍼캐주얼 장르답게, 조작은 단순하고 각 상태로의 전환 또한 복잡하지 않아서 이러한 스테이트 머신 구조를 간단하게 보여줄 만한 훌륭한 표본이 되겠군요. Run, Jump, Climb를 정의해줍니다.
public partial class PlayerController
{
class StateRun : IState
{
private PlayerController player ;
...
각 상태를 내부클래스 로 정의해주는 이유는 몇 가지가 있습니다. 플레이어의 상태를 외부에서 참조할 필요도 없으며, 참조가 되어서도 안 됩니다. 플레이어 안에서만 활용할 수 있도록 만드는 것이 첫 번째 이유죠. 또한 내부클래스는 외부클래스의 private 필드에 접근할 수 있습니다. 여러모로 나쁠 것 없는 거죠.
이렇게 만들게 되면, 각 상태에서 받는 입력 처리가 매우 명확해집니다. 버그가 생겨도, 어떤 상태에서 생기는 버그인지 쉽게 찾을 수 있게 되죠.
// Run State
private IState HandleKeyboardInput ()
{
if ( Input . GetKeyDown ( KeyCode . Space ))
{
// 위로 점프
player . rb . AddForce ( Vector2 . up * player . eff , ForceMode2D . Impulse );
// 점프했다면, StateJump를 반환하여 플레이어가 점프 상태가 될 수 있도록 함
return new StateJump ( player );
}
return this ;
}
// Jump State
private IState HandleKeyboardInput ()
{
if ( Input . GetKeyDown ( KeyCode . Space ))
{
// 더블 점프
if (! doubleJump )
{
doubleJump = true ;
player . rb . velocity = Vector2 . zero ;
player . rb . AddForce ( Vector2 . up * player . eff , ForceMode2D . Impulse );
}
}
return this ;
}
// Climb State
private IState HandleKeyboardInput ()
{
if ( Input . GetKeyDown ( KeyCode . Space ))
{
// 붙어있는 나무의 충돌판정 비활성화
RaycastHit2D tree = Physics2D . BoxCast ( player . coll . bounds . center , player . coll . bounds . size , 0f , Vector2 . right , 0.01f , LayerMask . GetMask ( "Tree" ));
tree . transform . gameObject . GetComponent < Collider2D >(). enabled = false ;
player . rb . velocity = Vector2 . zero ;
// 벽 점프
player . rb . AddForce ( Vector2 . up * player . eff , ForceMode2D . Impulse );
player . rb . gravityScale = 1f ;
return new StateJump ( player ); // Jump 상태로 이동
}
return this ;
}
딱히 갈라놓을 이유가 없겠다 생각될 수도 있겠지만, 더 큰 프로젝트였다면 말이 달라지겠죠.
다음번엔 현준님께 맡겨뒀던 레벨디자인 툴을 리뷰해볼까요?