[섹션6] 애니메이션(Animation) - 심화
<Animation 심화 - State Pattern>
- State 패턴
: 현재 내 상태에 따라 부르는 코드를 달리한다. 다음과 같이 PlayerState 별로 나누워 관리를 하면 boolean으로 나누었을 때(boolean으로 관리하게되면 스파게티 코드가 됨) 적용 코드를 분리하여 관리할 수 있음.
//Player 상태 정의
public enum PlayerState
{
Die,
Moving,
Idle,
Channeling,
Jumping,
Falling,
}
PlayerState _state = PlayerState.Idle;
void UpdateDie()
{
}
void UpdateMoving()
{
// 아래의 코드에 해당 상태일 때 animation 동작을 넣어주면 된다.
}
void UpdateIdle()
{
}
// 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);
}
}
switch (_state)
{
case PlayerState.Die:
// Die일때 불러올 함수
UpdateDie();
break;
case PlayerState.Moving:
// 움직일때 불러올 함수
UpdateMoving();
break;
case PlayerState.Idle:
UpdateIdle();
break;
}
1. 우선 키보드를 제외하고 마우스로만 동작하도록 Keyboard Action을 주석처리한다. _moveToDest의 boolean 변수도 state로 관리한다.
=> Update에서 마우스로 Player를 움직이던 부분을 UpdateMoving함수로 이동을 한다.
*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;
float wait_run_ratio = 0;
//Player 상태 정의
public enum PlayerState
{
Die,
Moving,
Idle,
}
PlayerState _state = PlayerState.Idle;
void UpdateDie()
{
// 아무것도 못함
}
void UpdateMoving()
{
// 아래의 코드에 해당 상태일 때 animation 동작을 넣어주면 된다.
Vector3 dir = _destPos - transform.position; // 현재 위치에서 최종 도착해야할 위치를 - : 방향 벡터
if (dir.magnitude < 0.0001f) // 그 차이가 아주 작으면 -> 도착
{
_state = PlayerState.Idle; // 멈춰있는 상태로 state 변경
}
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);
}
// 애니메이션 처리
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");
}
void UpdateIdle() // 멈춰있는 상태
{
//애니메이션
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");
}
// Update is called once per frame
void Update()
{
switch (_state)
{
case PlayerState.Die:
// Die일때 불러올 함수
UpdateDie();
break;
case PlayerState.Moving:
// 움직일때 불러올 함수
UpdateMoving();
break;
case PlayerState.Idle:
UpdateIdle();
break;
}
}
/*
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)
{
// 만약 Player가 Die 상태라면
if (_state == PlayerState.Die)
return;
/*
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한 점으로 바꿔야함.
_state = PlayerState.Moving;
//Debug.Log($"Raycast Camera @ {hit.collider.gameObject.tag}");
}
}
}
<Satae Machine>
- Satae Machine
1. 코드에서 적었던 상태하나하나가 Animator Controller에서 행동(=상태)와 대응된다.
Animation과 State는 깊은 연관성이 존재.
2. Animator Controller의 애니메이션들을 상태라고 생각하고 우클릭을 하여 Transaction을 만들어 준다.
위에서 만든 코드로 Animation을 관리할 수 있지만, 상태가 굉장히 많아지기 때문에 Animator Controller를 이용하여 그림으로 관리를 하는 것이 좋다.
-> 기존에 Unity Chan에서 제공하는 Animator Controller를 보면 복잡한 애니메이션 동작을 그림으로 표현한다. 이를 코드로 구현하기에는 가독성이 떨어지기 때문에 그림으로 나타낸다.
아래와 같이 Animator Controller를 만들어준다.

3. 이후 PlayerController.cs에서 WAIT_RUN과 관련된 부분을 없앤다. (주석처리를함.)
*PlayerController.cs
//float wait_run_ratio = 0;
//Player 상태 정의
public enum PlayerState
{
Die,
Moving,
Idle,
}
PlayerState _state = PlayerState.Idle;
void UpdateDie()
{
// 아무것도 못함
}
void UpdateMoving()
{
// 아래의 코드에 해당 상태일 때 animation 동작을 넣어주면 된다.
Vector3 dir = _destPos - transform.position; // 현재 위치에서 최종 도착해야할 위치를 - : 방향 벡터
if (dir.magnitude < 0.0001f) // 그 차이가 아주 작으면 -> 도착
{
_state = PlayerState.Idle; // 멈춰있는 상태로 state 변경
}
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);
}
// 애니메이션 처리
//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");
}
void UpdateIdle() // 멈춰있는 상태
{
//애니메이션
//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");
}
4. 그 후에 실행을 하면, 자동적으로 RUN을 하다가 WAIT을 하게 된다. 이때 뚝뚝 끊기는 것이 아닌 Blend 되어 자연스럽게 섞인 동작이 나오게 된다.
Animator Controller에서 WAIT와 RUN 사이의 화살표를 누르게 되면 다음과 같이 RUN과 WAIT를 조절할 수 있다. 다음 파란 부분을 보면 RUN과 WAIT이 적절히 섞인 것을 확인할 수 있다. 위에서 처럼 code로 blend하지 않고 바로 파란 부분을 조절하여 설정할 수 있다.

5. 위의 Play를 보면 WAIT과 RUN을 반복하여 진행하는데 이는 has Exit Time이 체크되어있기 때문에 일정 시간이 지나면 해당 행동에서 빠져나와 다음 행동으로 가는 것이다. 체크를 풀면 RUN을 반복하는 것을 확인할 수 있다.
6. JUMP00 행동을 추가하고 RUN과 다음과 같이 Transaction을 연결하여 준다.

7.이 상태에서 실행을 하면 WAIT이 먼저 시작한다. 이것은 RUN에서 Transaction이 WAIT이 먼저 이기 때문이다.
이 순서를 JUMP00가 먼저 되게 하면 RUN과 WAIT이 반복하여 행동한다.

8. 만일 JUMP00의 has Exist Time을 꺼버린 후 실행을 하면 JUMP가 아닌 WAIT이 실행된다
9. 다음은 아래 Setttings의 상세 부분이다.
Exit Time: 몇초 후에 나갈것인지?
Fixed Duration: 고정된 Exit Time인지 해당 체크를 해제하면 총 애니메이션 동작 시간에 Exit Time 변수 퍼센트만큼 실행하고 빠져나간다.
Transition Duration: 행동이 교차하는 시간은 어느정도인지

<State Machin #2>
위에서 처럼 자동적으로 다음 동작으로 넘어가게하는 방법이 유용한 경우는 예를 들어 하나의 점프 동작에서 여러개의 Animation이 합쳐진 경우 자동적으로 넘어가게 하는 것이 좋다. *물론 위에서 WAIT과 RUN을 하나로 합쳐준 것은 좋은 방법이 아님.
*Loop
: 다시 RUN00F 애니메이션을 추가하여 Entry에서 default state로 설정해놓고 실행하면, 계속 loop를 도는 것을 확인할 수 있지만, JUMP00을 추가하여 Entry에서 default state로 설정해놓고 실행하면, 한번만 실행한다. 이것은 Animation을 만들때, loop로 설정하지 않았기 때문이다.

- Animator 빠져나가는것 코드로
: Animator에서 해당 애니메이션이 빠져나가는것을 파라미터를 통해 통신한다. 코드에서 파라마터가 어떤 조건을 만족하면, 다음 행동으로 되도록 빠져나가게 만드는 것이다.
1. 다음과 같이 Animator Controller를 바꿔준다. 각각의 화살표에서 has Exit Time의 체크를 없애 준다. (특정 조건을 만족해야 다음 동작으로 넘어가도록 해야하기 때문에)

2. 그후 기존의 wait_run_ratio를 없애주고 speed라는 float 파라미터를 만들어준다. 그리고 각 Transaction condition(조건)에 추가한다. speed가 1보다 크면 RUN으로 speed가 1보다 작으면 WAIT로 하도록 설정한다.
그 후, 실행하면 0일때는 WAIT로 1보다 클 때는 RUN 동작을 실행하게 된다.
=> 이것은 Animator Controller는 오직 파라미터를 기준으로 생각한다.
3. 다음과 같이 speed 파라미터를 통해 게임의 상태를 Animator Controller에게 전달한다. 후에 실행하면 Player가 자연스럽게 동작하는 것을 확인할 수 있다.
*PlayerController.cs
void UpdateDie()
{
// 아무것도 못함
}
void UpdateMoving()
{
// 아래의 코드에 해당 상태일 때 animation 동작을 넣어주면 된다.
Vector3 dir = _destPos - transform.position; // 현재 위치에서 최종 도착해야할 위치를 - : 방향 벡터
if (dir.magnitude < 0.0001f) // 그 차이가 아주 작으면 -> 도착
{
_state = PlayerState.Idle; // 멈춰있는 상태로 state 변경
}
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);
}
// 애니메이션 처리
Animator amin = GetComponent<Animator>();
// 현재 게임 상태에 대한 정보를 넘겨준다.
amin.SetFloat("speed", _speed);
}
void UpdateIdle() // 멈춰있는 상태
{
//애니메이션
Animator amin = GetComponent<Animator>();
amin.SetFloat("speed", 0);
}
4. 상황에 따라 Layer를 여러개 하여 상 하체 분리 애니메이션을 동작하도록 할 수 있다.
<Animation 만들기>
Sence에서 플레이어 뿐만 아니라 게임 세상에서 존재하는 객체에 대해서도 일정 동작을 하도록 애니메이션 파일을 만들면 유용 => 게임에 종속적인 애니메이션은 애니메이터가 아닌 개발자가 만들어야함.
- KeyFrame
1. window -> Animation -> Animation을 통해 Animation을 추가한다. 그리고 객체를 선택하여 어떤 객체에 대해 Animation을 만들 것인지 선택한다. Cube Two를 선택하고 해당 파일을 저장한다. CubeTestAnim 파일을 생성한다.

2. Add Property에서 Transform-Position, Transform-Rotation, Transform-Scale을 추가한다.
- 왼쪽(인터페이스가 3개의 줄로) 위는 시간을 표시하는 줄이고, 그 아래 줄에서 왼쪽 마우스 클릭하면 Add Animaiton Event를 할 수 있고 그 아래 줄에넌 Add Key를 할 수 있다.
- 시간 간격은 아무 시간대를 클릭하고 스크롤을 올리거나 내리면 조절가능함.

3. Key는 해당 Key에 어떤 이벤트가 발생한다. 는 뜻으로 시간에 따라 객체가 움직이거나 할 때, 모든 프레임마다 좌표를 설정해 주는 것이 아닌, 시작과 끝에만 좌표를 설정해주고 그 중간 과정은 보정을 해서 부드럽게 움직이도록 하는 것이다.
- 추가 방법은 검정 줄 두번째 줄에서 Add Key를 해서 전체의 Key를 추가하거나
하얀줄 시간대를 옮겨서 특정 변수에 대한 Key를 추가할 수 있다. 특정 변수에 대해 클릭하면 Add Key로 추가 가능

4. 1초의 Key에서 x Position을 5, 2초의 Key에서 x Position을 10으로 한 후 플레이하면 자동적으로 Cube Two가 움직인다. -> Animation을 만들고 저장하면서 자동적으로 Cube Two의 Component로 Animator가 추가된다.

5. 설정 해준 시간대 사이의 Position 값을 Curve를 통해 부드럽게 움직이도록 설정해줄 수 있다.
설정한 Curve에 따라 새로운 Key가 생길 수 있다.

6. 사실 이렇게 하나하나 변수를 설정해주는 것도 어렵다. 객체 자체의 변화를 주어 설정하도록 하는 것이 더 편함.
다음에 Record 버튼을 눌러서 현재 객체를 촬영하는 동안의 변화를 Animation으로 바로 적용한다. 시간대를 움직여서 해당 시간대에 위치나 Rotation, Scale을 변화 시킬 수 있다.

- Animation Event
1. 위에서 설정한 Animation에서 특정 시간대에 이벤트 처리를 해야할 경우(효과음과 같은) 사용
-> 이러한 이벤트를 하드코딩하여 특정 시간대에 이벤트 발생을 하도록 하면 애니메이션이 변경 될 때 이벤트 발생 시점도 조절해야하는 어려움이 있음
2. Event의 경우 call back으로 동작, Event 발생 후, 함수가 실행되고 완료되면 call back으로 되돌아와야한다.
이를 위해 Cube Two에 call back으로 함수를 동작하도록 Component를 추가한다. -> CubeEventTest.cs 파일을 만든 후 추가한다.
*CubeEventTest.cs
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class CubeEventTest : MonoBehaviour
{
void TestEventCallback()
{
Debug.Log("Event Receive");
}
}
3. 그 후 Event에 함수를 위에서 정의한 TestEventCallback()으로 설정한다.

4. 플레이하면 해당 시점에 Event 함수가 실행되어 console 창에 다음과 같이 찍힌 것을 확인할 수 있다.

- Animation Event 받기
Animation에 Event를 추가한다. 달리는 동작에서 발이 땅에 닿는 곳에 맞춰서 사운드가 나오도록 설정
1. Animation에서 RUN00_F에 Animation을 추가하여 다음과 같이 설정 -> 함수를 OnRunEvent로 하고 해당 함수를 PlayerController.cs에 정의해서 이벤트를 실행하도록

*PlayerController.cs에 아래 함수 추가
void OnRunEvent()
{
Debug.Log("walk~~");
}
2. 이후 플레이를 하면 RUN 동작을 할때 OnRunEvent 함수를 실행하여 console 창에 다음과 같이 뜬다.

3. Animation에 Event를 추가할때 void 형 뿐만아니라 인자를 넘겨 줄 수 있다. 다음과 같이 int 값을 넘겨주고 함수도 int 인자를 받는 함수로 수정한다.

void OnRunEvent(int a)
{
Debug.Log($"walk~~{a}");
}
4. 플레이하면 다음과 같이 console 창에 뜬다.
