이븐아이 게임톤 2. State Machine
프로토타입은 급하게 만든다고 코드가 못봐줄 지경이었죠.
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;
}
딱히 갈라놓을 이유가 없겠다 생각될 수도 있겠지만, 더 큰 프로젝트였다면 말이 달라지겠죠.
다음번엔 현준님께 맡겨뒀던 레벨디자인 툴을 리뷰해볼까요?