본문으로 건너뛰기

· 약 6분
karais89

환경

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

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

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

UniRx에 대한 기사 요약은 여기


UniRx 최신 버전부터 ThrottleFirst가 추가 되었습니다! (UniRx 4.8.1 이상 버전)

역주: ThrottleFirstFrame도 4.8.1 이상 버전에서 추가 되었습니다!

게임 개발에서 "어떤 처리를 하고 나서 한동안은 이벤트를 무시하고 싶다", "이벤트가 많이 왔을 때 처음만 처리하고 나머지는 당분간 무시하고 싶다"는 수요는 많을 것이라고 생각합니다.

(예를 들어 "버튼을 길게 눌러 이벤트를 300밀리 초 간격으로 솎아 내고 싶다", "마지막으로 애니메이션 이벤트가 오고 나서 3초 동안 이벤트를 무시하고 싶다" 등)

기존의 Rx의 오퍼레이터 조합으로도 구현 할 수 있지만, 자주 사용되는 만큼 전용 오퍼레이터를 가지고 싶었습니다. 그래서 Throttle를 바탕을 하여 제작 하였습니다.

RxJS 등에는 "ThrottleFirst"라는 이름으로 같은 동작을 하는 오퍼레이터가 존재하고 있었습니다.

이미지는 ReactiveX에서 인용

UniRx에서 ThrottleFirst

using System;
namespace UniRx
{
public static partial class Observable
{
public static IObservable<TSource> ThrottleFirst<TSource>(this IObservable<TSource> source, TimeSpan dueTime)
{
return source.ThrottleFirst(dueTime, Scheduler.DefaultSchedulers.TimeBasedOperations);
}

public static IObservable<TSource> ThrottleFirst<TSource>(this IObservable<TSource> source, TimeSpan dueTime, IScheduler scheduler)
{
return new AnonymousObservable<TSource>(observer =>
{
var gate = new object();
var open = true;
var cancelable = new SerialDisposable();

var subscription = source.Subscribe(x =>
{
lock (gate)
{
if (!open) return;
observer.OnNext(x);
open = false;
}

var d = new SingleAssignmentDisposable();
cancelable.Disposable = d;
d.Disposable = scheduler.Schedule(dueTime, () =>
{
lock (gate)
{
open = true;
}
});

},
exception =>
{
cancelable.Dispose();

lock (gate)
{
observer.OnError(exception);
}
},
() =>
{
cancelable.Dispose();

lock (gate)
{
observer.OnCompleted();

}
});

return new CompositeDisposable(subscription, cancelable);
});
}

public static IObservable<TSource> ThrottleFirstFrame<TSource>(this IObservable<TSource> source, int frameCount,
FrameCountType frameCountType = FrameCountType.Update)
{
return new AnonymousObservable<TSource>(observer =>
{
var gate = new object();
var open = true;
var cancelable = new SerialDisposable();

var subscription = source.Subscribe(x =>
{
lock (gate)
{
if (!open) return;
observer.OnNext(x);
open = false;
}

var d = new SingleAssignmentDisposable();
cancelable.Disposable = d;

d.Disposable = Observable.TimerFrame(frameCount, frameCountType)
.Subscribe(_ =>
{
lock (gate)
{
open = true;
}
});
},
exception =>
{
cancelable.Dispose();

lock (gate)
{
observer.OnError(exception);
}
},
() =>
{
cancelable.Dispose();

lock (gate)
{
observer.OnCompleted();

}
});

return new CompositeDisposable(subscription, cancelable);
});
}
}
}
  • ThrottleFirst (OnNext를 무시하는 시간)
  • ThrottleFirstFrame (OnNext를 무시하는 프레임 수)

둘 다 첫 번째는 반드시 OnNext를 통과 시킵니다.

2 번째 이후의 OnNext 내용은 마지막으로 OnNext를 통과시킨 후 일정 시간 경과 할 때까지 OnNext를 흘리지 않고 버리도록 처리하고 있습니다.

예를 들어 ThrottleFirst(TimeSpan.FromSeconds(1)) 와 같은 지정 방법을 한 경우에는 메시지가 1초 이내에 연속 해 온 경우는 처음에만 통과하고 그 이외는 무시하게 됩니다.

역주

AnonymousObservable 클래스에 대한 정체가 궁금하다. (C# Rx에서 기본적으로 제공해주는 클래스를 UniRx 상에서도 사용할 수 있게 구현된 클래스 인 것 같다. 익명 함수 개념과 비슷한 느낌이라고 생각하면 될 것 같다. Observable은 만들고 싶지만, 이름은 지어 주고 싶지 않은 경우 사용하는 용도)

UniRx 5.0 이상 버전 부터는 오퍼레이터를 생성하는 구조가 변함. 위 클래스를 사용하지 않고, 각각의 오퍼레이터에 해당되는 Observable 클래스를 따로 만드는 구조 (OperatorObservableBase를 상속 받고, 필요한 내용들을 구현 하는 방식) 개인적으로는 바뀐 구조가 더 깔끔한 느낌이 든다.

사용 사례

클릭 되고 나서 5초 동안 클릭을 무시하는 예제

this.UpdateAsObservable()
.Where(_=>Input.GetMouseButtonDown(0))
.ThrottleFirst(TimeSpan.FromSeconds(5))
.Subscribe(x => Debug.Log("Clicked!"));

업데이트를 1/10로 솎아 내는 예제 (9회 Update가 올 때까지 무시)

this.UpdateAsObservable()
.ThrottleFirstFrame(9)
.Subscribe(x => Debug.Log("tenth part Update"));

· 약 4분
karais89

환경

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

원문 : https://qiita.com/toRisouP/items/35b4d6255c1f9ecf27d1

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

UniRx에 대한 기사 요약은 여기


Subscribe 및 Dispose

Rx에서는 Observable를 구독하고 메시지를 대기하는 것을 Subscribe라고 부르고, 그 구독을 중지하는 것을 Disponse라고 부릅니다.

이 Subscribe는 조심해야 되는 부분이 있습니다. Observable이 파괴된 타이밍에 제대로 Dispose 할 필요가 있습니다.

Observable이 파괴될 때에 OnCompleted가 발행되면 자동으로 Dispose가 됩니다만, 스트림에 따라서는 OnCompleted가 발행되지 않는 경우도 있습니다.

Observable의 수명을 고려하지 않고 사용한 경우

우선 다음 코드와 실행 결과를 봅시다.

using System;
using UniRx;
using UnityEngine;

public class ObservableLifeTime : MonoBehaviour
{
private void Start()
{
// 1 초마다 메시지를 발행하는 Observable
Observable.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1))
.Subscribe(x => Debug.Log(x));

// 3 초 후에 GameObject를 제거한다
Invoke("DestroyGameObject", 3);
}

