[섹션6] 애니메이션(Animation)
<Animation 기초>
- Animation 맛보기
1. 애니메이션이란? 플레이어 동작에 관여하는 요소, 디자이너가 만든 캐릭터의 동작을 코드와 합쳐서 자연스럽게 만든다.
Asset을 다운받은 다음 Animation 폴더에 UnityChan에 대한 애니메이션이 들어있다.
애니메이션을 클릭한 후 아래의 JUMP00을 drag drop하여 위로 올리면 애니메이션 동작화면을 살펴 볼 수 있다.
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으로 관리를하면 안된다.