본문으로 건너뛰기

StateMachineBehaviour에서 Animator를 감시하자

· 약 8분
karais89

환경

  • macOS Mojave v10.14.6
  • Unity 2019.2.5f1
  • Github Desktop
  • Rider 2019.2
  • UniRx v7.1.0

원문 : https://qiita.com/toRisouP/items/b6540b7f514d18b9a426

이 포스팅은 원문을 단순히 구글 번역을 하여 정리한 내용입니다. 일본어를 잘하시는 분은 원문을 보시는게 더 좋으실 것 같습니다.

UniRx에 대한 기사 요약은 여기


StateMachineBehaviour는

Unity5 이상 버전 부터 AnimatorController에 붙일 수 있는 StateMachineBehaviour 스크립트가 추가되었습니다.

이 StateMachineBehaviour는 Animator 상태 머신의 상태 변화에 맞게 CallBack 함수를 실행해주는 방식으로 구성되어 있습니다.

StateMachineBehaviour 샘플

using UnityEngine;

public class StateMachineExample : StateMachineBehaviour
{
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// 새로운 상태로 변할 때 실행
}

public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// 처음과 마지막 프레임을 제외한 각 프레임 단위로 실행
}

public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// 상태가 다음 상태로 바뀌기 직전에 실행
}

public override void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// MonoBehaviour.OnAnimatorMove 직후에 실행
}

public override void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
// MonoBehaviour.OnAnimatorIK 직후에 실행
}

public override void OnStateMachineEnter(Animator animator, int stateMachinePathHash)
{
// 스크립트가 부착된 상태 기계로 전환이 왔을때 실행
}

public override void OnStateMachineExit(Animator animator, int stateMachinePathHash)
{
// 스크립트가 부착된 상태 기계에서 빠져나올때 실행
}
}

StateMachineBehaviour 사용

StateMachineBehaviour는 보통의 Component와 달리 Animator 레이어에 붙여 사용합니다.

일반 MonoBehaviour 스크립트에서 호출할때는 아래와 같은 방법으로 Animator의 GetBehaviour을 호출하여 가져 옵니다.

StateMachineBehaviour의 취득 방법

private Animator _animator;
private StateMachineExample _stateMachineExample;

void Start()
{
_animator = GetComponent<Animator>();
_stateMachineExample = _animator.GetBehaviour<StateMachineExample>();
}

(여담) UniRx를 사용하여 Observable로 상태 변화를 모니터링 할 수 있도록 했다

콜백 그대로 사용하는 것은 조금 불편한 부분이 있어서, UniRx를 사용하여 콜백을 Observable로 변환 해서 사용 합니다.

using System;
using UniRx;
using UnityEngine;

public class StateMachineObservables : StateMachineBehaviour
{
#region OnStateEnter
private Subject<AnimatorStateInfo> onStateEnterSubject = new Subject<AnimatorStateInfo>();

public IObservable<AnimatorStateInfo> OnStateEnterObservable => onStateEnterSubject;

public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
onStateEnterSubject.OnNext(stateInfo);
}
#endregion

#region OnStateExit
private Subject<AnimatorStateInfo> onStateExitSubject = new Subject<AnimatorStateInfo>();

public IObservable<AnimatorStateInfo> OnStateExitObservable => onStateExitSubject;

public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
onStateExitSubject.OnNext(stateInfo);
}
#endregion

#region OnStateMachineEnter
private Subject<int> onStateMachineEnterSubject = new Subject<int>();

public IObservable<int> OnStateMachineEnterObservable => onStateMachineEnterSubject;

public override void OnStateMachineEnter(Animator animator, int stateMachinePathHash)
{
onStateMachineEnterSubject.OnNext(stateMachinePathHash);
}
#endregion

#region OnStateMachineExit
private Subject<int> onStateMachineExitSubject = new Subject<int>();