/// <summary>
/// 로그를 출력하고 오브젝트를 제거한다.
/// </summary>
private void DestroyGameObject()
{
Debug.Log("Destroy");
Destroy(gameObject);
}
}

실행 결과

Destroy에서 GameObject가 파괴 된 후에도, Observable.Timer로 만든 스트림은 계속 실행되고 있습니다.

이것은 Observable.Timer로 만든 Observable가 static으로 생성되어 버려, GameObject에 관계없이 독립적으로 작동하기 때문 입니다.

AddTo로 Subscribe와 GameObject를 연결한다.

AddTo를 사용하면 이 문제를 쉽게 해결할 수 있습니다.

using System;
using UniRx;
using UnityEngine;

public class ObservableLifeTime : MonoBehaviour
{
private void Start()
{
// 1 초마다 메시지를 발행하는 Observable
Observable.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1))
.Subscribe(x => Debug.Log(x))
.AddTo(gameObject); // GameObject의 수명과 연결.

// 3 초 후에 GameObject를 제거한다
Invoke("DestroyGameObject", 3);
}

/// <summary>
/// 로그를 출력하고 오브젝트를 제거한다.
/// </summary>
private void DestroyGameObject()
{
Debug.Log("Destroy");
Destroy(gameObject);
}
}

실행 결과

AddTo를 사용하는 것으로, Subscribe의 Dispose를 지정한 GameObject의 수명에 연결시켜, GameObject가 Destroy시에 자동으로 Dispose 해주게 되었습니다.

이제 Dispose 관리를 신경 쓰지 않고 팩토리 메소드를 사용할 수 있게 되었습니다! (UniRx 내부적으로 Timer의 경우 new TimeObservable 형태의 팩토리 메서드 패턴 형태로 구현되어 있습니다)

다만, OnCompleted가 실행되는 것은 아니기 때문에 그 점은 조심하시기 바랍니다.

· 약 6분
karais89

환경

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

원문 : https://qiita.com/toRisouP/items/972b97367df12c3457d2

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

UniRx에 대한 기사 요약은 여기


UniRx를 사용하다보면 Update()를 Observable로 변환하여 사용하는 경우가 많습니다.

이번에는 그 Update를 Observable로 변환하는 방법을 소개하고 싶습니다.

주의

본 포스트보다 더 많은 내용이 상세하게 작성되어 있는 포스트가 있으므로 여기를 참조하십시오.

UniRx 입문 4 - Update를 스트림으로 변환하는 방법 및 장점 -

이하 오래된 설명

Update → Observable로 변환하기

Update를 Observable로 변환하는 방법은 2015/04/10 시점에서 3종류가 있습니다.

  • ObservableMonoBehaviour & UpdateAsObservable
  • ObservableUpdateTrigger & UpdateAsObservable
  • Observable.EveryUpdate

각각 미묘하게 다르므로 사용하는데 주의가 필요합니다.

ObservableMonoBehaviour & UpdateAsObservable

[주의] UniRx 4.8 이후 버전 부터는 ObservableMonoBehaviour의 사용이 비추천이 되었습니다.

역주: UniRx 6.0.0 이후 버전 부터는 ObservableMonoBehaviour 스크립트 자체가 제거 되었습니다.

public class UniRxSample : ObservableMonoBehaviour
{
public override void Start()
{
base.Start();

UpdateAsObservable()
.Subscribe(_ => Debug.Log("Update!"));
}

public override void Update()
{
// Update를 override하면 base.Update ()의 호출을 잊지
base.Update();
}
}

특징

  • ObservableMonoBehaviour는 MonoBehaviour 파생 클래스에서 상속하여 사용할 필요가 있다.
  • UpdateAsObservable()는 Update 타이밍에 통지된다.
  • Component가 삭제되면 자동으로 Dispose가 호출된다.
  • 스트림은 IObservable<Unit> 형태이다.
  • ObservableMonoBehaviour에는 Update 이외의 통지도 준비되어 있으므로 편리하다.
  • Script Execution Order에서 다른 스트립트와 실행 순서를 세세하게 지정할 수 있다.

설명

UpdateAsObservable는 내부적으로 Subject를 가지고 있으며, Update()시 OnNext를 호출하는 단순한 구조로되어 있습니다. (해당란)

따라서 Update를 Override할 때 base.Update()의 호출을 제대로 추가해주지 않으면, UpdateAsObservable()는 작동하지 않기 때문에 주의가 필요합니다.

또한 ObservableMonoBehaviour는 다양한 이벤트 트리거가 포함되어 있으며 이를 상속 하면 편하게 사용이 가능합니다. 하지만 ObservableMonoBehaviour 상속할 수 없는 장면이나 의도적으로 상속하지 않으려는 경우에는 사용할 수 없습니다.

[추기]

제작자 @neuecc님이 말하길 ObservableMonoBehaviour을 사용하는 것은 비추천이며, UniRx 4.8 이상에서는 지원되지 않습니다.

대신 아래의 ObservableUpdateTrigger를 사용 합시다.

ObservableUpdateTrigger

using UniRx.Triggers;
using UniRx;
using UnityEngine;

public class UniRxSample : MonoBehaviour
{
private void Start()
{
this.UpdateAsObservable()
.Subscribe(_ => Debug.Log("Update!"));
}
}

특징

  • ObservableUpdateTrigger는 UniRx.Triggers 네임스페이스에 정의되어 있습니다.
  • UniRx.Triggers를 Using에 선언해두면 UpdateAsObservable()를 직접 호출할 수 있습니다.
  • 실제 동작은 ObservableUpdateTrigger에 있습니다.
  • 호출시에 내부적으로 ObservableUpdateTrigger가 AddComponent 됩니다. (실제로 사용할 때는 Trigger의 존재는 신경 쓰지 않아도 됩니다.)
  • ObservableMonoBeaviour과 내부 구조는 동일 합니다.

ObservableUpdateTrigger는 이벤트 트리거마다 분할한 것 중 1개 입니다.

ObservableMonoBehaviour은 상속이 필요했지만, 이쪽은 단지 Using을 추가 할 뿐이므로 사용성이 높습니다.

