횡스크롤 러닝 게임이 있다고 해보자. 사용자가 입력할 수 있는 버튼은 점프와 슬라이딩이 있다. 그럼 캐릭터가 가질 수 있는 상태는 아래와 같을 것이다.


달리기(평소)

점프

슬라이딩

죽음


만약 게임이 업데이트가 되어서 상태들이 늘어날 수도 있을 것이다. 기획자에 따라서 점프 도중 한 번 더 점프를 할 수 있는 이중점프, 점프시 빠르게 땅으로 내려오는 급하강, 무적아이템을 먹었을 때의 무적상태 등등을 추가할 수도 있다. 그 때마다 다른 클래스에서 이 상태들을 사용할 때 이중점프.Do() 혹은 급하강.Do() 이런 식으로 직접 써준다면, 상태가 늘어날 때마다 그 상태들을 사용하는 클래스를 수정해야 할 것이다. 그것은 객체지향의 Open-Closed Principle(확장에는 열리게 하고, 수정에는 닫히게 해야 한다는 객체지향 원칙)을 어기는 것이다. 그래서 각 상태들을 IState라는 인터페이스로 묶어주고, IState.Do()로 실행하게 한다면, 상태가 아무리 추가가 되어도 IState 변수에 들어갈 상태 객체만 바꿔주면 되기 때문에 더이상 상태를 사용해주는 클래스를 수정하지 않아도 될 것이다.


이런식으로 객체들을 인터페이스로 묶어주고, 객체들의 멤버함수를 실행할 때 직접실행하는게 아니라 인터페이스.Do() 이런식으로 실행한다. 이렇게 OCP를 지치고 좀 더 유연하게 구조를 짜는 것이 전략패턴의 핵심이다. 그리고 Set함수를 만들어서 인터페이스 변수에 내가 원하는 객체를 넣어줄 수 있게 한다.


보통 게임프로그래밍을 본격적으로 시작할 때 가장 먼저 배우는 것 중 하나가 유한상태기계(FSM, Finite State Machine)일 것이다. 키보드 마우스의 입력에 따라서 캐릭터의 상태가 바뀌게 하고, 현재 상태에 따라 다른 행동이나 반응을 하게 하는 것, 그것이 캐릭터의 유한상태기계이다. 전략 패턴을 사용하면 유한상태기계를 객체지향적으로 만들 수 있다.


위의 횡스크롤 러닝 게임의 유한상태기계를 전략 패턴을 이용해서 간단하게 만들면 아래와 같다.






클래스 구조 설명 : 

Player는 유저가 조작하는 캐릭터이고,

StateMachine은 키보드 입력에 따라 상태를 바꿔주고 각 상태의 Operate 함수를 실행시켜주는, 상태들을 총괄해주는 역할이다.

IState 및 세부 상태들은 맨 위에서 말했듯이 각 상태들을 객체로 만들어준 것이다.



게임 플레이 시에 상태끼리 연결되는 과정 : 

Run 상태일 때 : z를 누르면 점프. X를 누르면 슬라이딩. 적에게 닿으면 죽음.

Jump 상태일 때 : 적에게 닿으면 죽음

Sliding 상태일 때 : z를 누르면 점프, 적에게 닿으면 죽음.

dead 상태일 때 : 키 입력 받지 않음. 마우스로 다시하기 버튼을 클릭하면 게임 리셋(여기서는 구현하지 않겠음).



OperateEnter, OperateUpdate, OperateExit는 무엇 ? :

간략하게 말하자면, Enter 함수에는 상태가 바뀌자마자 하고 싶은 행동을 넣고, Update는 매 프레임마다 하고 싶은 행동을, Exit에는 다른 상태로 바뀌기 직전에 하고 싶은 행동을 넣으면 된다. 보통 객체의 라이프 사이클을 짜다보면 Enter, Update, Exit 구조로 짜는 경우가 많다. 유니티에도 이러한 경우가 많은데, 충돌처리의 OnCollision 함수 시리즈에도 OnCollisionEnter, OnCollisionStay, OnCollisionExit 로 이루어져있고, 유니티의 MonoBehaviour에도 Start, Update OnDestroy 등등의 함수가 존재하는 걸 알 수 있다.



왜 Enter, Update, Exit가 필요? : 