public IObservable<int> OnStateMachineExitObservable => onStateMachineExitSubject;

public override void OnStateMachineExit(Animator animator, int stateMachinePathHash)
{
onStateMachineExitSubject.OnNext(stateMachinePathHash);
}
#endregion

#region OnStateMove
private Subject<AnimatorStateInfo> onStateMoveSubject = new Subject<AnimatorStateInfo>();

public IObservable<AnimatorStateInfo> OnStateMoveObservable => onStateMoveSubject;

public override void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
onStateMoveSubject.OnNext(stateInfo);
}
#endregion

#region OnStateIK
private Subject<AnimatorStateInfo> onStateIKSubject = new Subject<AnimatorStateInfo>();

public IObservable<AnimatorStateInfo> OnStateIKObservable => onStateIKSubject;

public override void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
onStateIKSubject.OnNext(stateInfo);
}
#endregion
}

이후 아래와 같이 사용하고 싶은 Observable을 취득해서 Subscribe 하면 사용할 수 있습니다.

Observable로 스테이트 변화를 감지한후 shortNameHash를 출력하는 예제

private Animator _animator;
private StateMachineObservables _stateMachineObservables;

private void Start()
{
_animator = GetComponent<Animator>();
_stateMachineObservables = _animator.GetBehaviour<StateMachineObservables>();

// 시작한 애니메이션의 shortNameHash를 보여준다.
_stateMachineObservables
.OnStateEnterObservable
.Subscribe(stateInfo => Debug.Log(stateInfo.shortNameHash));
}

UniRx에서 상태 변화를 제어해 보자

"Idle 애니메이션이 5초 이상 재생되면 Rest 상태로 변경 되는 로직"

은 상태 변화를 방금 전의 StateMachineObservables를 사용하면 이런 식으로 작성할 수 있습니다.

private Animator _animator;
private StateMachineObservables _stateMachineObservables;

private void Start()
{
_animator = GetComponent<Animator>();
_stateMachineObservables = _animator.GetBehaviour<StateMachineObservables>();

_stateMachineObservables
.OnStateEnterObservable // 상태 변화를 감지
.Throttle(TimeSpan.FromSeconds(5)) // 마지막 상태 변화 후 5초 경과했을 때
.Where(x => x.IsName("Base Layer.Idle")) // 현재 재싱중인 애니메이션이 Base Layer의 Idle이라면
.Subscribe(_ => _animator.SetBool("Rest", true)); // Animator의 Rest 파라미터를 True로 한다.
}

Idle (기본) 상태에서 5 초 가 지나면 Rest (휴식) 모션을 재생하도록 할 수 있었습니다.


역주: 추가내용 (19.09.28)

UniRx가 업데이트 되면서, 기본적으로 제공되는 Trigger 클래스들이 추가되고, 값의 변화를 감지하는 부분을 제공되는 클래스들을 사용하여 구현할 수 있게 되었습니다. ObservableStateMachineTrigger 스크립트를 사용하면 위 내용과 동일한 내용을 클래스 구현 없이 사용 가능 합니다.

// Unity5 버전 이상부터 사용 가능하다.
#if !(UNITY_4_7 || UNITY_4_6 || UNITY_4_5 || UNITY_4_4 || UNITY_4_3 || UNITY_4_2 || UNITY_4_1 || UNITY_4_0_1 || UNITY_4_0 || UNITY_3_5 || UNITY_3_4 || UNITY_3_3 || UNITY_3_2 || UNITY_3_1 || UNITY_3_0_0 || UNITY_3_0 || UNITY_2_6_1 || UNITY_2_6)

using System; // 윈도우 유니버셜 앱에서 요구된다.
using UnityEngine;

