Development/유니티

[섹션6] 애니메이션(Animation)

mine__ral 2021. 7. 21. 19:31

<Animation 기초>

  • Animation 맛보기

1. 애니메이션이란? 플레이어 동작에 관여하는 요소, 디자이너가 만든 캐릭터의 동작을 코드와 합쳐서 자연스럽게 만든다.

Asset을 다운받은 다음 Animation 폴더에 UnityChan에 대한 애니메이션이 들어있다.

애니메이션을 클릭한 후 아래의 JUMP00을 drag drop하여 위로 올리면 애니메이션 동작화면을 살펴 볼 수 있다. 

 

플레이버튼을 누르면 애니메이션 동작 과정을 확인할 수 있다. Model은 유니티에서 기본으로 제공하는 모델로 설정하였다. 

2. Scene에 있는 UnityChan을 위의 화면에 drag drop하면 Model이 Unity Chan으로 바뀐다. 

여러 애니메이션을 클릭하여 플레이하면 다양한 동작들을 확인할 수 있다. 

=> 상황에 따라 여러 애니메이션을 적용하여 구성한다. 

 

3. UnityChan 전용 애니메이션을 다운받았음에도 Unity에서 제공하는 기본 모델에도 해당 애니메이션이 적용 가능하다. 물론, Cube 같은 것에는 애니메이션이 적용하지않는다. -> Rig에보면 Animation Type이 Humanroid로 사람의 형태인 것에만 적용이된다. 

 

  • Animation 적용

1. 예전에는 Animation이라는 Component를 사용하였지만, 현재는 Animator를 사용한다. 

=> 이러한 Animator를 control하는 Controller가 필요하다. Art 폴더 아래에 Animator Controller를 만든다. 

다음 Animator Controller창을 키고 RUNF 애니메이션과 WAIT 애니메이션을 drag drop한다. Animator Controller는 행동들의 캡쳐 합친 동영상이라고 볼 수 있다.  다음과 같이 설정한 후 Play를 하면, WAIT의 애니메이션이 계속적으로 반복한다. Entry를 클릭한 후 마우스의 우버튼을 눌러서 Set State Mashine default mashine을 선택한 후, RUN을 선택하면 Player가 달리는 애니메이션으로 바뀐다. 

=> 코드를 통해 해당 동작의 화살표가 상황에 따라 달라지도록 조정해줘야한다. 

*이때, Animator이름은 WAIT00은 Animation 폴더에 저장되어있는 이름 그대로 디폴트로 가져온다. Controller에서 자동으로 이름을 바꿀 수 있다. 

* alt 버튼을 누르고 마우스로 왼쪽 버튼을 클릭하면 위치를 조절할 수 있음.

 

2. PlayerController.cs를 통해서 해당 애니메이션을 조정한다. GetComponent를 통해 Animator를 가져올 수 있다. 

anim.Play("Animation이름")  -> 해당 애니메이션으로 움직인다. 

 void Update()
    {
        if (_moveToDest)
        {
            Vector3 dir = _destPos - transform.position; // 현재 위치에서 최종 도착해야할 위치를 - : 방향 벡터
            if(dir.magnitude < 0.0001f) // 그 차이가 아주 작으면 -> 도착
            {
                _moveToDest = false;
            }
            else
            {
                float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude); // clamp(value, float min, float max) value가 min보다 작으면 min, max보다 크면 max로 변함
                transform.position +=  dir.normalized * moveDist;

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

        if (_moveToDest) // 플레이어가 움직이고 있다면 RUN
        {
            Animator amin = GetComponent<Animator>();
            amin.Play("RUN");

        }
        else // 아닐 시에는 WAIT
        {
            Animator amin = GetComponent<Animator>();
            amin.Play("WAIT");
        }


    }

* 전체 PlayerController.cs

 

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

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

    bool _moveToDest = false;
    Vector3 _destPos; // 옮겨가야할 포인트 
    // Start is called before the first frame update

    void Start()
    {
        Managers.Input.KeyAction -= OnKeyboard;
        Managers.Input.KeyAction += OnKeyboard;

        Managers.Input.MouseAction -= OnMouseClicked;
        Managers.Input.MouseAction += OnMouseClicked;

    }

    float _yAnlgle = 0.0f;
    // Update is called once per frame
    void Update()
    {
        if (_moveToDest)
        {
            Vector3 dir = _destPos - transform.position; // 현재 위치에서 최종 도착해야할 위치를 - : 방향 벡터
            if(dir.magnitude < 0.0001f) // 그 차이가 아주 작으면 -> 도착
            {
                _moveToDest = false;
            }
            else
            {
                float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude); // clamp(value, float min, float max) value가 min보다 작으면 min, max보다 크면 max로 변함
                transform.position +=  dir.normalized * moveDist;

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

        if (_moveToDest) // 플레이어가 움직이고 있다면 RUN
        {
            Animator amin = GetComponent<Animator>();
            amin.Play("RUN");

        }
        else // 아닐 시에는 WAIT
        {
            Animator amin = GetComponent<Animator>();
            amin.Play("WAIT");
        }


    }

    void OnKeyboard()
    {
        if (Input.GetKey(KeyCode.W))
        {
            // W를 누르면 해당 위쪽(북쪽)방향을 바라보도록 rotation 변경
            //transform.rotation = Quaternion.LookRotation(Vector3.forward);
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.forward), 0.2f);
            transform.position += (Vector3.forward * Time.deltaTime * _speed); // 시간 x 거리 = 속력
        }
        if (Input.GetKey(KeyCode.S))
        {
            //transform.rotation = Quaternion.LookRotation(Vector3.back);
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.back), 0.2f);
            transform.position += (Vector3.back * Time.deltaTime * _speed);
        }

        if (Input.GetKey(KeyCode.A))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.left), 0.2f);
            //transform.rotation = Quaternion.LookRotation(Vector3.left);
            transform.position += (Vector3.left * Time.deltaTime * _speed);

        }

        if (Input.GetKey(KeyCode.D))
        {
            transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(Vector3.right), 0.2f);
            //transform.rotation = Quaternion.LookRotation(Vector3.right);
            transform.position += (Vector3.right * Time.deltaTime * _speed);

        }

        // keyboard에서 목적지로 이동하는 방식이 아님
        _moveToDest = false;

    }

    void OnMouseClicked(Define.MouseEvent evt)
    {
        /*
        if (evt != Define.MouseEvent.Click)
            return;
        */ // 마우스를 클릭한 상태에서도 움직이도록 주석처리

        Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition); ;// Screen의 좌표를 바로 World 좌표로 가져옴

        Debug.DrawRay(Camera.main.transform.position, ray.direction * 100.0f, Color.red, 1.0f);

        LayerMask mask = LayerMask.GetMask("Monster") | LayerMask.GetMask("Wall");
        // int mask = (1 << 8) | (1 << 9);

        RaycastHit hit;
        if (Physics.Raycast(ray, out hit, 100.0f, LayerMask.GetMask("Wall"))) // Plane이 현재 Layer가 Wall이기 때문에 
        {
            _destPos = hit.point; // 마지막 도착 지점을 Mouse가 point한 점으로 바꿔야함.
            _moveToDest = true;

            //Debug.Log($"Raycast Camera @ {hit.collider.gameObject.tag}");
            
        }

       
    }
}

