코루틴
http://neue.cc/2014/12/18_499.html
StartCoroutine
Rx에서 코루틴을 스트림 소스로 만드는 방법은 MainThreadDispatcher.StartCoroutine 을 사용하는 것입니다.
IEnumerator MyCoroutine()
{
....
yield return null;
}
// Global StartCoroutine runner
MainThreadDispatcher.StartCoroutine(MyCoroutine);
코드에서와 같이 MonoBehaviour.StartCoroutine 과 다르지 않습니다.
namespace UniRx
{
public sealed class MainThreadDispatcher : MonoBehaviour
{
/// static!
new public static Coroutine StartCoroutine(IEnumerator routine)
{
...
}
MainThreadDispatcher.StartCoroutine 함수는 Coroutine 형을 반환합니다.
MainThreadDispatcher.StartCoroutine vs MonoBehaviour.StartCoroutine
MainThreadDispatcher.StartCoroutine 함수로 실행한 코루틴 함수는 MonoBehaviour 인스턴스의 메소드가 아닌 정적(static) 메소드라는 점이 차이점입니다.
FromCoroutine
FromCoroutine은 코루틴을 IObservable로 바꿀 때 사용합니다.
다음은 플레이어가 생존해 있는 동안에만 주어진 시간동안 작동하는 코루틴 함수를 처리하는 코드입니다.
private void Start ()
{
...
// 플레이어가 생존 해있는 시간을 30 초 카운트 다운
// 타이머의 현재 카운트 [초]이 통지되는
Observable . FromCoroutine < int > ( observer => CountDownCoroutine ( observer , 30 , player ))
. Subscribe ( count => Debug.Log ( count ));
}
/// <summary>
/// 플레이어가 살아있는 동안에 만 카운트 다운 타이머
/// 플레이어가 죽은 경우 계산은 중지
/// </ summary>
IEnumerator CountDownCoroutine ( IObserver < int > observer, int startTime, player player )
{
var currentTime = startTime ;
while ( currentTime > 0 )
{
if ( player.IsAlive )
{
observer.OnNext ( currentTime);
currentTime -= 1;
}
yield return new WaitForSeconds ( 1 );
}
observer.OnCompleted ();
}
FromCoroutine 연산자로 스트림 소스를 만드는 경우는 이처럼 IObservable을 코루틴 쪽으로 넘겨 줄 수 있기 때문에 코루틴 내에서 OnNext의 메시지 발생을 필요한 조건에 맞춰서 처리할 수 있습니다.
그러면 FromCoroutine
와 MainThreadDispatcher.StartCoroutine(IEnumerator)
은 어떻게 구분해서 사용해야 할까요?
FromCoroutine/FromMicroCoroutine은 IObservable
StartCoroutine/StartUpdateMicroCoroutine 함수는 단순히 지정한 코루틴 함수를 실행하는 역할만 합니다. MonoBehaviour의 StartCoroutine과 같다고 볼 수 있습니다.
MainThreadDispatcher는 싱글톤(singleton) MonoBehaviour이며 FromCoroutine/FromMicroCoroutine 에서도 코루틴 실행시에는 내부에서 StartCoroutine/StartUpdateMicroCoroutine 함수를 호출합니다.
StartAsCoroutine
StartAsCoroutine은 IObservable을 코루틴으로 바꿀 때에 사용합니다.
FromCoroutine은 코루틴을 IObservable로 바꿀 때, StartAsCoroutine은 IObservable을 코루틴으로 바꿀 때 사용합니다.
private void Start ()
{
StartCoroutine ( CoroutineA ());
}
IEnumerator CoroutineA ()
{
// 코루틴의 실행 결과를 저장하는 변수
var result = 0 ;
//Observable.Range를 코 루틴으로 변환한다
yield return Observable . Range ( 0 , 10 ). StartAsCoroutine ( x => result = x );
Debug . Log ( "Result: " + result );
}
실행 결과는 아래와 같습니다.
Result: 9
StartAsCoroutine은 다음과 같은 특징이 있습니다.
- OnCompleted를 호출할 때까지 yield return null을 계속 반복한다.
- StartAsCoroutine에 전달 된 함수는 OnCompleted를 호출할 때에 한 번만 실행되고 마지막 OnNext 값이 전달된다.
StartAsCoroutine을 잘 사용하면 비동기 처리를 포함하는 복잡한 처리를 Task의 await 처리와 같이 동기화 처리처럼 다룰 수 있습니다.
/// 시간이 소요되는 처리.
bool HeavyTask ()
{
// 무거운 처리를하는
Thread . Sleep ( 3000 );
// 랜덤으로 true / false를 반환
var random = new System . Random ();
return random . Next () % 2 == 0 ;
}
private IEnumerator HeavyTaskCoroutine ()
{
// 실행 결과의 반환값
bool result = false ;
// Observable.Start 사용시 다른 스레드에서 HeavyTask 작업을 수행.
yield return Observable . Start (() => HeavyTask ()). StartAsCoroutine ( x => result = x );
if ( result )
Debug . Log ( "Success" );
else
Debug . Log ( "Failure" );
}
Start() 연산자는 멀티쓰레딩으로 비동기 처리를 하는 연산자로 HeavyTask는 다른 쓰레드에서 처리를 하게 됩니다. 이 IObservable을 StartAsCoroutine 연산자를 사용해서 코루틴으로 변경함으로써 이 쓰레드의 처리를 완료하기 전까지 yield return null을 반환하게 됩니다. HeavyTask에서 처리를 완료하면 OnCompleted 메시지가 전달되고 이 때 랜덤으로 선택한 true나 false 반환값이 result에 전달됩니다.
5.3 버전부터는 ToYieldInstruction으로 사용할 수도 있습니다.
yield return Observable.Start(()=>HeavyTask).ToYieldInstruction(); // 5.3 이후 버전
에디터에서의 사용
MonoBehaviour의 코루틴은 Unity 에디터 스크립트에서 사용할 수 없지만 FromCoroutine 연산자는 Unity 에디터 스크립트에서 사용하는 것이 가능합니다.
UniRx는 FromCoroutine을 실행하면 내부에서 MainThreadDispatcher.SendStartCoroutine 사용해서 작동하기 때문에 Editor에서 실행할 수 있습니다.
아래 코드는 에디터 윈도우를 실행하면 OnEnable 함수에서 코루틴 함수를 실행하고 30초후 라벨에 "completed!" 라는 문자열을 출력하는 예제입니다.
using UnityEngine;
using UnityEditor;
using System.Collections;
using UniRx;
public class CoroutineWindow : EditorWindow
{
static float deltaTime;
static bool isCompleted;
[MenuItem ("Tools/CoroutineTest")]
static void Open ()
{
GetWindow<CoroutineWindow> ();
}
static void OpenFromCLI ()
{
Observable.FromCoroutine (Update).Subscribe (_ => {
Debug.Log ("complete:" + deltaTime);
EditorApplication.Exit (0);
});
}
void OnEnable ()
{
Debug.Log ("init");
Observable.FromCoroutine (Update).Subscribe (_ => isCompleted = true);
}
void OnDestroy ()
{
Debug.Log ("end");
deltaTime = 0f;
isCompleted = false;
}
void OnGUI ()
{
GUILayout.Label ("deltaTime:" + deltaTime);
if (isCompleted) {
GUILayout.Label ("completed!");
}
}
static IEnumerator Update ()
{
while (30f > deltaTime) {
deltaTime += Time.fixedDeltaTime;
yield return null;
}
}
}
위의 코드에서처럼 에디터 코드에서의 FromCoroutine 사용도 동일합니다.
정리
UniRx를 사용해서 코루틴을 실행하면 다음과 같은 특징이 있습니다.
- 다양한 것으로 합성할 수 있다.
- 다양한 반환값의 사용이 가능하다.
- 실해 도중 취소가 쉽다.
- 에디터에서 코루틴의 사용이 가능하다.
// 인자가 없는 코루틴 함수
IEnumerator CoroutineA ( )
{
Debug.Log ( "a start" ) ;
yield return new WaitForSeconds ( 1 ) ;
Debug.Log ( "a end" ) ;
}
// 이런 식으로 사용할
Observable.FromCoroutine ( CoroutineA ).Subscribe ( _ => Debug.Log ( "complete" ) ) ;
// IObservable를 인자로 가지는 코루틴 함수
IEnumerator CoroutineB ( IObserver < int > observer )
{
observer.OnNext ( 100 ) ;
yield return new WaitForSeconds ( 2 ) ;
observer.OnNext ( 200 ) ;
observer.OnCompleted ( ) ;
}
// 합성
var coroutineA = Observable.FromCoroutine ( CoroutineA ) ;
var coroutineB = Observable.FromCoroutine < int > ( observer => CoroutineB ( observer ) ) ;
// A가 끝난 후 B를 시작. B Subscribe시 100이 전달, 2초후 200이 전달.
var subscription = coroutineA.SelectMany ( coroutineB ).Subscribe ( x => Debug.Log ( x ) ) ;
// Subscribe 반환에서 Dispose를 호출하면 취소 가능
// subscription.Dispose ();