Observable.EveryUpdate

Observable
.EveryUpdate()
.Subscribe(x => Debug.Log(x));

특징

  • static 메서드로 정의되어 있기 때문에 MonoBehaviour 이외의 장소에서 사용할 수 있다.
  • 스트림에는 Subscribe 하고 나서의 프레임 수가 흘러 나온다.
  • Subscribe의 Dispose는 수동으로 호출 해야 한다. (또는 AddTo를 사용하여 GameObject에 연결할 필요가 있다.)

설명

Observable.EveryUpdate는 static 메서드로 정의되어 있기 때문에 MonoBehaviour 이외의 장소에서도 사용할 수 있습니다. 내부적으로 MainThreadDispatcher의 코루틴의 실행 타이밍에 통지 됩니다.

참고로, Subscribe의 IDisposable을 올바르게 관리해야 하여, 사용시에는 주의가 필요합니다. (AddTo 등을 잊지 않고 붙여 둘 필요가 있습니다.)

정리

  • 빨리 사용한다면 ObservableMonoBehaviour를 상속하는 형태를 사용 ObservableMonoBehaviour은 비추천 되었습니다.
  • UniRx.Triggers을 Using 추가 UpdateAsObservable()를 직접 호출하는 것이 가장 간단하고 사용하기 쉽습니다.
  • Observable.EveryUpdate는 사용하는 곳이 별로 없습니다.

· 약 4분
karais89

환경

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

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

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

UniRx에 대한 기사 요약은 여기


최근 게시 된 "미래의 프로그래밍 기술을 Unity에서 -UniRx-"에서 UniRx에서 값의 변동을 모니터링 하는 방법을 소개했습니다.

하지만, UniRx의 15/03/03 업데이트 내용에 ObserveEveryValueChanged 라는 기능이 추가되었습니다. 이 기능은 수정 된 값의 변화의 감시를 더 간편하게 사용할 수 있기 때문에 사용법을 소개하고 싶습니다.

ObserveEveryValueChanged()

ObserveEveryValueChanged는 전체 클래스에 대한 확장 메서드로 정의되며 다음과 같은 특징이 있습니다.

  • 임의의 클래스 객체 사용할 수 있다. (그러나 UnityEngine.Object 이외에 적용하는 것은 안전하지 않다.)
  • 이 인스턴스 객체의 속성 값 (속성뿐만 아니라 값을 반환 하는 것이면 무엇이든)을 이전 프레임과 비교하여 그 값이 변화할때 OnNext를 스트림에 흐르게 된다.
  • 1 프레임 내에서 발생한 여러 차례의 변화는 감지하지 못하고 이전 프레임과 현재 프레임의 변화만 감지할 수 있다.
  • 메시지가 흐르는 타이밍은 엄밀하게는 Update()가 아닌 코루틴의 실행 타이밍이 된다. (참고)
  • 대상이 UnityEngine.Object의 파생 개체 인 경우 Destroy시 OnCompleted가 스트림에 흐르게 된다.
  • MonoBehaviour 이외의 장소에서도 사용할 수 있다.

사용 예) CharacterController.isGrounded를 감시한다.

ObserveEveryValueChanged를 사용한 예

// 캐릭터가 착지한 순간을 감지
_characterController
.ObserveEveryValueChanged(x => x.isGrounded)
.Where(x => x)
.Subscribe(_ => Debug.Log("OnGrounded!"));

비교) ObserveEveryValueChanged를 사용하지 않는 예 (UpdateAsObservable을 사용한 예)

this.UpdateAsObservable()
.Select(_ => _characterController.isGrounded)
.DistinctUntilChanged()
.Where(x => x)
.Subscribe(_ => Debug.Log("OnGrounded!"));

UpdateAsObservable().Select(_ => _characterController.isGrounded).DistinctUntilChanged() 부분이 없어 깔끔하게 코드를 작성 할 수 있었습니다.

정리

ObserveEveryValueChanged()를 사용해서 "매 프레임 값의 변동을 모니터링하고 뭔가하고 싶다" 라고 하는 것을 더 간결하게 작성할 수 있게 되었습니다.

그러나 UnityEngine.Object 이외의 Class 객체에 ObserEveryValueChanged을 사용하면 수동으로 객체 삭제시 SubScribe의 Dispose를 할 필요가 있으므로 주의가 필요합니다.

(추가: 15/03/23 수정되어 일반 인스턴스 객체에 대해서도 폐기시 자동으로 OnCompleted가 불러지게 되었습니다)

· 약 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

참고


사용한 것

· 약 5분
karais89

환경

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

원문 : https://qiita.com/toRisouP/items/72c8950b53f79fadc4af

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

UniRx에 대한 기사 요약은 여기


하고 싶은 일

  • PhotonAnimatorView를 사용하여 Trigger 동기화하고 싶다.
  • 그러나 현재 Trigger 동기화는 지원하지 않는다.
  • 어쩔 수 없기 때문에 Animator의 Bool을 사용하여 Trigger 기능을 재현한다.

원래 "Trigger"는

  • 기본은 Bool과 같다
  • 그러나 True가 되고 자동으로 False로 돌아 오는 성질이 있다.
  • 잠깐 작동하는 애니메이션 전환 등에 자주 사용 된다.

즉, Bool을 True로 설정하고 다음 프레임에서 False로 변환하면 Trigger과 같은 기능을 할 수 있다.


만들어 보았다

저는 뭐든지 컴포넌트로 분할하는 파이기 때문에, 이번에는 애니메이션 관리 컴포넌트를 만들고, 거기에 Trigger 기능을 가진 메소드를 만들었습니다. (컴포넌트 개발 지향)

밖에서 "SetTriggerAttackA", "SetTriggerAttackB"를 실행하면 애니메이션이 전환하도록 하고 있습니다.

using System;
using UniRx;
using UnityEngine;

