✅  기능

게임을 하다 보면 여러 NPC나 몹들이 특정한 행동을 취하지 않고 가만히 있거나, 앉아있는 등 Idle 상태로 있는 경우를 많이 볼 수 있습니다. 이러한 상태에서 해당 캐릭터가 항상 같은 Idle 애니메이션을 무한 반복하고 있어도 큰 문제는 없습니다. 하지만 서있는 Idle 같은 경우 가끔씩 기지개를 켜거나, 두리번거리는 등, 여러 가지의 애니메이션을 섞어서 제공하면 플레이어 입장에서는 더욱 생동감 넘치게 느낄 수 있습니다.

이번 글에서는 아주 간단하게 Idle애니메이션 등 여러 종류의 애니메이션을 랜덤으로 플레이하도록 기능을 구현하고 정리해보았습니다.

 

✅  해당 기능의 장점

1. 애니메이션의 랜덤재생을 초기에 설정한 후 더 이상 런타임 중에 접근하여 제어할 필요가 없습니다.
2. 애니메이션 각 클립의 길이를 모두 획득하여 애니메이션이 반복재생되거나 시간초에 대해 고려할 필요가 없습니다.
3. 스크립트를 등록한 후 몇개의 변수만 설정해주면 모든 세팅이 끝나 매우 간단하게 설정할 수 있습니다.

 

✅  응용 기능

이번 기능은 StateMachineBehaviour이라는 기능을 사용하여 구현하였습니다.

StateMachineBehaviour은 애니메이터의 스테이트 머신에서 사용할 수 있는 스크립트입니다.

 

상태 머신 동작 - Unity 매뉴얼

상태 머신 동작(State Machine Behaviour)은 특별 스크립트 클래스입니다. 일반 Unity 스크립트(MonoBehaviours)를 개별 게임 오브젝트에 연결하는 것과 유사한 방법으로 StateMachineBehaviour 스크립트를 상태 머

docs.unity3d.com

 

UnityEngine.StateMachineBehaviour - Unity 스크립팅 API

StateMachineBehaviour is a component that can be added to a state machine state. It's the base class every script on a state derives from.

docs.unity3d.com

 

✅  흐름도

블렌드 트리에서 스테이트 머신이 최초로 실행되면 AlreadyExecuted가 false인 상태에서 초기화를 하며 true가 됩니다. 그 후부터는 스테이트 머신이 실행이 되어도 초기화는 되지 않으며 랜덤 클립을 재생하도록 하였습니다.

 

✅ 사용 예시

 

✅ 구현

 📑 BlendTreeRandomAnimation.cs

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

public class BlendTreeRandomAnimation : StateMachineBehaviour
{
    /// <summary>
    /// 블렌드에서 사용하는 파라미터의 이름
    /// </summary>
    [Header("Parameter name used in blend tree")][SerializeField] private string mStateParameterName;

    /// <summary>
    /// 블렌드 하는 시간
    /// </summary>
    [Header("Takes to switch to another clip")][SerializeField] private float mBlendDuration = 0.5f;

    /// <summary>
    /// 각 클립들의 시간
    /// </summary>
    [Space(50)][Header("Times in the order you put the clips in")][SerializeField] float[] mClipLengths;

    /// <summary>
    /// 애니메이터 블렌더
    /// </summary>
    private AnimatorBlender mAnimBlender;

    /// <summary>
    /// 최초 1회 실행하기위해 구분
    /// </summary>
    bool mIsAlreadyExecuted = false;

    /// <summary>
    /// 현재 딜레이 시간
    /// </summary>
    private float mCurrentDelay;

    /// <summary>
    /// 현재 재생중인 클립의 인덱스 번호
    /// </summary>
    private int mCurrentClipIndex;

    private void RefreshClip()
    {
        mCurrentClipIndex = Random.Range(0, mClipLengths.Length);
        mCurrentDelay = mClipLengths[mCurrentClipIndex];
    }

    private void PlayUpdatedClip(Animator animator)
    {
        RefreshClip();

        mAnimBlender.BlendLerp(animator, mStateParameterName, mCurrentClipIndex, mBlendDuration);
    }

    // OnStateEnter is called when a transition starts and the state machine starts to evaluate this state
    override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        //최초 한번만 실행하도록 한다
        if (mIsAlreadyExecuted) { return; }

        //애니메이터블렌더 찾기
        mAnimBlender = animator.GetComponent<AnimatorBlender>();    

        //클립 재조정
        RefreshClip();

        //실행 완료
        mIsAlreadyExecuted = true;
    }

    // OnStateUpdate is called on each Update frame between OnStateEnter and OnStateExit callbacks
    override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    {
        mCurrentDelay -= Time.deltaTime;
        if (mCurrentDelay < 0f) { PlayUpdatedClip(animator); }
    }


    // OnStateExit is called when a transition ends and the state machine finishes evaluating this state
    //override public void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    //{
    //    
    //}

    // OnStateMove is called right after Animator.OnAnimatorMove()
    //override public void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    //{
    //    // Implement code that processes and affects root motion
    //}

    // OnStateIK is called right after Animator.OnAnimatorIK()
    //override public void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
    //{
    //    // Implement code that sets up animation IK (inverse kinematics)
    //}
}
[SerializeField] private string mStateParameterName;
  • 블렌드 트리에서 사용할 파라미터의 이름입니다.

 