3. 후에 플레이를 하게되면, 마우스를 계속 눌러서 이동하면 달리다가 마우스의 클릭이 멈추면 플레이어가 멈춘다. 

 

  • Animation Blending

위의 플레이에서의 문제점은 달려가다가 멈출때 너무 딱 멈추는 (자연스럽게 멈추지않는) 문제점이 존재한다. 

=> 이를 해결하기 위해 RUN 애니메이션과 WAIT 애니메이션의 비율을 조절하여 멈추도록 한다. 

 

1. Animator Controller에서 우클릭을 하여 Create State 선택 후 Blend Tree를 클릭하여 생성한다. 이름을 WAIT_RUN으로 바꾼 후, 클릭하여 타고 들어간다. 아래와 같이 Blend할 Motion을 추가한다. 해당 Motion에 섞고 싶은 motion을 추가한다. 

 

2. 아래에 Parameter에 wait_run_ratio를 두어 행동 비율을 몇으로 할 것인가를 정한다. 

wait_run_ration 파라미터를 WAIT_RUN blend tree에 파라미터로 둔다. 

3. Threshold값을 정해줘야하는데 0에 가까울수록 WAIT, 1에 가까울수록 RUN을 하도록 threshold를 다음과 같이 설정한다. 

4. 위의 그림에서 빨간 선은 wait_run_ratio 파라미터를 가르킨다. 해당 빨간 선을 조절하여 파라미터값을 지정해줄 수 있는데 만약 0.75라면 RUN 행동을 75%, WAIT 행동을 25%로 조절한다. 

=> 천천히 멈추기위해서는 wait_run_ratio의 비율을 줄여나가면 자연스럽게 멈출 수 있다. 

=> 파라미터의 값을 코드로 조정

 

5. PlayerController.cs에서 Mathf.Lerp를 이용하여 wait_run_ratio를 점점 줄어 들도록 할 수 있다. 

float wait_run_ratio를 선언한 후, anim.SetFloat("파라미터 이름", 변수)를 통해 파라미터 이름을 변수로 설정한다. 

float wait_run_ratio = 0; // wait_run_ratio 변수 선언
    // Update is called once per frame
    void Update()
    {
        if (_moveToDest)
        {
            Vector3 dir = _destPos - transform.position; // 현재 위치에서 최종 도착해야할 위치를 - : 방향 벡터
            if(dir.magnitude < 0.0001f) // 그 차이가 아주 작으면 -> 도착
            {
                _moveToDest = false;
            }
            else
            {
                float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude); // clamp(value, float min, float max) value가 min보다 작으면 min, max보다 크면 max로 변함
                transform.position +=  dir.normalized * moveDist;

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

        if (_moveToDest) // 플레이어가 움직이고 있다면 RUN
        {
            wait_run_ratio = Mathf.Lerp(wait_run_ratio, 1, 10.0f * Time.deltaTime);
            // Mathf.Lerp(float a, float b, float t) - a를 b까지 t비율로 서서히 다가간다. 
            Animator amin = GetComponent<Animator>();
            amin.SetFloat("wait_run_ratio", wait_run_ratio);// wait_run_ratio의 파라미터를 가져온다.
            amin.Play("WAIT_RUN");

        }
        else // 아닐 시에는 WAIT
        {
            wait_run_ratio = Mathf.Lerp(wait_run_ratio, 0, 10.0f * Time.deltaTime);
            Animator amin = GetComponent<Animator>();
            amin.SetFloat("wait_run_ratio", wait_run_ratio);
            amin.Play("WAIT_RUN");
        }


    }

 

=> 문제 규모가 큰 게임에서는 애니메이션이 굉장히 많기 때문에 해당 코드로 일일이 ratio를 지정해주는 것(Blending을 하는 것)은 어렵다. 

현재 동작 상태가 점프 중인지, 낙하 중인지 등등의 애니메이션 동작을 boolean으로 관리를하면 안된다.