예를 들어 Dead의 경우에는 상태가 바뀌자마자 캐릭터의 모습을 Dead.Png로 바꾸고, 다른 키 입력이 안되게만 하면 되므로 Enter만 필요하게 된다.

반면 Sliding에 경우에는 상태가 바뀌자마자 엎드려있는 모습으로 바꾸고, x버튼에서 손을 떼면 슬라이딩을 끝내고 다시 일어서는 모습으로 바꿔야하므로 Enter와 Exit가 둘 다 필요하다.

이렇게 상태별로 Enter, Exit, Update가 필요한 경우가 있으므로 원활한 게임 제작을 위해서 이런식으로 구조를 짜는 것이다. 




상세한 코드는 아래와 같다.



//플레이어블 캐릭터
public class Player : MonoBehaviour
{
    private enum PlayerState
    {
        Run,
        Sliding,
        Jump,
        Dead
    }

    private StateMachine stateMachine;

    //스테이트들을 보관
    private Dictionary<PlayerState, Istate> dicState = new Dictionary<PlayerState, Istate>();

    // Start is called before the first frame update
    void Start()
    {
        //상태 생성
        IState run = new StateRun();
        IState sliding = new StateSliding();
        IState jump = new StateJump();
        IState dead = new StateDead();

        //키입력 등에 따라서 언제나 상태를 꺼내 쓸 수 있게 딕셔너리에 보관
        dicState.Add(PlayerState.Run, run);
        dicState.Add(PlayerState.Sliding, sliding);
        dicState.Add(PlayerState.Jump, jump);
        dicState.Add(PlayerState.Dead, dead);

        //기본상태는 달리기로 설정.
        stateMachine = new StateMachine(run);
    }

    // Update is called once per frame
    void Update()
    {
        //키입력 받기
        KeyboardInput();

        //매프레임 실행해야하는 동작 호출.
        stateMachine.DoOperateUpdate();
    }

    //키보드 입력
    void KeyboardInput()
    {
        if(Input.GetKeyDown(KeyCode.Z))
        {
            //달리기, 슬라이딩 중일 때만 점프 가능
            if (stateMachine.CurrentState == dicState[PlayerState.Run] || stateMachine.CurrentState == dicState[PlayerState.Sliding])
            {
                stateMachine.SetState(dicState[PlayerState.Jump]);
            }
        }

        if(Input.GetKeyDown(KeyCode.X))
        {
            //달리기 중에만 슬라이딩 가능.
            if(stateMachine.CurrentState == dicState[PlayerState.Run])
            {
                stateMachine.SetState(dicState[PlayerState.Sliding]);
            }
        }
    }

    public void OnTriggerEnter(Collider other)
    {
        if(other.tag == "Enemy")
        {
            stateMachine.SetState(dicState[PlayerState.Dead]);
        }
    }
}



public class StateMachine
{
    //현재 상태를 담는 프로퍼티.
    public IState CurrentState { get; private set; }

    //기본 상태를 생성시에 설정하게 생성자 만들기.
    public StateMachine(IState defaultState)
    {
        CurrentState = defaultState;
    }

    //외부에서 현재상태를 바꿔주는 부분.
    public void SetState(IState state)
    {
        //같은 행동을 연이어서 세팅하지 못하도록 예외처리.
        //예를 들어, 지금 점프중인데 또 점프를 하는 무한점프 버그를 예방할수도 있다.
        if(CurrentState == state)
        {
            Debug.Log("현재 이미 해당 상태입니다.");
            return;
        }

        //상태가 바뀌기 전에, 이전 상태의 Exit를 호출한다.
        CurrentState.OperateExit();

        //상태 교체.
        CurrentState = state;
        
        //새 상태의 Enter를 호출한다.
        CurrentState.OperateEnter();
    }
    
    //매프레임마다 호출되는 함수.
    public void DoOperateUpdate()
    {
        CurrentState.OperateUpdate();
    }
}



//상태들의 최상위 인터페이스.
public interface IState
{
    void OperateEnter();
    void OperateUpdate();
    void OperateExit();
}



//각 세부 상태들은 굳이 정의하지 않겠다.
public class StateSliding : IState
{
    public void OperateEnter()
    {
        
    }

    public void OperateExit()
    {
        
    }

    public void OperateUpdate()
    {
        
    }
}


+ Recent posts