[SerializeField] float[] mClipLengths;
  • 블렌드 트리에 들어가있는 애니메이션 클립들의 길이를 넣는 배열입니다.
  • 인스펙터에서 각 클립의 길이를 넣으면 됩니다.

 

override public void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
  • StateMachineBehaviour을 생성하면 자동으로 오버 로딩되는 함수들 중 하나입니다. 해당 스테이트 머신이 실행되면 이 함수가 실행됩니다. 내부 구현에서 mIsAlreadyExecuted를 사용하여 mIsAlreadyExecuted가 false(기본값) 일 때만 최초 한번 실행되어 변수들이 초기화됩니다.

 

override public void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
  • StateMachineBehaviour을 생성하면 자동으로 오버 로딩되는 함수들 중 하나입니다.
  • 스테이트 머신이 플레이 중일 때 매 프레임마다 호출되며 이 함수에서는 현재 남은 시간을 체크하며 남은 시간이 0보다 작아질 경우(끝난 경우) 다른 애니메이션으로 재생하도록 하였습니다.

 

private void RefreshClip()
  • 재생할 다음 클립을 무작위로 설정하고, 설정된 클립의 길이를 시간초에 저장해둡니다.

 

private void PlayUpdatedClip(Animator animator)
  • 클립을 업데이트합니다. 애니메이터 블렌더 클래스에 접근하여 코루틴을 사용하여 애니메이션을 블렌드 하도록 합니다.
  • StateMachineBehaviour를 상속받은 클래스에서는 코루틴을 실행할 수 없어 해당 오브젝트에 MonoBehaviour를 상속받은 클래스(AnimatorBlender)를 생성하여 코루틴을 실행할 수 있도록 했습니다.

 

 📑 AnimatorBlender.cs

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

public class AnimatorBlender : MonoBehaviour
{
    /// <summary>
    /// 현재 클립에서 다른 클립으로 블렌드
    /// </summary>
    /// <param name="animator">애니메이션 컨트롤러</param>
    /// <param name="parameterName">블렌드 트리에서 사용하는 파라미터 이름</param>
    /// <param name="toAnimState">설정한 애니메이션 스테이트</param>
    /// <param name="duration">몇초동안 블렌드를 할지 설정, -1인경우에는 블렌드하지않고 즉시 toAnimState로 설정</param>
    public void BlendLerp(Animator animator, string parameterName, float toAnimState, float duration)
    {
        if(duration == -1) 
        { 
            animator.SetFloat(parameterName, toAnimState);
            return; 
        }

        StartCoroutine(SetState(animator, parameterName, toAnimState, duration));
    }

    /// <summary>
    /// 코루틴을 사용하여 블렌드
    /// </summary>
    private IEnumerator SetState(Animator animator, string parameterName, float toAnimState, float duration)
    {
        float process = 0;
        float currentState = animator.GetFloat(parameterName);

        while (true)
        {
            animator.SetFloat(parameterName, Mathf.Lerp(currentState, toAnimState, process));

            process += Time.deltaTime / duration;

            if (process > 1.0f) 
            { 
                animator.SetFloat(parameterName, toAnimState);
                yield break; 
            }
            yield return null;
        }
    }
}
 
public void BlendLerp(Animator animator, string parameterName, float toAnimState, float duration)
  • 애니메이션의 클립을 블렌드 하기 위해 코루틴을 사용할 때 필요한 MonoBehaviour을 상속받은 클래스입니다.
  • BlendTreeRandomAnimation에서 호출되며 코루틴을 실행합니다.
  • duration이 -1인 경우에는 블렌드 하지 않고 즉시 다음 클립으로 설정합니다.

 

✅ 사용

  • 블렌드 트리가 있는 스테이트 머신에 BlendTreeRandomAnimation를 추가합니다.
  • 블렌드 트리에서 사용할 파라미터 변수 이름을 지정합니다. (변수 이름은 무조건 파라미터 이름이어야 합니다)
  • 블렌드 시간은 원하는대로 설정합니다. -1인경우에는 블렌드하지않고 즉시 다른 클립으로 전환됩니다.
  • mClipLength 배열에 자신이 넣을 클립의 각 시간만큼 순서에 맞게 시간을 적어줍니다.

 

  • 블렌드 트리에 블렌드 하고 싶은 애니메이션들을 넣습니다.
  • Threshold는 각각 1의 배수로 설정합니다. 인덱스 번호와 일치시키기 위함입니다.

애니메이션 클립을 원하는만큼 넣는다

 

  • 코루틴을 사용하여 블렌드를 하기 위해 AnimatorBlender.cs를 캐릭터 오브젝트에 추가합니다.
  • 애니메이터가 있는 오브젝트에 추가해야 합니다.

 

  • 플레이하여 블렌드가 제대로 적용되는지 확인합니다.

 

✅ 도움받은 곳(Reference)

해당 기능을 생각하고 있었지만, 기존에 구현되어있던 것들 중 제 기준에 만족스러운 구현되어있는 것을 찾을 수 없었습니다. 여러 레퍼런스를 참고하여 구현했습니다.

 

How can i access blend tree in animator controller from script ? - Unity Answers

 

answers.unity.com

 

 

Getting AnimatorController from Animator

Hello, In attempts to improve workflow, and stop inputting of incorrect Data I wanted to create a dropdown list of all the (trigger) parameters for...

forum.unity.com

 

bonnate