namespace UniRx.Triggers
{
[DisallowMultipleComponent]
public class ObservableStateMachineTrigger : StateMachineBehaviour
{
public class OnStateInfo
{
public Animator Animator { get; private set; }
public AnimatorStateInfo StateInfo { get; private set; }
public int LayerIndex { get; private set; }

public OnStateInfo(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
Animator = animator;
StateInfo = stateInfo;
LayerIndex = layerIndex;
}
}

public class OnStateMachineInfo
{
public Animator Animator { get; private set; }
public int StateMachinePathHash { get; private set; }

public OnStateMachineInfo(Animator animator, int stateMachinePathHash)
{
Animator = animator;
StateMachinePathHash = stateMachinePathHash;
}
}

// OnStateExit

Subject<OnStateInfo> onStateExit;

public override void OnStateExit(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (onStateExit != null) onStateExit.OnNext(new OnStateInfo(animator, stateInfo, layerIndex));
}

public IObservable<OnStateInfo> OnStateExitAsObservable()
{
return onStateExit ?? (onStateExit = new Subject<OnStateInfo>());
}

// OnStateEnter

Subject<OnStateInfo> onStateEnter;

public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (onStateEnter != null) onStateEnter.OnNext(new OnStateInfo(animator, stateInfo, layerIndex));
}

public IObservable<OnStateInfo> OnStateEnterAsObservable()
{
return onStateEnter ?? (onStateEnter = new Subject<OnStateInfo>());
}

// OnStateIK

Subject<OnStateInfo> onStateIK;

public override void OnStateIK(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if(onStateIK !=null) onStateIK.OnNext(new OnStateInfo(animator, stateInfo, layerIndex));
}

public IObservable<OnStateInfo> OnStateIKAsObservable()
{
return onStateIK ?? (onStateIK = new Subject<OnStateInfo>());
}

// OnStateMove에 영향을 미치지 않습니다.
// ObservableStateMachine Trigger는 애니메이션을 중지 시킵니다.
// OnAnimatorMove를 정의함으로써 루트 객체의 움직임을 가로 채서 직접 적용하고 싶다는 의미입니다.
// http://fogbugz.unity3d.com/default.asp?700990_9jqaim4ev33i8e9h

//// OnStateMove

//Subject<OnStateInfo> onStateMove;

//public override void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
//{
// if (onStateMove != null) onStateMove.OnNext(new OnStateInfo(animator, stateInfo, layerIndex));
//}

//public IObservable<OnStateInfo> OnStateMoveAsObservable()
//{
// return onStateMove ?? (onStateMove = new Subject<OnStateInfo>());
//}

// OnStateUpdate

Subject<OnStateInfo> onStateUpdate;

public override void OnStateUpdate(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (onStateUpdate != null) onStateUpdate.OnNext(new OnStateInfo(animator, stateInfo, layerIndex));
}

public IObservable<OnStateInfo> OnStateUpdateAsObservable()
{
return onStateUpdate ?? (onStateUpdate = new Subject<OnStateInfo>());
}

// OnStateMachineEnter

Subject<OnStateMachineInfo> onStateMachineEnter;

public override void OnStateMachineEnter(Animator animator, int stateMachinePathHash)
{
if (onStateMachineEnter != null) onStateMachineEnter.OnNext(new OnStateMachineInfo(animator, stateMachinePathHash));
}

public IObservable<OnStateMachineInfo> OnStateMachineEnterAsObservable()
{
return onStateMachineEnter ?? (onStateMachineEnter = new Subject<OnStateMachineInfo>());
}

// OnStateMachineExit

Subject<OnStateMachineInfo> onStateMachineExit;

public override void OnStateMachineExit(Animator animator, int stateMachinePathHash)
{
if (onStateMachineExit != null) onStateMachineExit.OnNext(new OnStateMachineInfo(animator, stateMachinePathHash));
}

public IObservable<OnStateMachineInfo> OnStateMachineExitAsObservable()
{
return onStateMachineExit ?? (onStateMachineExit = new Subject<OnStateMachineInfo>());
}
}
}

#endif

참고


사용한 것