[RequireComponent(typeof(Animator))]
public class PlayerAnimationController : MonoBehaviour
{
private enum AnimatorParameters
{
IsAttackA,
IsAttackB
}

private Animator _animator;

/// <summary>
/// Action 대리자를 내보내는 Subject
/// </summary>
private Subject<Action> _actionSubject = new Subject<Action>();

private void Start()
{
_animator = GetComponent<Animator>();

// 1 프레임 지연시켜 Action 대리자를 실행한다.
_actionSubject
.DelayFrame(1)
.Subscribe(x => x());
}

/// <summary>
/// 공격 애니메이션 A 시작
/// </summary>
public void SetTriggerAttackA()
{
// IsAttackATrigger을 true로 설정
_animator.SetBool(AnimatorParameters.IsAttackA.ToString(), true);

// IsAttackATrigger을 다음 프레임에서 false로하는
_actionSubject.OnNext(() => _animator.SetBool(AnimatorParameters.IsAttackA.ToString(), false));
}

/// <summary>
/// 공격 애니메이션 B 시작
/// </summary>
public void SetTriggerAttackB()
{
// IsAttackBTrigger을 true로 설정
_animator.SetBool(AnimatorParameters.IsAttackB.ToString(), true);

// IsAttackBTrigger을 다음 프레임에서 false로하는
_actionSubject.OnNext(() => _animator.SetBool(AnimatorParameters.IsAttackB.ToString(), false));
}
}

처리를 다음 프레임으로 미루는 방법

"True로 변경 한 후 다음 프레임에서 False로 변경 한다." 라는 처리는 UniRx를 사용하여 구현 하였습니다.

Action 대리자를 내보내는 Subject를 생성하고 거기에 DelayFrame(1)을 사이에 두는 것으로 프레임을 지연시켜 Subscribe에서 Action 대리자를 실행하도록 하고 있습니다.

UniRx를 사용한 지연 실행

private void Start()
{
_animator = GetComponent<Animator>();

// 1 프레임 지연시켜 Action 대리자를 실행한다.
_actionSubject
.DelayFrame(1)
.Subscribe(x => x());

Debug.Log($"Start 실행 : {Time.frameCount}");

// _ actionSubject 흘린 Action 대리자가 다음 프레임에서 실행되는
_actionSubject
.OnNext(() => Debug.Log($"1 프레임 후에 실행 : {Time.frameCount}"));
}

실행결과

Start 실행 : 1
1 프레임 후에 실행 : 3

역주: 포스트에서는 1 프레임 후에 실행 결과가 2로 나오지만, 실제 실행시에 3으로 출력 됩니다. 이 부분은 확인이 필요할 것 같습니다.


정리

제대로 Trigger이 작동 하였습니다.

역시 UniRx는 편리합니다. (이번 사용법은 상당히 변칙적인 방법이라고 생각합니다만..)

참고로 이 방법으로 만든 Trigger (내용은 단순히 Bool)은 PhotonAnimatorView에 작동시켰는데, 제대로 작동하였습니다.

단지 동기화 해주지 않는 타이밍이 있기 때문에 조사가 필요할 것 같습니다.


15/03/29 추가

다음 프레임 처리를 실행한다면 Observable.NextFrame를 사용하는 것이 더 좋을 것 같습니다.

public void SetTriggerAttackB()
{
_animator.SetBool(AnimatorParameters.IsAttackB.ToString(), true);
Observable.NextFrame()
.Subscribe(_ => _animator.SetBool(AnimatorParameters.IsAttackB.ToString(), false));
}

· 약 6분
karais89

환경

  • macOS Mojave v10.14.6
  • Unity 2019.2.5f1
  • Github Desktop
  • Rider 2019.2
  • UniRx v7.1.0
  • Photon Unity Networking 2 (2.14)

원문 : https://qiita.com/toRisouP/items/228e42e6c65b0cbc349e

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

UniRx에 대한 기사 요약은 여기


Photon Cloud의 RoomList 업데이트를 UniRx를 이용해서 구현하도록 해보았습니다.

준비

위를 참고하여 Photon 콜백을 집계하고 받아들이는 PhotonCallbacks라는 싱글톤 오브젝트를 만들었습니다.

이 PhotonCallbacks은 Photon 콜백을 모두 Observable로 변환하여 제공 해줍니다.

방 업데이트 알림 Observable

using System.Linq;
using UnityEngine;
using UniRx;

/// <summary>
/// Photon 방 정보 변경을 모니터링 한다.
/// </summary>
public class RoomListManager : TypedMonoBehaviour
{
private IObservable<RoomInfo[]> _onRoomInfoChangedObservable;

/// <summary>
/// 방 정보가 업데이트되었을 때 새로운 방 목록을 제공하는 Observable
/// </summary>
public IObservable<RoomInfo[]> OnOnRoomInfoChangedObservable
{
get { return _onRoomInfoChangedObservable; }
}

public override void Awake()
{
base.Awake();

// OnReceivedRoomListUpdate 타이밍에서 최신 RoomList []를 흐르는 스트림
_onRoomInfoChangedObservable = PhotonCallbacks.Instance
// OnReceivedRoomListUpdate이 실행되었을 때 발화 Observable
.OnReceivedRoomListUpdateAsObservable()
// 방 업데이트 알림이 온 타이밍에서 최신 RoomInfo [] 으로 대체
.Select(_ => PhotonNetwork.GetRoomList())
// Hot 변환 (현재 방 목록으로 초기화)
.Publish(PhotonNetwork.GetRoomList())
.RefCount();
}

Photon 버전 변경으로 인해 포스트에 있는 스크립트를 아래 처럼 변경 하였습니다.

using System;
using System.Collections.Generic;
using Photon.Realtime;
using UniRx;
using UnityEngine;

public class RoomListManager : MonoBehaviour
{
private IObservable<List<RoomInfo>> _onRoomInfoChangedObservable;

/// <summary>
/// 방 정보가 업데이트 되었을 때 새로운 방 목록을 제공하는 Observable
/// </summary>
public IObservable<List<RoomInfo>> OnRoomInfoChangedObservable => _onRoomInfoChangedObservable;

private void Awake() => _onRoomInfoChangedObservable = PhotonCallbacks.Instance
.OnRoomListUpdateAsObservable()
.Publish()
.RefCount();
}

수정한 부분

