기초 개념
애니메이션 파일 살펴보기
애니메이션은 짧은 동영상의 캡처 파일이라고 생각할 수 있다. 애니메이션 에셋 파일을 클릭한 후, Animation 탭으로 이동하면 애니메이션을 볼 수 있다. 선택된 모델이 없다면, 기본 유니티의 휴머노이드 모델을 선택하는 Unity Model을 클릭하면 된다,


아니면 DragDrop을 통해 다른 모델을 놓을 수도 있다.

유니티짱의 애니메이션인데, 유니티 기본 모델에서도 동작하는 것이 신기하다. 이는 사람과 같이 생긴 휴머노이드에 대해서 적용된다.

유니티짱의 모델 자체를 살펴보면, 해당 Avatar파일이 존재한다.


이를 열어보면 위와 같은 화면을 마주할 수 있으며, 이것이 아바타의 구조이다. 여러 골격 구조로 구성되어 있는데, 각 부위들을 연결해주면 그 부분을 인식해서 자동으로 애니메이션 재생할 수 있다. 이것을 "Animation retargeting"이라고 한다. 이를 통해 공용 애니메이션을 하나 만들어서 나머지 모델과 공유해서 쓸 수 있다.
Animation, Animator 컴포넌트
Animation 컴포넌트는 레거시로 현재는 사용을 하지 않는다고 한다.

Animator

Animator 컴포넌트는 avatar에 사용할 모델의 아바타를 넣어주면 되고, Controller 부분이 존재한다. 애니메이터를 조절하기 위해 컨트롤러를 꼭 사용해야 하는데, 이 방식을 메카님 애니메이션이라고 한다.
Animator controller를 만든 후 열어보면 다음과 같은 화면을 볼 수 있다.

애니메이션을 드래그 드롭을 통해 애니메이터 창에 옮긴 뒤, Entry에 우클릭을 하여 Set StateMachine Default State를 통해 기본 시작 애니메이션을 이을 수 있다.


그리고 애니메이션 파일 이름과 동기화되있는 것은 아니라, 이름을 마음대로 바꾸어도 된다.

Animator의 스크립트 사용
이와 같이 스크립트를 사용하여 애니메이션을 재생할 수 있다.
if (_moveToDest)
{
Animator anim = GetComponent<Animator>();
anim.Play("RUN");
}
else
{
Animator anim = GetComponent<Animator>();
anim.Play("WAIT");
}
Animator blending
위 처럼 애니메이션을 컨트롤하면, 애니메이션이 상태에 따라 뚝 끊겨버리므로 애니메이션을 서서히 변화시키는 블렌딩 기술을 이용해야 한다.


그래서 일정 퍼센트는 wait으로 틀고, 일정 퍼센트는 run으로 재생하는 적당히 섞는 비율을 만들어야 자연스럽게 멈춘다.


위 블렌드 트리에 우클릭을 한 번 더 하면 다음과 같은 창이 뜬다.

여기서 Add Blend Tree를 하면 한 번 더 블렌딩을 하는 것인데, 보통 기초 단계에서는 Add Motion을 많이 사용하게 될 것 같다.


인스펙터에서 Add motion Field를 누르는 것도 애니메이터에서 Add motion을 클릭하는 것과 같은 행위이다. 섞고 싶은 두 애니메이션을 위 인스펙터의 Motion에 등록해주면 된다.
이제 이를 섞을만한 적절한 Blend 값을 넣어주어야 한다.


기본으로 Blend값을 사용해도 되고, 새로운 파라미터를 추가할 수도 있다. 변수를 추가한 뒤, 인스펙터의 Parameter선택에서 사용할 파라미터를 선택하면 된다.

Threshold를 통해 어떻게 섞을 것인지 정의를 해야한다. Threshold가 0에 가까울수록 wait을하고, 1에 가까울 수록 RUN을 할 것이니 RUN을 1로 바꾸어 준다.

빨간색을 조정하면 블렌딩을 할 수 있는데, 빨간색을 0.7로 두면 RUN을 70% 비율로, WAIT을 30%비율로 섞는다. 즉, 부드럽게 멈추려면 1에서 시작해서 0까지 조금씩 변형시키면 자연스럽게 멈출 수 있게 된다. 이를 코드에서 동작하게 만들면 다음과 같다.

