공부/인프런 - Rookiss

Part 3-6-2. 애니메이션 : State 패턴, State Machine

셩잇님 2023. 8. 22. 15:37
반응형

 

 

Animation(애니메이션)

State 디자인 패턴

 

bool isFalling;
bool isJumping;
bool isSkillCasting;
bool isSkillChanneling;

if(isJumping)
{

}
else
{
    if(isSkillChanneling && isFalling)
    {

    }
    else
    {
        if(isSkillCasting)
        {

        }
    }
}

 

이런식으로 bool타입의 상태 flag 변수를 여러개 사용하면 너무 복잡하다. if - else문이 너무 많아진다. 상태 flag 변수가 적은 갯수라면 괜찮지만 규모가 큰 게임일 수록 많이 필요하기 때문에 적합하지 않다.

해결 방법 👉 State 패턴 사용하기!

 

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class PlayerController : MonoBehaviour
{
    float wait_run_ratio = 0;

    [SerializeField]
    float _speed = 10.0f;

    Vector3 _destPos;  // 목적지

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

    public enum PlayerState
    {
        Die,
        Moving,
        Idle,
    }

    PlayerState _state = PlayerState.Idle;

    void Update()
    {
        switch (_state)
        {
            case PlayerState.Die:
                UpdateDie();
                break;
            case PlayerState.Moving:
                UpdateMoving();
                break;
            case PlayerState.Idle:
                UpdateIdle();
                break;
        }
    }

    void UpdateDie()
    {
        // 아무것도 못함
    }

    void UpdateMoving()
    {
        Vector3 dir = _destPos - transform.position;
        if (dir.magnitude < 0.0001f)  // 도착함
        {
            _state = PlayerState.Idle;
        }
        else   // 도착 X 
        {
            float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);
            transform.position = transform.position + dir.normalized * moveDist;

            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 10 * Time.deltaTime);
            transform.LookAt(_destPos);
        }

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

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

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

        if (evt != Define.MouseEvent.Click)
            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")))
        {
            _destPos = hit.point;
            _state = PlayerState.Moving;
        }
    }
}
if (_state == PlayerState.Die)

//...

        switch (_state)
        {
            case PlayerState.Die:
                UpdateDie();
                break;
            case PlayerState.Moving:
                UpdateMoving();
                break;
            case PlayerState.Idle:
                UpdateIdle();
                break;
        }

 

enum으로 상태들을 정의하고, if-else문 혹은 switch문 사용하기!

대신 bool 상태 플래그 변수와는 다르게 동시에 두가지 상태를 가질 순 없음. 👉 이와 같은 특성 때문에 여러 동작이 꼬일 일이 없음. 동작 별로 분리해서 만들 수 있다는 장점.

isJumping && isAttacking  // 👉 가능
_state == PlayerState.Jump && _state == PlayerState.Attack  // 👉 불가능

 


 

State Machine

State 패턴과 관련이 깊다. 어떤 상태이냐에 따라 재생시킬 애니메이션이 다르다. State Machine 에서 하나의 State 는 하나의 Animation 클립에 대응한다.

 

다른 상태로 바뀌는 전이를 화살표로 이어주기만 하면 된다.

화살표에서 Condition 에서 그 전이 조건을 설정할 수 있다.

Parameter 값을 조건식에 이용한다.

 

Transition
✔️ Has Exit Time

 

 

그래프를 보면 RUN 상태(애니메이션)에서 WAIT 상태로 블렌딩 되듯이 전이가 일어나는 것을 볼 수 있다. 위 파란색 범위를 조절하여 설정할 수 있다. 이 범위가 좁을 수록 블렌딩이 적고 RUN 끝나자마자 바로 WAIT 이 재생되서 부자연스러울 수 있음


Has Exit Time 애니메이션 재생 종료 후 다음 전이로 이동한다. (Condition 조건에 따라 전이 되는게 아님)

 

체크 👉 종료 시점이 존재하게 되는 것.

  • Settings 의 Fixed Duration이 체크되어 있다면 Exit Time 값이 “시간”으로 해석된다. Exit Time 값이 0.7 이고 Has Exit TIme이 체크되어 있다면 절대적인 “0.7초”의 시간 후에 다음 전이가 일어난다.
  • Settings 의 Fixed Duration이 체크되어 있지 않다면 Exit Time 값이 “% 비율”로 해석된다. Exit Time 값이 0.7 이고 Has Exit TIme이 체크되어 있다면 “해당 애니메이션의 전체 재생 길이의 70 %”까지만 재생한 후에 다음 전이가 일어난다.

 

 

체크 해제 👉 애니메이션이 다 재생 되더라도 전이가 일어나지 않는다.

  • 전이가 일어나려면 Contidion 조건을 만족해야 한다. 따라서 파라미터 값으로 Condition 조건을 통해 전이가 즉각 일어나게 하려면 Has Exit Time 을 체크 해제해주어야 한다.

 

 

블렌딩 Has Exit Time 이걸 체크하면 Exit Time 비율만큼 애니메이션 재생 후 다음 상태로 전이된다. 체크 해제 하면 전이 조건을 만족해야만 즉시 전이가 이루어진다. 전이 조건이 만족될 때 즉각 재생되게 하고 싶으면 이게 체크 해제되어야 한다.

 

 

“RUN 👉 JUMP”, “RUN 👉 WAIT” 두 전이 모두 다 Has Exit Time이 체크되어 있다는 가정하에, 현재 전이 순서(Transitions)이 위와 같기 때문에 “RUN 👉 JUMP”이 먼저 실행되게 된다. 만약 “RUN 👉 JUMP” 전이는 Has Exit Time이 체크되어 있지 않다면 “RUN 👉 WAIT”가 실행될 것이다.

여담으로 애니메이션 자체가 계속 반복 실행되는 경우는 해당 애니메이션 상태가 loop Time 속성이 체크되어 있어서 그렇다.

 

 

Transition Duration이 바로 이 블렌딩 되는 파란색 범위 구간이다. 이 지연시간이 길면 길수록 자연스럽고 짧으면 짧을수록 애니메이션이 즉각즉각 바뀐다.

 

 

파라미터 사용

Animator anim = GetComponent<Animator>();
anim.SetFloat("speed", _speed);

 

이렇게 스크립트에서 움직일 때에 애니메이션 파라미터 값을 설정할 수가 있다. 이 파라미터 값의 변화에 따라 전이 Condition 조건시기 만족하여 전이가 일어나게 된다.

여담으로 실무에선 기본적인 움직임 동작은 State Machine 에서, 스킬 애니메이션은 어마어마하게 다양할 수 있기 때문에 따로 블렌딩 트리로 묶어 작성하는 식으로 작업 한다고 한다.

 

 

 

반응형