유니티

유니티 - 애니메이션

bugmin 2024. 5. 24. 19:32

Animation

2D Sprite Animation은 이미지를 빠르게 전환시켜 움직이는 거처럼 보이게함, 프레임수가 낮으면 끊겨보일 수 있어 어느정도 프레임을 만들어줘야함

2D Bone Animation이나 3D 애니메이션은 스프라이트 방식은 바로바로 바뀐다면 얘는 중간에 보간처리를 하게된다. 그렇기에 스프라이트 방식보단 부드러워보인다.

 

Animation Component

간단한 애니메이션에 쓰긴하지만 Animator 컴포넌트를 주로 쓰게 된다. 간단한 상황이 없다시피함...

 

Animator

아무것도 안하는 Idle 상태, 뛰는 Run상태 공격하는 Attack 상태 등 애니메이션엔 상태들이 있다. 애니메이터에선 그러한 상태를 정의할 수 있고 상태 간에 왔다갔다 하는 전이(transition)을 정의를 할 수가 있음 

 

변수, 파라미터에 따라 상태를 바꾼다던가 레이어를 둬서 서로 다른 여러 개의 상태를 관리한다던가 할때 쓰임

 

Animator는 Animation Controller를 사용해  복잡한 상태 기계를 구현할 수 있음

 

더보기

상태 기계

다양한 상태가 있고 하나의 상태안에서 여러개가 동시에 일어나는 일이 없는 상태가 유한 개인 FSM(Finite State Machin) 즉 유한 상태 기계라 한다.

블렌드 트리, 레이어 기능을 통해 훨씬 복잡한 고도화된 상태머신을 구현할 수 있음

 

StringToHash

 

애니메이션에 Animatior.SetBool 로 Bool 값을 바꾸는 기능이 있음

SetBool("IsDead")와 같이 Bool 값의 이름을 넣어주다보니 계속 문자열 계산을 하게 됨

 

문자열 연산은 숫자를 비교하는 것보다 연산의 비용이 크다.

 

고로 StringToHash로 통해 문자열을 고유한 정수 값인 해시 값으로 바꿔버림

 

해시 값은 고유하기에  만들어두면 Int의 경우 21억 개안에서 겹칠일이 있을까..? 거의 충돌(Collision)이 일어나지 않게 된다.

 

한 개의 정수 숫자만 비교하면 되기에 문자열을 해시로 변환하면 효율적이게 된다.

 

허나 StringToHash 함수는 일방향으로만 작동을 함 즉 문자열 -> 숫자 는 되지만 역방향인 숫자 -> 문자열은 안된다. 이런 암호화적인 특징으로 해시 값은 암호학에서도 많이 쓴다.

 

딕셔너리도 해시를 활용해 찾게하는 기법이다.

 

AnimationController

 

실은 CharacterController를 쓰게되긴 함

public class AnimationController : MonoBehaviour
{
    protected Animator animator;
    protected TopDownController controller;

    protected virtual void Awake()
    {
        animator = GetComponentInChildren<Animator>();
        controller = GetComponent<TopDownController>(); 
    }
}

 

상태, 전이를 정의할 수 있는 Animator 변수를 선언하고 TopDownController를 통해 움직이고 있는지 뭐하고 있는 지내려 받기 위해 변수 선언하고 GetComponent로 받는다 생각하면 됨.

 

발자국 나오는 부분서 애니메이션과 같이 움직일 때 편하게 하고자 Children으로 찾는다.

 

더보기

https://bugmin.tistory.com/24 를 보면 TopDownController에 대해 알 수가 있다. 

 

다시 한 번 풀어 말하면 움직이는 이동 동작에 대해서만 보면

TopDownController는 X박스 컨트롤러에 비유해서 생각해보면 X박스 컨트롤러의 껍데기를 만들어 놓은 것이다. 

이동을 위해선 컨트롤러에 이동을 위한 버튼을 만들어줘야한다. event Action으로 OnMoveEvent라는 이동을 위한 버튼을 만들어주었고 버튼을 원할 때 누를 수 있는 CallMoveEvent라는 버튼을 누르는 메서드(Action을 Invoke하는 함수)를 만들어둔다. 

 

CharacterInputController는 WASD 키보드 입력이나 발생하면 Input System에 의해 OnMove 함수가 호출되고 해당함수선 TopDownController의 이동을 위한 버튼을 누르는 CallMoveEvent 함수를 불러와 수행한다.

(TopDownController를 상속받았기에 바로 CallMoveEvent 메서드의 사용이 바로 사용이 가능하다)

 

버튼을 눌렀을 때 생기는 변화에 대해서는 TopDownMoveMent 클래스에서 Action 변수(OnMoveEvent)에 함수를 등록을 해둔 상태기에 버튼을 누른다면 OnMoveEvent가 Invoke될 시 등록된 함수들이 수행되게 된다.

CharacterAnimationController

 

AnimationController를 상속받아 만듬 Awake 함수는 virtual로 된 것을 정의를 해줘야하기에 protected override를 붙여준 것

 

Animator Hash 만드는 작업을 해보자

private static readonly int isWalking = Animator.StringToHash("isWalking");
private static readonly int isHit = Animator.StringToHash("isHit");
private static readonly int Attack = Animator.StringToHash("Attack");

 

Animator의 함수를 통해 해시값으로 만들 수가 있다. 

 

변경될 값도 아니고 나말고 볼 사람이 없기에 private static readonly 로 만들어줌

 

근데 왜 static으로 하는거지...?

더보기