  • UniRx의 TypedMonoBehaviour와 ObservableMonoBehaviour 퍼포먼스 이슈로 인해 Deprecated 되었다. 대신에 ObservableTriggers를 사용하면 된다.
  • PhotonNetwork.GetRoomList() → OnRoomListUpdate 콜백으로 대체
  • OnReceivedRoomListUpdate() → 없어짐 위의 OnRoomListUpdate로 통합.
  • 수정한 스크립트의 경우 Select 해주는 부분이 없어서 사실 Hot 변환 자체가 필요 없지만, 기존 스크립트와 통일성을 유지하기 위해 Hot 변환해주는 부분을 그냥 유지 하였습니다. (PhotonCallbacks에서 Subject 값을 넘겨주기 때문에 Subject의 경우 자체적으로 Hot 입니다.)

UniRx의 Publish 해주는 부분은 아래와 같이 이해 했다. Hot 변환 현재 방 리스트로 초기화 해주는 부분인데. 방 정보가 갱신될때 ReceivedRoomListUpdate가 호출되고, 호출된 이후에는 GetRoomList로 룸 리스트를 받고 난 후. Cold한 Stream을 Hot으로 변환해주면서 GetRoomList로 초기화 해준다.

결국 위 스크립트는 OnRoomInfoChangedObservable 방 목록의 업데이트가 있을 때 최신의 방 정보를 흘려 주는 스트림입니다.

이 스트림을 Subscribe하면 방 정보가 업데이트 되었을 때 자동으로 최신 상태를 취득 할 수 있습니다.

사용법

OnRoomInfoChangedObservable를 Subscribe 하는게 전부 입니다.

OnReceivedRoomListUpdate 콜백이 온 타이밍에서 Subscribe의 처리가 실행 됩니다.

using UniRx;
using UnityEngine;

public class RoomListView : MonoBehaviour
{
[SerializeField] private RoomListManager _roomListManager;

private void Start() =>
// 방 정보가 업데이트 되었을 때 현재의 방의 개수를 콘솔에 출력한다.
_roomListManager
.OnRoomInfoChangedObservable
.Subscribe(roomList =>
{
Debug.Log($"현재 방 수 : {roomList.Count}");
});
}

이번에는 그냥 Debug.Log로 방의 개수를 출력하고 있는 처리 뿐입니다.

이 Subscribe의 내용으로 GUI를 변경하면 좋을 것 같습니다.

여담

Hot과 Cold 특성을 제대로 파악해 두지 않으면 의도하지 않은 동작을 하므로 주의하는 것이 좋습니다.

참고: Rx의 Hot과 Cold에 대해

추가 (2015/04/07)

ReactiveProperty이라는 것이 UniRx에 추가 되었기 때문에, 이 쪽을 사용하는 것이 더 간단하게 처리할 수 있습니다.

using System.Collections.Generic;
using Photon.Pun;
using Photon.Realtime;
using UniRx;

public class PhotonRoomListModel : MonoBehaviourPunCallbacks
{
public ReactiveProperty<List<RoomInfo>> RoomInfoReactiveProperty
= new ReactiveProperty<List<RoomInfo>>();

public override void OnRoomListUpdate(List<RoomInfo> roomList) =>
RoomInfoReactiveProperty.Value = roomList;
}
using UniRx;
using UnityEngine;

public class RoomsUpdateObserver : MonoBehaviour
{
[SerializeField] private PhotonRoomListModel roomModel;

private void Start() => roomModel
.RoomInfoReactiveProperty
.AsObservable()
.Subscribe(rooms =>
{
Debug.Log(rooms.Count);
});
}

PhotonRoomListModel 스크립트의 경우 원본 포스트의 스크립트와 내용이 다릅니다. 포톤 클라우드 버전이 달라지면서 API가 달라져서 새로운 스크립트로 대체 합니다. 아래는 원본 스크립트 입니다.

public class PhotonRoomListModel : MonoBehaviour
{
public ReactiveProperty<RoomInfo[]> RoomInfoReactiveProperty
= new ReactiveProperty<RoomInfo[]>(new RoomInfo[0]);

private void OnReceivedRoomListUpdate()
{
_reactiveRooms.Value = PhotonNetwork.GetRoomList();
}
}

· 약 11분
karais89

환경

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

원문 : https://qiita.com/toRisouP/items/581ffc0ddce7090b275b

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

UniRx에 대한 기사 요약은 여기


Unity 개발에서 카운트 다운 타이머 (또는 카운트 업 타이머)를 구현해야 되는 경우가 많습니다.

이번에는 그 카운트 다운 타이머를 UniRx를 사용하여 구현 할 예정입니다.

Rx로 만든 카운트 다운 타이머

지정한 초만큼 카운트 다운하는 스트림

/// <summary>
/// countTime 만큼 카운트 다운하는 스트림
/// </summary>
/// <param name="countTime"></param>
/// <returns></returns>
private IObservable<int> CreateCountDownObservable(int countTime)
{
return Observable
.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1)) // 0초 이후 1초 간격으로 실행
.Select(x => (int) (countTime - x)) // x는 시작하고 나서의 시간(초)
.TakeWhile(x => x > 0); // 0초 초과 동안 OnNext 0이 되면 OnComplete
}

Rx로 초를 카운트 다운하는 경우 위와 같은 방법으로 구현할 수 있습니다.

(Generateplan/Pattern를 사용해도 좋지만, UniRx에는 이러한 기능이 아직 구현되지 않았다.)

다만, 이 CreateCountDownObservable로 만든 스트림은 Cold라는 점에 주의할 필요가 있습니다.

(Subscribe한 타이밍부터 시작하고 카운트다운이 시작되는 점, 1개의 타이머 스트림을 여러번 Subscribe하려면 Hot 변환이 필요하다는 점 등)

Hot/Cold 내용은 여기를 참고하십시오.

Rx에서 타이머를 만드는 장점/단점

Rx에서 타이머를 만드는 것과 Unity 표준 코루틴과 InvokeRepeat에서 타이머를 만드는 것을 비교하면 Rx로 만드는 경우 다음과 같은 장단점이 있습니다.

장점

  • 스트림 자체가 시간을 관리 해준다.
  • 타이머를 감시하는 측의 처리가 쉽다.
  • 타이머를 바탕으로 다른 작업을 시작 하기 쉽다.

단점

  • 어느 순간에 타이머의 시간을 얻을 수 없다 (임시 변수에 저장할 필요가 있다)
  • 타이머를 중단하거나 남은 시간을 고쳐 쓰기가 어렵다 (불가능하지는 않다)
  • Rx 자체가 어렵고 이해하기 어렵다 (Hot/Cold를 알지 못하면 오동작 가능성이 있다)

타이머를 Rx에서 만드는 가장 큰 장점은 "타이머를 감시하는 측의 처리가 쉽다""타이머를 바탕으로 다른 작업을 시작하기 쉽다" 입니다.

예) Rx 타이머의 값을 사용하여 작업

예를 들어, 방금 전의 CountDownTimer을 이용하여 다음과 같은 기능을 구현하려고 합니다.

