본문 바로가기
Utils

Unity, Animator 상태 변환 완료 때까지 기다리기 (확장함수)

by Client. DJ 2022. 2. 23.
반응형

애니메이션 클립 이벤트키

애니메이터 사용 중, 애니메이터 상태 변환 완료 이후 시점에 처리가 필요한 경우가 있습니다. 아래의 스크립트를 추가하면, 확장 함수로 편하게 사용 가능합니다.

우리가 필요로 하는 기능

animator.SetTrigger("Die");

캐릭터가 죽고 나서 보여지는 애니메이션 이후, 해당 상태가 완전히 끝나고 나서 처리가 필요하지만, 이는 유니티에서 지원하지 않습니다. 보통은 AnimationEvent 달아 주어 처리를 하거나, 특정을 직접 입력하여 처리를 합니다.

animator.SetTrigger("Die", () => 
{
	// 스테이트가 종료된 이후 호출되는 구간
	this.gameObject.SetActive(false);
});

위와 같이, 애니메이션 종료 이후 타이밍을 얻게 되며, 추가 적인 처리가 가능해집니다.

스크립트

AnimatorEventReceiever.cs

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

public static class AnimatorEventExtension
{
    private static AnimatorEventReceiever AttachReceiever(ref Animator animator)
    {
        AnimatorEventReceiever receiever = animator.gameObject.GetComponent<AnimatorEventReceiever>();
        if (receiever == null) receiever = animator.gameObject.AddComponent<AnimatorEventReceiever>();
        return receiever;
    }

    public static void SetInteger(this Animator animator, string name, int value, Action onFinished)
    {
        animator.SetInteger(name, value);
        AttachReceiever(ref animator).OnStateEnd(onFinished);
    }

    public static void SetInteger(this Animator animator, int id, int value, Action onFinished)
    {
        animator.SetInteger(id, value);
        AttachReceiever(ref animator).OnStateEnd(onFinished);
    }

    public static void SetFloat(this Animator animator, string name, float value, Action onFinished)
    {
        animator.SetFloat(name, value);
        AttachReceiever(ref animator).OnStateEnd(onFinished);
    }

    public static void SetFloat(this Animator animator, int id, float value, Action onFinished)
    {
        animator.SetFloat(id, value);
        AttachReceiever(ref animator).OnStateEnd(onFinished);
    }

    public static void SetBool(this Animator animator, string name, bool value, Action onFinished)
    {
        animator.SetBool(name, value);
        AttachReceiever(ref animator).OnStateEnd(onFinished);
    }

    public static void SetBool(this Animator animator, int id, bool value, Action onFinished)
    {
        animator.SetBool(id, value);
        AttachReceiever(ref animator).OnStateEnd(onFinished);
    }

    public static void SetTrigger(this Animator animator, string name, Action onFinished)
    {
        animator.SetTrigger(name);
        AttachReceiever(ref animator).OnStateEnd(onFinished);
    }

    public static void SetTrigger(this Animator animator, int hashid, Action onFinished)
    {
        animator.SetTrigger(hashid);
        AttachReceiever(ref animator).OnStateEnd(onFinished);
    }
}

[RequireComponent(typeof(Animator))]
public class AnimatorEventReceiever : MonoBehaviour
{
    #region Inspector

    public List<AnimationClip> animationClips = new List<AnimationClip>();

    #endregion

    private Animator _animator = null;
    private Dictionary<string, List<Action>> _startEvnets = new Dictionary<string, List<Action>>();
    private Dictionary<string, List<Action>> _endEvents = new Dictionary<string, List<Action>>();

    private Coroutine _coroutine = null;
    private bool _isPlayingAnimator = false;

    public void OnStateEnd(Action onFinished)
    {
        if (_coroutine != null)
            StopCoroutine(_coroutine);
        _coroutine = StartCoroutine(OnStateEndCheck(onFinished));
    }

    public IEnumerator OnStateEndCheck(Action onFinished)
    {
        _isPlayingAnimator = true;
        while (true)
        {
            yield return new WaitForEndOfFrame();
            if (!_isPlayingAnimator)
            {
                // 다음 애니메이션 클립이 재생되는지 1프레임 더 기다림
                yield return new WaitForEndOfFrame();
                if (!_isPlayingAnimator) break;
            }
        }
        onFinished?.Invoke();
    }

    private void Awake()
    {
        // 애니메이터 내에 있는 모든 애니메이션 클립의 시작과 끝에 이벤트를 생성한다.
        _animator = GetComponent<Animator>();
        for (int i = 0; i < _animator.runtimeAnimatorController.animationClips.Length; i++)
        {
            AnimationClip clip = _animator.runtimeAnimatorController.animationClips[i];
            animationClips.Add(clip);

            AnimationEvent animationStartEvent = new AnimationEvent();
            animationStartEvent.time = 0;
            animationStartEvent.functionName = "AnimationStartHandler";
            animationStartEvent.stringParameter = clip.name;
            clip.AddEvent(animationStartEvent);

            AnimationEvent animationEndEvent = new AnimationEvent();
            animationEndEvent.time = clip.length;
            animationEndEvent.functionName = "AnimationEndHandler";
            animationEndEvent.stringParameter = clip.name;
            clip.AddEvent(animationEndEvent);
        }
    }

    /// <summary>
    /// 각 클립 별 시작 이벤트
    /// </summary>
    /// <param name="name"></param>
    private void AnimationStartHandler(string name)
    {
        if (_startEvnets.TryGetValue(name, out var actions))
        {
            for (int i = 0; i < actions.Count; i++)
            {
                actions[i]?.Invoke();
            }
            actions.Clear();
        }
        _isPlayingAnimator = true;
    }

    /// <summary>
    /// 클립 별 종료 이벤트
    /// </summary>
    /// <param name="name"></param>
    private void AnimationEndHandler(string name)
    {
        if (_endEvents.TryGetValue(name, out var actions))
        {
            for (int i = 0; i < actions.Count; i++)
            {
                actions[i]?.Invoke();
            }
            actions.Clear();
        }
        _isPlayingAnimator = false;
    }
}

예제

animator.SetTrigger("Trigger Parameter Name", () =>
{
	// 애니메이터 상태 변환 완료 이후 처리
});

위와 같이 사용하면, 기존 함수와 동일한 방법에서 확장함수로 언제든 사용 가능합니다. 주의하실 점으로는 Loop되고 있는 애니메이션 클립에 걸리면 이벤트를 돌려주지 않습니다. 무한으로 돌고 있으니 애니메이터 상태 변화는 계속 일어나고 있으므로 안 주는 부분 인지바랍니다.

호출 시, 인스턴스하게 생성되는 컴포넌트와 이벤트 키

확장 함수 사용 시, Animator 컴포넌트가 있는 오브젝트와 클립에서는 아래와 같은 변화가 일어납니다.

1. 컴포넌트를 붙인다.

애니메이터 이벤트 리시버가 붙는다.

2. 각 애니메이션 클립에 이벤트키 생성

리시버가 붙음과 동시에 각 애니메이션 처음과 끝에 이벤트키 생성

반응형

댓글