객체화 시킬 때 값이 달라지는가? 그렇지 않다. 스트링 isWalking에 대한 해시 값으로만 쓸 것이기에 불변의 값이니 static을 통해 그냥 데이터 영역으로 빼둔 것이다. 만일 객체화를 여러번 시킬 경우가 생기면 힙영역이 아니라 데이터 영역에 하나만 만들어지니 용량도 적게 먹을 것이다. 불변값이면 그냥 const 대신 static readonly를 쓰자

 

private readonly float magnitudeThreshold = 0.5f;

 

위의 코드는 예를 들어 Walking을 할 때 너무 조금 움직이면 멈춘 거로 처리하고자 설정해둔 변수임. threshold는 문턱을 의미함 

0.5는 넘어야 한다는 의미다.

 

static이 없는 이유는 인스턴스마다 가지는 magnitudeThreshold 값이 다를 수 있기 때문이다.

Treshold는 많이 쓰는 단어니 알아두면 좋다.

 

    private void Start()
    {
        controller.OnAttackEvent += Attacking;
        controller.OnMoveEvent += Move;
    }

    private void Move(Vector2 vector)
    {
        animator.SetBool(isWalking, vector.magnitude > magnitudeThreshold);
    }

    private void Attacking(AttackSO sO)
    {
        animator.SetTrigger(Attack);
    }

    private void Hit()
    {
        animator.SetBool(isHit, true);
    }

    private void InvincibilityEnd()
    {
        animator.SetBool(isHit, false);
    }

 

Start 메서드

Action에 맞는 메서드들을 등록을 해둠

 

Move 메서드

아까 위에서 설명했듯이 벡터의 magnitude(크기)가 magnitudeThreshold보다 크면 true 작거나 같으면 false로 하여 threshold 값을 넘겨야만 움직이는 상태로 전이시키고자 하는 것이다.

 

Attacking 메서드

공격의 경우엔 그냥 SetTrigger로 Attack이 들어오면 바로 때린다.

 

Hit 메서드

피격부분은 없지만 만들 것이기에 Hit 함수를 만들어둠

InvincibilityEnd 함수선 isHit을 false로 한 모습이다.

 

Animation Window

 

 

윈도우에 애니메이션 창(Win: Ctrl + 6 Mac:Cmd + 6)을 띄워놓고 프로젝트 뷰서 Character라는 이름의 Animator Controller를 만든 후에 Main Sprite에 이를 달아주었다. 

 

Animation 윈도우 창에서 Create로 player_idle을 만들고 녹화버튼을 누르고 이미지들을 끌어와 넣어줌

만든 애니메이션 클립들은 애니메이터 컨트롤러와 같은 폴더 안에 넣어두는 것이 좋다.

 

 

여러개를 드래그하나 쉬프트로 선택하면 양 쪽에 바가 나오는데 이 바를 통해 일정 간격으로 배치를 할 수 있게 된다.

 

Samples가 안보이면 아래에 있는 오른쪽 맨끝에 아래에 있는 점 세개를 눌러 Show Sample Rate를 눌러야함

Sample Rate는 유니티서 초당 재생되는 애니메이션의 프레임 수다.

 

만일 60 프레임을 12프레임으로 줄이면 애니메이션 속도가 느려지게 되는데 왜일까...?

 

 

여기선 샘플레이트를 12로하여 초당 12프레임으로 설정하였는데 12로 설정하면 타임라인이 0:11 서 1:00으로 넘어가는 걸 볼 수 있는데

 

Sample Rate를 12로 조정하면 1초에 12장을 처리하는 것이고 

 

0:011초를 의미하는게 아니라 한 프레임을 의미한다. 

 

시간이 아니라 프레임이기에 1초에 12칸을 움직인다는 것이다.

 

기존의 60이라면 1초에 60칸을 이동한다는 것이다. 

칸을 초로 생각하면 60초 = 1분이 되기에 1분이 아닌가 싶지만 사실은 1분이 아니라 1초였던 것...

 

이것만 알고 가면된다. 숫자가 초가 아니라 프레임이기에 1초에 몇 칸을 움직일지를 Sample Rate로 결정한다.

 

Sample Rate 12 : 1초에 12칸 이동

Sample Rate 60: 1초에 60칸 이동

 

 

Create New Clip을 통해 여러개의 애니메이션을 만들 수 있다.

 

여기서 rgb를 설정할 수 있다.

 

player_attack 클립엔 무기 이미지인 WeaponSprite의 transform서 x 스케일 값을 줄였다 늘렸다 줄였다 조절해 트랜스폼을 조정하여 화살을 잡아당기는 효과를 넣어주었다.

 

반복되지 않는 애니메이션은 Loop Time을 꺼줘야한다.

 

Animator

 

애니메이터에선 player_Idle을 Default State로 해주고 player_attack은 플레이어가 움직이는 것은 아니기에 무기를 플레이어의 Sprite가 아닌 무기 Sprite를 건들였기에 상태가 좀 달라 다른 레이어로 빼줌

 

 

Has Exit Time으로 어떤 시점에 전이될 지 정해주는 거지만 조건이 맞을 때 바로 전이시키고 싶으면 그냥 Has Exit Time은 체크 해제하면된다.

 

바로 넘어갈 것이라면 Transition Duration도 0으로 해둔다.

 

player_hit -> player_idle은 hit 애니메이션이 다 끝난다음에 이동시킬 것이기에 1초의 Exit Time을 두어 바로 전이되지 않도록 하였다.

player_attack 은 Attack 레이어에 넣어준다.

 

참고로 Exit 상태가 되면 바로 Entry 상태로 돌아간다.

 

 

서로 별개로 같이 존재해야하는 레이어이기에 Weight(가중치)를 1로 하고 Additive Blending으로 해준다.

 

근데 Additive나 Override나 차이가 없어 보이는데.. 이는 질문으로 물어봐야할 것으로 보인다.