구현 목록

  • Start() 시점에서 60초를 카운트 한다.
  • 타이머의 숫자를 UnityEngine.UI.Text에 그린다.
  • 카운트가 10초 이하가 되면 위의 Unity.UI.Text의 글자의 색상을 붉은 색으로 변하게 한다.
  • 카운트가 10초 이하가 되면 카운트마다 효과음을 울린다.
  • 계산이 끝나면 Unity.UI.Text의 문자를 지운다.
  • 계산이 끝나면 효과음을 울린다.

RxCountDownTimer.cs

/// <summary>
/// 카운트 구성 요소
/// </summary>
public class RxCountDownTimer : MonoBehaviour
{
/// <summary>
/// 카운트 다운 스트림
/// 이 Observable을 각 클래스가 Subscribe 한다.
/// </summary>
public IObservable<int> CountDownObservable => _countDownObservable.AsObservable();

private IConnectableObservable<int> _countDownObservable;

// 60초 카운트 스트림을 생성
// Publish로 Hot 변환
private void Awake() =>
_countDownObservable = CreateCountDownObservable(60).Publish();

// start시 카운트 시작
private void Start() =>
_countDownObservable.Connect();

/// <summary>
/// countTime 만큼 카운트 다운하는 스트림
/// </summary>
/// <param name="countTime"></param>
/// <returns></returns>
private IObservable<int> CreateCountDownObservable(int countTime) =>
Observable
.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1))
.Select(x => (int) (countTime - x))
.TakeWhile(x => x > 0);
}

CountDownTextComponent.cs

/// <summary>
/// 타이머의 시간을 기초로 Text를 업데이트 할 컴포넌트
/// </summary>
public class CountDownTextComponent : MonoBehaviour
{
/// <summary>
/// UnityEditor에서 할당 한다.
/// </summary>
[SerializeField] private RxCountDownTimer _rxCountDownTimer;

/// <summary>
/// uGUI의 Text
/// </summary>
private Text _text;

private void Start()
{
_text = GetComponent<Text>();

// 타이머의 남은 시간을 표시한다.
_rxCountDownTimer
.CountDownObservable
.Subscribe(time =>
{
// onNext에서 시간을 표시한다.
_text.text = $"남은 시간 : {time}";
}, () =>
{
// onComplete에서 문자를 지운다.
_text.text = string.Empty;
}).AddTo(gameObject);

// 타이머가 10초 이하로 되는 타이밍에 색을 붉은 색으로 한다.
_rxCountDownTimer
.CountDownObservable
.First(timer => timer <= 10)
.Subscribe(_ => _text.color = Color.red);
}
}

CountDownSoundComponent.cs

[RequireComponent(typeof(AudioSource))]
public class CountDownSoundComponent : MonoBehaviour
{
// 효과음
[SerializeField] private AudioClip _seCountDownTick;
[SerializeField] private AudioClip _seCountDownEnd;

private AudioSource _audioSource;

/// <summary>
/// UnityEditor에서 할당 한다.
/// </summary>
[SerializeField] private RxCountDownTimer _rxCountDownTimer;

private void Start()
{
_audioSource = GetComponent<AudioSource>();

// 카운트가 10초 이하가 되면 효과음을 1초마다 울리게 한다.
_rxCountDownTimer
.CountDownObservable
.Where(time => time <= 10)
.Subscribe(_ => _audioSource.PlayOneShot(_seCountDownTick));

// 계산이 완료된 시점에서 효과음을 울린다.
_rxCountDownTimer
.CountDownObservable
.Subscribe(_ => { ; }, () => _audioSource.PlayOneShot((_seCountDownEnd)));
}
}

RxCountDownTimer에서 카운트 다운 스트림을 만들고 그것을 CountDownTextComponent와 CountDownSoundComponent에서 모니터링하여 값의 변화와 동시에 처리를 실시하게 구현되어 있습니다.

Rx를 사용하면 "타이머의 현재 시간이 흐른다"는 이벤트와 같은 (라기 보다는 그 이상)것을 보다 적은 코드로 처리할 수 있게 됩니다.

"값의 변화를 감시하고 처리한다", "변화하는 값이 (복잡한) 조건을 충족했을 때 처리한다" 라는 부분에서는 Rx를 사용하는 것이 유용합니다.

예) 타이머를 바탕으로 복잡한 처리를 한다.

방금 전의 60초 타이머의 동작을 바꾸어 봅시다.

변경할 목록

  • Start()시 3초간 카운트 다운을 한다.
  • 3초 카운트 다운이 끝나고 나서 60초 카운트 다운을 시작한다.
  • 60초 카운트 다운이 끝나고 난 후 5초 정도 기다린 후 다른 씬으로 이동한다.

"게임 시작 전 3초 카운트 다운" → "게임 시작 60초 카운트 다운" → "5초 동안 결과 표시후 게임 종료" 순서라고 생각해 주십시오.

이를 Rx에서 쓰면 다음과 같습니다.

public class GameTimerManager : MonoBehaviour
{
/// <summary>
/// 경기 시작 전 카운트 다운
/// </summary>
public IObservable<int> GameStartCountDownObservable { get; private set; }

/// <summary>
/// 경기 중 카운트 다운
/// </summary>
public IObservable<int> BattleCountDownObservable { get; private set; }

private void Start()
{
// 경기 전 3초 타이머
// 3초 타이머의 스트림을 Publish로 Hot으로 변환 (아직 Connect는 하지 않는다)
var startConnectableObservable = CreateCountDownObservable(3).Publish();
// 외부에 공개하기 위해 Observable로 저장
GameStartCountDownObservable = startConnectableObservable;

// 경기 중 60초 타이머
// 60초 타이머의 스트림을 Publish로 Hot으로 변환 (아직 Connect는 하지 않는다)
var battleConnectableObservable = CreateCountDownObservable(60).Publish();
// 외부에 공개하기 위해 Observable로 저장
BattleCountDownObservable = battleConnectableObservable;

// 3초 타이머의 OnComplete에서 60초 타이머를 Connect한다 (60초 타이머 시작)
GameStartCountDownObservable
.Subscribe(_ => { ; }, () => battleConnectableObservable.Connect());

// 60초 타이머 뒤에 Concat으로 5초 타이머를 연결하고 OnComplete에서 Scene를 전환한다.
BattleCountDownObservable
.Concat(CreateCountDownObservable(5))
.Subscribe(_ => { ; }, () =>
{
SceneManager.LoadScene("NextScene");
}).AddTo(gameObject);

// 3초 타이머 시작
startConnectableObservable.Connect();
}

/// <summary>
/// countTime 만큼 카운트 다운하는 스트림
/// </summary>
/// <param name="countTime"></param>
/// <returns></returns>
private IObservable<int> CreateCountDownObservable(int countTime) =>
Observable
.Timer(TimeSpan.FromSeconds(0), TimeSpan.FromSeconds(1))
.Select(x => (int) (countTime - x))
.TakeWhile(x => x > 0);
}

