State 패턴 / State Machine

개발조하·2023년 12월 2일
0

Unity

목록 보기
13/30
post-thumbnail

1. [디자인패턴] State 패턴

게임 속 애니메이션이 많아질 경우.. 모든 애니메이션을 bool로 상태를 파악해서 if~else문으로 출력해주는 코드는 매우 비효율적이며 특히 Blending이 필요한 애니메이션이 많다면 더욱 피곤하다.. 뿐만 아니라, 상태 분기가 많아질수록 버그 발생 확률도 높아지게 된다.

💡 그래서, State 패턴을 사용한다!
ㄴ 각 상태에 따른 행동들 출력하는 함수 생성

[전체 코드]

public class PlayerController : MonoBehaviour
{
    [SerializeField] float _speed = 10.0f;

    Vector3 _destinationPos;

    void Start()
    {
        Managers.Input.MouseAction -= OnMouseClicked;
        Managers.Input.MouseAction += OnMouseClicked;
    }

    float wait_run_ratio = 0f;

    public enum PlayerState 
    {
        Die,
        Moving,
        Idle,
    }

    PlayerState _state = PlayerState.Idle;

    private void UpdateIdle()
    {
        //애니메이션
        Animator anim = GetComponent<Animator>();
        wait_run_ratio = Mathf.Lerp(wait_run_ratio, 0, 10.0f * Time.deltaTime);
        anim.SetFloat("wait_run_ratio", wait_run_ratio);
        anim.Play("WAIT_RUN");
    }

    private void UpdateMoving()
    {
        //player 이동 및 회전
        Vector3 dir = _destinationPos - transform.position;
        if (dir.magnitude < 0.00001f) //player가 destination에 도착하면
        {
            _state = PlayerState.Idle;
        }
        else
        {
            //목적지로 이동
            float moveDist = Math.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);
            transform.position += dir.normalized * moveDist;
            //목적지를 주시하며 이동 (자연스러운 회전)
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime);
        }

        //애니메이션
        Animator anim = GetComponent<Animator>();
        wait_run_ratio = Mathf.Lerp(wait_run_ratio, 1, 10.0f * Time.deltaTime);
        anim.SetFloat("wait_run_ratio", wait_run_ratio);
        anim.Play("WAIT_RUN");
    }

    private void UpdateDie()
    {
        //아무것도 못함
    }
    void Update()
    {
        //애니메이션 실행
        switch (_state) 
        {
            case PlayerState.Die:
                UpdateDie();
                break;
            case PlayerState.Moving:
                UpdateMoving();
                break;
            case PlayerState.Idle:
                UpdateIdle();
                break;
        }
    }

    void OnMouseClicked(Define.MouseEvent evt)
    {
        if (_state == PlayerState.Die)
            return;

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, LayerMask.GetMask("Wall")))
        {
            //클릭한 땅 위치로 player가 이동
            _destinationPos = hit.point;
            _state = PlayerState.Moving;
        }
    }
}

현재 내 상태(state)에서 적용할 수 있는 코드를 분리해서 관리할 수 있다는 장점!

2. State Machine

지금은 상태에 따른 애니메이션 출력을 코드로 해주고 있다. 하지만 이것을 Animator창에서 노드를 활용하여 하나의 State Machine으로 사용하면 훨씬 용이하다. ㄴ 한 애니메이션(상태)에서 마우스 우클릭 - Make Transition 클릭 - 노드로 다른 애니메이션(상태) 연결 ㄴ 이렇게 노드로 연결해주면 굳이 Blend값을 주지 않아도 자동으로 Blending 모드로 세팅된다. (인스펙터창에서 조절 가능)

2.1 Has Exit Time

ㄴ즉, Exit Time으로 지정한 시간이 끝나면 전환을 할지 말지의 여부 결정.

  • 모든 노드의 Has Exit Time을 체크하고 실행하면, RUN 다음에 어떤 애니메이션이 출력될까? ㄴ RUN 상태의 인스펙터에 있는 Transitions에 설정된 순서에 따라 첫 번째에 있는 애니메이션과 변환된다.

단, JUMP가 Transitions에서 첫 번째 순서라고 하더라도 RUN -> JUMP 노드의 Has Exit Time이 체크해제되어 있다면? ㄴ RUN이 끝난 후 WAIT으로 변환되어 버린다!

2.2 Settings

  • Exit Time
    : Has Exit Time 에 체크가 되면 이 값은 전환이 실행되는 시간을 나타낸다. 이는 정규화된 시간에서 나타난다. (ex. 종료 시간이 0.75이면 애니메이션의 75%가 재생된 첫 프레임에서 Exit Time == true;이다. 다음 프레임에서 조건은 false가 된다. 반복 애니메이션의 경우 종료 시간이 1보다 작은 전환은 반복할 때마다 측정되며, 이를 사용하여 애니메이션 반복마다 적절한 전환 시간을 맞출 수 있다. Exit Time 이 1보다 큰 전환은 한 번만 측정되며, 이를 사용하여 일정 횟수 반복 후 지정한 시간에 종료할 수 있다. (ex. 종료 시간이 3.5인 전환은 3.5회 반복 후 한 번 측정됨)
  • Fixed Duration
    : 체크되어 있을 경우 전환 시간은 초(절대시간)로 나타낸다. 체크해제 시, 전환 시간은 소스 상태의 정규화된 시간(%)의 부분으로 나타낸다.
  • Transition Duration(s)
    : 현재 상태의 지속 시간을 기준으로 한 상대적인 전환 지속 시간. Fixed Duration 모드에 따라 정규화된 시간 또는 초 단위로 표시된다. 전환 그래프에서 두 파란색 마커 사이에 있는 부분으로 시각화된다.(블렌딩되는 시간)

2.3 Loop Time

실행했을 때 RUN의 경우 계속 반복해서 애니메이션이 출력되고, JUMP의 경우는 한 번만 출력된 후 가만히 있는다. 이는 애니메이션 파일의 Loop Time 체크 여부에 따라 결정된다.

3. 프로젝트 적용하기

3.1 특정 조건에 따라 WAIT / RUN 애니메이션 변환하기

  • 자동으로 변환되면 안되므로 연결 노드 모두 Has Exit Time 체크해제

  • 조건 삽입 ㄴ float로 변수를 선언했기 때문에 Conditions에 비교문이 Greater와 Less밖에 없다 (Equal X) 그러므로 1을 기준으로 1보다 크면 RUN출력, 1보다 작으면 WAIT 출력으로 조건을 넣어준다.

-> 즉, 코드와 Animator가 업무를 분담하여 애니메이션을 관리한다.
Animator에서 조건을 설정하여 사용한다면 코드에서 직접 애니메이션을 호출하지 않고, 그저 speed 값만 지정한 다음, Animator가 해당 speed 값에 해당하는 애니메이션을 출력해주게 된다.

📄 참고 자료
[인프런] C#과 유니티로 만드는 MMORPG 게임 개발 시리즈_State패턴
애니메이션 변환_Has Exit Time_Unity 공식 문서

profile
Unity 개발자 취준생의 개발로그, Slow and steady wins the race !

0개의 댓글