if (_moveToDest)
{
wait_run_ratio = Mathf.Lerp(wait_run_ratio, 1, 10.0f * Time.deltaTime); // 현재 값에서 1쪽에 근접하게
Animator anim = GetComponent<Animator>();
anim.SetFloat("wait_run_ratio", wait_run_ratio);
anim.Play("WAIT_RUN");
}
else
{
wait_run_ratio = Mathf.Lerp(wait_run_ratio, 0, 10.0f * Time.deltaTime); // 현재 값에서 1쪽에 근접하게
Animator anim = GetComponent<Animator>();
anim.SetFloat("wait_run_ratio", wait_run_ratio);
anim.Play("WAIT_RUN");
}
State 패턴
위에 있던 기능으로 적당히 돌아가는 게임은 만들 수 있지만 코드 부분에서 석연치 않은 부분들이 있다. 게임 규모가 커질수록 이런 식으로 애니메이션을 블렌딩하는 것은 불가능에가깝다. 와우는 한 캐릭터당 100개 넘는 애니메이션이 있다고 한다. 아래처럼 하드코딩으로 처리하는 것도 찝찝하다. 그리고 블렌딩을 떠나서라도 애니메이션의 상태가 많은 환경이면 정말 많은 상태가 있을 것이다. 점프를 하고 있냐, 점프가 떨어진 상태, 스킬 캐스팅, 스캘 채널링, 전투 상태 등 bool값 노가다를 통해 늘리면 지옥에 빠진다.
bool isJumping;
bool isFalling;
bool isSkillCasting;
bool isSkillChanneling;
if (_moveToDest)
{
wait_run_ratio = Mathf.Lerp(wait_run_ratio, 1, 10.0f * Time.deltaTime); // 현재 값에서 1쪽에 근접하게
Animator anim = GetComponent<Animator>();
anim.SetFloat("wait_run_ratio", wait_run_ratio);
anim.Play("WAIT_RUN");
}
else
{
wait_run_ratio = Mathf.Lerp(wait_run_ratio, 0, 10.0f * Time.deltaTime); // 현재 값에서 1쪽에 근접하게
Animator anim = GetComponent<Animator>();
anim.SetFloat("wait_run_ratio", wait_run_ratio);
anim.Play("WAIT_RUN");
}
그래서 이런 상황에서 심플하고 효과적인 것이 state 패턴이다.
public enum PlayerState
{
Die,
Moving,
idle,
Channeling,
Jumping,
Falling,
}
PlayerState _state = PlayerState.idle;
void UpdateDie()
{
}
void UpdateMoving()
{
}
void UpdateIdle()
{
}
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);
transform.position += dir.normalized * moveDist;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
}
}
switch (_state)
{
case PlayerState.Die:
UpdateDie();
break;
case PlayerState.Moving:
UpdateMoving();
break;
case PlayerState.idle:
UpdateIdle();
break;
}
}
위 상태에서 다른 상태로 넘어갈 수 있도록 생각을 한 번 해보자. 기존에 if - else로 관리할 때 동시에 작용을 하고 꼬여서 문제가 되었지만, 상태별로 생각한다면 쉽다. 멈춰있는 상태에서 키를 누른다면 moving으로 넘어갈 수 있고, 멈춰있는 상태에서 죽으면 Die로 넘어갈 수 있다. 이처럼 상태를 분리해서 코드를 만들면, 상태에서 적용할 수 있는 코드를 분리해서 만들 수 있다.
그래서 정리하자면 다음과 같이 State패턴을 사용하여 코드를 정리할 수 있다.
float wait_run_ratio = 0;
public enum PlayerState
{
Die,
Moving,
idle,
}
PlayerState _state = PlayerState.idle;
void UpdateDie()
{
// 아무것도 못함.
}
void UpdateMoving()
{
Vector3 dir = _destPos - transform.position;
if (dir.magnitude < 0.0001f)
{
_state = PlayerState.idle;
}
else
{
float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);
transform.position += dir.normalized * moveDist;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
}
// 애니메이션
wait_run_ratio = Mathf.Lerp(wait_run_ratio, 1, 10.0f * Time.deltaTime); // 현재 값에서 1쪽에 근접하게
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); // 현재 값에서 1쪽에 근접하게
Animator anim = GetComponent<Animator>();
anim.SetFloat("wait_run_ratio", wait_run_ratio);
anim.Play("WAIT_RUN");
}
void Update()
{
switch (_state)
{
case PlayerState.Die:
UpdateDie();
break;
case PlayerState.Moving:
UpdateMoving();
break;
case PlayerState.idle:
UpdateIdle();
break;
}
}
그러나, 이 패턴의 단점은 동시에 여러 상태를 가지지 못한다. 어느 정도 규모가 커지기 전까지는 좋다고 한다. 정확하게 만병통치약은 아닌게, 스킬이 들어간다면 만약 움직이면서 스킬을 쓸 수 있다고 하면 또 까다로워지기 시작한다.
State Machine - 1
State에 따라 로직이 동작하도록 만들면, 거기에 해당하는 애니메이션이 하나씩 있을 것이다. 그래서 이런 애니메이션 관리는 State패턴과 밀접한 연관이 있다.