조금 복잡 할지도 모릅니다만, 이 코드만으로 구현이 완료됩니다.

상태를 관리하는 필드 변수는 불 필요 하고, 스트림의 합성만으로 구현할 수 있었습니다.

정리

  • Rx를 이용하면 타이머를 보다 적은 코드로 구현할 수 있다.
  • 스트림 자신이 시간을 유지하기 위해 임시 저장 변수가 불필요 하다. (그러나 임의의 프레임에서의 시간 취득은 할 수 없게 된다)
  • 타이머의 복잡한 구현은 Rx의 합성 메서드로 구현 할 수 있다.
  • UI 갱신 등에 Rx 스트림을 편리하게 사용할 수 있다. (Model에서 View로 통지)
  • Rx의 Hot/Cold의 개념을 알고 있지 않으면 오동작 가능성이 있다.
  • Rx 자체가 어렵다.
  • UniRx가 더 유행 했으면 좋겠다.

· 약 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/f6088963037bfda658d3

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

UniRx에 대한 기사 요약은 여기


Rx의 IObservable<T>는 Hot/Cold라는 큰 두가지 특징이 있습니다.

이러한 성질을 이해하지 않은 채 스트림을 설계하면 의도된 동작을 해주지 않는 경우가 있습니다.

이번에는 이 Hot/Cold의 성격에 대해 간략하게 정리하고자 합니다.


요약


한마디로 말하면?

  • Cold: 스트림의 전후를 연결하는 파이프. 단독으로는 의미가 없다. 대부분의 오퍼레이터는 Cold이다.
  • Hot: 스트림에서 값을 계속 발행하는 수도꼭지. 항상 흐린다. 뒤에 파이프를 많이 연결 할 수 있다.

자세히 설명하면

Cold Observable

  • 자발적으로 아무것도 하지 않는 수동적인 Observable
  • Observer이 등록되어 (Subscribe 되고) 처음 일을 시작한다.
  • 스트림의 전후를 그냥 연결만 한다. 스트림을 분기시키는 기능은 없다.

Hot Observable

  • 자신이 값을 발행하는 능동적인 Observable
  • 후속 Observer의 존재에 관계없이 메시지를 발행한다
  • 자신보다 상류의 Cold Observable을 시작하고 값의 발행을 요구하는 기능을 가진다
  • 하류의 Observer를 모두 묶어, 정리해 같은 값을 발행한다 (스트림을 분기시킨다)

Hot과 Cold 구분법

대부분의 오퍼레이터는 Cold인 성질이며, 자신이 명시적으로 스트림(stream)을 Hot으로 변환하지 않는 한 Cold 그대로 입니다.

Hot 변환용 오퍼레이터는 이른바 Publish 계의 오퍼레이터가 해당 합니다.


Hot에 대해


Hot Observable의 성질


스트림을 가동시키는 성질

Rx 스트림은 기본적으로 Subscribe가 된 순간에 각 오퍼레이터의 작동이 시작하게 되어 있습니다. 하지만 Hot Observable을 스트림 중간에 끼우는 것으로, Subscribe를 샐행 이전에 스트림을 실행시킬 수 있습니다.


스트림을 분기하는 성질

Hot Observable은 스트림을 분기 할 수 있습니다.

Cold에 대해


Subscribe 될 때까지 작동하지 않는 성질

Cold Observable은 Subscribe될때 (또는 Hot 변환될때)까지 작동하지 않습니다. 마음이 없는 Observable 입니다.

작동하지 않는 Cold Observable에 전달된 메시지는 모두 처리 조차 되지 않고 소멸 됩니다.

특히 값의 발행 타이밍이나 전후 관계가 중요한 오퍼레이터를 사용하는 경우는, 어느 타이밍부터 처리가 시작되는지를 충분히 인지하고 사용하지 않으면 안됩니다. 같은 스트림 정의라도, Subscribe 한 타이밍에 따라서 동작이 바뀌어 버립니다. 아래가 그 예 입니다.

ColdExample.cs

var subject = new Subject<string>();

// subject에서 생성된 Observable은 [Hot]
var sourceObservable = subject.AsObservable();

// 스트림에 흘러 들어온 문자열을 연결하여 새로운 문자열로 만드는 스트림
// Scan()은 [Cold]
var stringObservable = sourceObservable.Scan((p, c) => p + c);

// 스트림에 값을 흘린다
subject.OnNext("A");
subject.OnNext("B");

// 스트림에 값을 흘린 후 Subscribe 한다.
stringObservable.Subscribe(Debug.Log);

// Subscribe 후 스트림에 값을 흘린다.
subject.OnNext("C");

// 완료
subject.OnCompleted();

실행결과

C

위의 코드를 실행한 결과 C가 출력 될 것입니다.

이것은 Scan 오퍼레이터가 Cold이기 때문에 Subscribe 전에 발행된 A 그리고 B 가 처리되지 않았기 때문입니다.

만약 여기에서 "Subscribe하기 이전에 발급 된 값을 처리 했으면 좋겠다"는 경우는 어떻게 하면 좋을까요. 이 경우 Hot 변환 오퍼레이터를 끼워 Subscribe하기 이전에 스트림을 시작하면 좋을 것입니다.

var subject = new Subject<string>();

// subject에서 생성된 Observable은 [Hot]
var sourceObservable = subject.AsObservable();

// 스트림에 흘러 들어온 문자열을 연결하여 새로운 문자열로 만드는 스트림
// Scan()은 [Cold]
var stringObservable = sourceObservable
.Scan((p, c) => p + c)
.Publish(); // Hot 변환 오퍼레이터

stringObservable.Connect(); // 스트림 가동 개시

// 스트림에 값을 흘린다
subject.OnNext("A");
subject.OnNext("B");

// 스트림에 값을 흘린 후 Subscribe 한다.
stringObservable.Subscribe(Debug.Log);

// Subscribe 후 스트림에 값을 흘린다.
subject.OnNext("C");

// 완료
subject.OnCompleted();

실행 결과

ABC

Publish 라는 Hot 변환 연산자를 사이에 끼우는 것으로, Subscribe하는 이전에 스트림을 강제로 실행시킬 수 있습니다.


각각의 Observer에 대해 별도의 처리를 한다 (스트림의 분기점이 되지 않는다.)

Cold Observable은 스트림을 분기시키는 성질을 가지고 있지 않습니다.

따라서 Cold Observable을 여러 Subscribe하는 경우 각각 별도의 스트림이 생성되고 할당 될 것입니다.


그러나 스트림에 Hot Observable이 존재하는 경우 가장 말단에 가까운 Hot Observable로 스트림이 분기되어, 또 다른 별도의 스트림이 생성됩니다.


정리

스트림이 어디에서 분기 하는가? 항상 의식하고 설계하는 것이 중요합니다.

"클래스 외에 Observable을 공개 할 때는 Cold인 채로 공개하지 말고, 반드시 말단에서 Hot으로 변환한 후 공개한다"등 의도하지 않은 곳에서 스트림이 분기되어 버리지 않도록 확실하게 제어해 줄 필요가 있습니다.

또한 Cold → Hot 변환에는 적용 오퍼레이터가 준비되어 있으므로, 그 쪽을 이용하면 좋을 것 같습니다.

(Publish, PublishLast, Multicast 등)

자세히 : [Reactive Extensions] Hot 변환은 어떤 때에 필요한가?

· 약 6분
karais89

환경


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

원문 : https://qiita.com/toRisouP/items/83fd28b6d4a70a7ed1d2

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

UniRx에 대한 기사 요약은 여기


Unity 개발에서 플레이어 캐릭터를 만들 때 자주 사용되는 컴포넌트에 CharacterControoler가 있습니다.

이 컴포넌트는 개체를 이동할 때 바닥과 벽의 판정, 언덕의 경사 및 계단 오르기 등의 판정을 쉽게 계산 해주는 매우 유용한 컴포넌트 입니다.

다만 이 Character Controller은 접지 판정의 IsGrounded의 정밀도가 별로 입니다.

언덕길이나 계단을 이동하는 동안 분명히 접지하고 있음에도 불구하고 false 판정이 리턴되곤 합니다.

따라서 IsGrounded의 기준으로 점프 여부를 결정하면 점프를 하지 못하는 경우가 생길 수 있습니다.

(경사면을 이동중에는 isGrounded가 true/false가 격렬하게 변한다)

그래서 이번에는 이 IsGrounded의 판정을 완만하게 하여 개선해 보려고 합니다.

방법 1: Raycast와 병행하여 판정을 정확하게 하기


IsGrounded는 엄격하게 딱 바닥에 접해 있지 않으면 true가 되지 않습니다.

그래서 이 판정을 어느 정도 바닥에 가까우면 "지면에 접하고 있다"로 판정하도록 하려고 합니다.

판정에 Physics.Raycast 라는 개체와의 충돌을 검사하는 API을 이용합니다.

(이름 그대로 점에서 가상의 광선을 쏘아 그것이 물체에 닿았는지 확인 하는 API입니다.)

이 Raycast를 GameObject의 발밑에서 발사하고 Raycast가 지상과 충돌 여부에 지면에 접하고 있는지 판정해 보겠습니다.

(개체의 바닥에서 바로 밑에 Raycast를 쏘아서 충돌 여부를 판단)

CheckGroundedWithRaycast.cs

/// <summary>
/// 땅에 접지되어 있는지 여부를 확인
/// Update에서 실행, _characterController, _fieldLayer의 경우 Start 메서드에서 캐시 처리.
/// </summary>
/// <returns></returns>
private bool IsCheckGrounded()
{
// CharacterController.IsGrounded가 true라면 Raycast를 사용하지 않고 판정 종료
if (_characterController.isGrounded) return true;
// 발사하는 광선의 초기 위치와 방향
// 약간 신체에 박혀 있는 위치로부터 발사하지 않으면 제대로 판정할 수 없을 때가 있다.
var ray = new Ray(this.transform.position + Vector3.up * 0.1f, Vector3.down);
// 탐색 거리
var maxDistance = 1.5f;
// 광선 디버그 용도
Debug.DrawRay(transform.position + Vector3.up * 0.1f, Vector3.down * maxDistance, Color.red);
// Raycast의 hit 여부로 판정
// 지상에만 충돌로 레이어를 지정
return Physics.Raycast(ray, maxDistance, _fieldLayer);
}

이와 같이 CharacterController.ISGrounded과 Raycast를 병행함으로써 경사면의 행동을 개선 한 수 있습니다. 그러나 이 CheckGround()는 실행할 때마다 매번 Raycast를 실행하게 됩니다. 실제로 사용할 때는 Raycast 결과를 프레임별로 캐시하면 더 좋지 않을까 생각합니다. (실제 캐시 처리를 할 수 있는지 의문이 듬)

방법 2: IsGrounded 값의 변동이 안정 될 때까지 IsGrounded 값을 무시


IsGrounded 값을 잘 관찰하면, "점프와 착지 직후" "경사면을 이동하는 동안" true/false 값이 수차례 변동하는 것을 알 수 있습니다. 그래서 이 값이 변동하는 것을 일정 시간 무시하고 값이 안정화 된 이후에 이용하도록 해봅시다. (마지막에 값이 변화하고 n 밀리 초 경과했을 때 그 값으로 결정합니다)

이러한 시간에 관한 판정 처리는 Rx가 적합하므로, Rx의 Unity용 구현 UniRx를 사용해 봅시다.

CheckGroundedComponent.cs

using UniRx;
using UnityEngine;

public class CheckGroundedWithRx : MonoBehaviour
{
private bool _isGrounded;

public bool IsGrounded { get => _isGrounded; }

private void Start()
{
_characterController = GetComponent<CharacterController>();
_characterController
.ObserveEveryValueChanged(x => x.isGrounded)
.ThrottleFrame(5)
.Subscribe(x => _isGrounded = x);
}
}

CharacterController.IsGrounded을 배치하고 값이 마지막으로 변화하고 5 프레임 안정 될때까지 그 값을 무시하도록 해 보았습니다.

이러한 "값의 변화를 감시한다", "시간을 사용하여 판정"등 처리는 Rx를 사용하면 정말 쉽게 작성할 수 있으므로 사용하는 것을 추천 합니다.

다만 학습 비용 (러닝커브)는 상당히 높습니다.

마지막으로


UniRx가 더 유행했으면 좋겠습니다.