각 애니메이션을 애니메이션으로만 보지말고, 각 상태로만 생각하면 그 상태에 대응되는 애니메이션을 넣어준 것이라 볼 수 있다. 위 이미지처럼 스테이트를 정리한 것을 state machine이라 한다.
근데 위에서 했던 것처럼 코드로 관리할 수 있는데, 이 방식을 사용하는 것은 state와 조건이 점점 많아지고 복잡해질수록 코드로 관리하는 것보다는 시각적인 방식으로 관리하는 것이 더 좋을 것이다.

기존에는 코드상에서 WAIT_RUN을 강제로 호출하고 있었는데, 이는 사용하지 않을 것이니 삭제한다. 이 상태에서 시작해보면 애니메이션도 기본적으로 조금 블렌딩되어 부드럽게 이어진다.
Transition 화살표를 클릭하여 해당 파란색 화살표를 움직일 수 있는데, 이를 조정해서 RUN을 끝까지 채우게 되면, WAIT을 기다리지 않고 툭 끊기게 된다. 즉, 이는 블렌딩을 하는 역할이다. 그래서 화살표로 연결할 때는 코드상에서 힘들게 블렌딩을 하지 않아도 된다.


그리고 코드에서 Transition 관련으로 호출하지 않아도 연결만 되어있으 계속 혼자서 왔다갔다 하는데, 이는 Has Exit Time 때문이다.

Hax Exit Time이 체크되면, 애니메이션이 다 실행되면 빠져나오라는 메시지이다. 이를 끄게 되면 RUN -> WAIT로 가지 못하므로 RUN 애니메이션만 재생된다.

Settings는 사실 블렌더를 조정하는 파란색 칸에 따라 수치가 바뀌는 그것이다.
- Exit Time: 몇 초 후에 다음 트랜지션으로 넘어갈 것이냐
- Fixed Duration: 이 부분이 체크되었다면, Exit time은 절대시간을 나타내게 된다. 그래서 0.68초후에 넘어가겠다는 것처럼 되고, 해당 체크를 제거하면 퍼센트처럼 작동해서 애니메이션의 68.75%가 완료되면 넘어가겠다는 의미이다.
- Transition Duration: 중간에 블렌딩된 겹친 부분은 몇 초동안 할 것인가
어쩄든 Transition은 Blend duration이나 활성화 조건도 정의한다. 그래서 특정 조건이 충족될 떄만 transition이 발생하도록 설정할 수 있다. 이러한 조건을 설정하려면 애니메이터 컨트롤러에서 파라미터의 값을 지정해야 한다.


그리고 이처럼 애니메이션이 두 번 표시되는 이유는애니메이션 트랜지션 중 다른 RUN 애니메이션의 시작 부분이 블렌딩 과정에 포함되기 때문이다. 그래서 블렌딩 위치를 왼쪽으로 조금만 변화시켜주어도 한 번만 보이는 것을 확인할 수 있다.
State Machine - 2
근데, 우리가 상태를 선택하는 것이 아니라, 자동으로 상태를 선택하는 것이 무슨 의미가 있을지 의문이 들 수 있다. 일반적으론 잘 사용되지 않지만, 경우에 따라서 유용한 순간이 있다. 예를 들어, 와우는 점프가 올라갈 때, 허공에 잠시 멈춰있을 때, 내려갈 때로 3개로 나누어져있는데, JUMP00, JUMP01, JUMP02를 Transition으로 엮은 뒤 인위적으로 상태 변경을 넣지 않더라도 Hax exit time설정을 하여 자동으로 트랜지션이 움직이는게 유용할 떄가 있다. 물론 Run, Wait에서는 곤란하기에 옵션을 꺼주는게 좋다.
우선 조건에 따른 애니메이션을 설정하는 것 전에 loop에 대해서 먼저 알아보자.


이렇게 설정해두면 무한으로 RUN 상태가 반복 실행되는 것을 확인할 수 있다, 그러나, JUMP는 반복되지 않는데 이는 loop 설정이 안되있어서 그렇다.


처음 애니메이션 파일을 만들 때 해당 부분이 켜져있고, 꺼져있고에 따라 반복 실행되고 안되고의 차이가 있다.

트랜지션에서 Has Exit Time을 해제하였다.
현재 상태를 기반으로 다음으로 넘어갈 것을 코드 상에서 호출을 해야한다고 가하여 현재 상태를 기반으로 코드에서 호출을 하게 된다면 복잡한 상황에 대한 처리는 매우 어려울 것이다. 그렇게 할거면 애초에 모든 것을 코드로 관리하는 것이 더 깔끔할 것이다.

파라미터를 통해 프로그램과 애니메이터 컨트롤러와 통신한다. 애니메이터 창에서는 코드에서 넘겨준 파라미터의 조건에 따라 Transition이 넘어갈 것인지 아닌지를 선택하게 된다.

파라미터를 통해 Transition의 condition을 설정할 수 있다. 이를 통해 코드와 애니메이션을 분리해서 관리할 수 있다.
void UpdateMoving()
{
Vector3 dir = _destPos - transform.position;
if (dir.magnitude < 0.0001f)
{
_state = PlayerState.idle;
}
else
{
float moveDist = Mathf.Clamp(_speed * Time.deltaTime, 0, dir.magnitude);
transform.position += dir.normalized * moveDist;
transform.rotation = Quaternion.Slerp(transform.rotation, Quaternion.LookRotation(dir), 20 * Time.deltaTime);
}
// 애니메이션
Animator anim = GetComponent<Animator>();
// 현재 게임 상태에 대한 정보를 넘겨준다.
anim.SetFloat("speed", _speed);
}
void UpdateIdle()
{
// 애니메이션
Animator anim = GetComponent<Animator>();
anim.SetFloat("speed", 0);
}
나중에는 Layer를 추가하여 상 하체 분리 애니메이션을 분리해서 재생할 수 있게 만들 수도 있다.


그리고 스킬은 조금 예외가 있긴하다. MMO로 만든다고하면 직업별로 스킬이 정말 많을텐데, 그것들 하나 하나를 state로 늘리기 시작하면 굉장히 관리하기 어려울 것이다. 그래서 대부분 이동과 관련된 것들은 state machine에 넣고, 스킬이 얼마없을 때는 state에 넣지만 스킬이 많다면 코드로 뺴서 처음했던 블렌딩처럼 따로 관리하는 경우가 훨씬 많다.
KeyFrame Animation


이처럼 Animation파일을 만들면 2가지의 파일인 애니메이션 파일과, 애니메이터 파일이 만들어지게 된다.

- 시간 조정
- Animation Event
- Key
Key
Key를 통해 특정 지점을 선택 후 그 사이 구간은 인터폴레이션처럼 보정하여 자연스럽고 편하게 애니메이션을 만들 수 있다. 보정은 Curves 탭으로 이동하여 할 수 있다.

Key의 추가 방법
- 우클릭을 통해 Add key 누르기
- 더블 클릭하기
- 시간을 옮긴 뒤 왼쪽의 특정 값에 대해서만 Key 추가하기

아래 이미지처럼 position에 대해 첫번쨰 key 0, 두 번째 key 5, 세 번째 key 10으로 설정하면 자동으로 인터폴레이션에 되어 실행할 수 있다. 그리고 대상 오브젝트는 이미 애니메이터가 붙어있을 텐데, 그 말은 즉, 인게임에서도 그대로 사용 가능하다.


또한 위처럼 설정하면 바로 curve도 설정되어 있는데, position에 대해 설정했으니 y축은 position이고, x축은 시간이다. 커브의 포인트는 Key와 동기화 되므로 필요한 부분에 key를 추가해서 curve를 변형할 수 있다.

이처럼 녹화버튼을 누른 뒤, 해당 시간에서 오브젝트의 변화를 주게되면 그 부분에 Key가 자동등록되게 되고, 해당 모습처럼 자연스럽게 변화할 수 있도록 만들 수 있다. 이 방식이 편하긴 하다.
Animation Event
사용법
- Animation Event 추가
- 콜백을 받을 스크립트 컴포넌트 추가
- 스크립트 작성
- 애니메이션 이벤트 등



public class CubeEventTest : MonoBehaviour
{
void TestEventCallback()
{
Debug.Log("Event Received!");
}
}

이것은 당연히 모든 애니메이션에 사용가능한 방법이다.

void OnRunEvent()
{
Debug.Log("뚜벅 뚜벅");
}
그리고 파라미터를 넘기고 싶을 수 있는데, 그 때 Function밑에 Float, Int, String 활용 가능. 그리고 똑같은 함수이름의 다른 버전은 먼저 이벤트를 선점한놈에게만 가기에, 함수는 하나씩만 써야 함. 매개 변수도 여러 개 사용 안됨.
참고자료
https://docs.unity3d.com/kr/2018.4/Manual/class-Transition.html
'유니티에서 게임개발을 추구하면 안되는걸까' 카테고리의 다른 글
| [Unity] Renderer.materials, Renderer.sharedMaterials (0) | 2024.11.08 |
|---|---|
| [Unity] TransformDirection() (1) | 2024.10.09 |
| [Unity] 기초 재정리[1] - 기본 컴포넌트 관련 (0) | 2024.08.27 |
| [Unity] 3D Project에서 2D Scene 생성 (0) | 2024.08.14 |
| [Unity] Time 클래스의 프로퍼티들 (0) | 2022.01.28 |