본문으로 건너뛰기

· 약 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/4fec0e9716be4d415798

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

UniRx에 대한 기사 요약은 여기


UniRx를 사용하여 마우스를 롱클릭을 판정하는 스트림을 만들어 보았습니다.

  • 롱클릭 판정 (일정 시간 이상 마우스가 클릭된 상태로 계속 될 때)
  • 롱클릭 판정 취소 (일정 시간 내에 마우스 클릭이 중단되었을 때)

를 각각 검출 할 수 있는 스트림을 오퍼레이터 체인만 쓰는 패턴과 코루틴과 결합하는 패턴 2종류를 만들어 보았습니다.

오퍼레이터 체인만 사용하는 경우

private void Start()
{
var mouseDownStream = this.UpdateAsObservable()
.Where(_ => Input.GetMouseButtonDown(0));
var mouseUpStream = this.UpdateAsObservable()
.Where(_ => Input.GetMouseButtonUp(0));

// 롱클릭 판정
mouseDownStream
// 마우스 클릭되면 3초 후 OnNext를 흐르게 한다.
.SelectMany(_ => Observable.Timer(TimeSpan.FromSeconds(3)))
// 도중에 MouseUp되면 스트림을 재설정 한다.
.TakeUntil(mouseUpStream)
.RepeatUntilDestroy(gameObject)
.Subscribe(_ => Debug.Log("롱클릭"));

// 롱클릭 취소 판정
mouseDownStream.Timestamp()
.Zip(mouseUpStream.Timestamp(), (d, u) => (u.Timestamp - d.Timestamp).TotalMilliseconds / 1000.0f)
.Where(time => time < 3.0f)
.Subscribe(t => Debug.Log(t + "초에서 취소"));
}

동작을 파악 하는 것이 조금 복잡하지만, 일단은 작동 합니다.

역주:

아래 오퍼레이터의 동작을 알고 있어야 정확한 이해가 가능 합니다.

  • SelectMany
    • 스트림 자체를 다른 스트림으로 대체해 버린다.
    • 위 예에서는 mouseDownStream 스트림 자체를 Observable.Timer로 대체 한다고 생각하면 된다. (마우스 클릭 스트림이 발행된 이후 3초 후에 대한 스트림을 새로 만듬)
  • TakeUntil
    • 스트림의 메시지가 오면 OnCompleted를 통해 스트림을 종료시킨다.
    • 위 예에서는 mouseUpStream이 발행되면 OnCompleted가 발행되고, Repeat가 실행되면서 다시 Subscribe 하게 되는 구조 (Repeat란?)
  • Timestamp
    • 스트림이 발행한 시간을 타임스탬프 값으로 반환한다.
  • Zip
    • 두개의 오퍼레이터가 모두 실행될때에 대한 처리를 하나로 묶어 처리해준다.

코루틴과 조합하는 경우

public class LongClick : MonoBehaviour
{
#region 롱클릭 스트림
private Subject<Unit> _longClickSubject;

private IObservable<Unit> LongClickAsObservable =>
_longClickSubject ?? (_longClickSubject = new Subject<Unit>());
#endregion

#region 롱클릭 취소 스트림
private Subject<float> _longClickCancelSubject;

private IObservable<float> LongClickCancelAsObservable =>
_longClickCancelSubject ?? (_longClickCancelSubject = new Subject<float>());
#endregion

private Coroutine _longClickCoroutine;

private void Start()
{
_longClickCoroutine = StartCoroutine(LongClickCoroutine(3.0f, _longClickSubject, _longClickCancelSubject));

LongClickAsObservable.Subscribe(_ => Debug.Log("롱클릭"));
LongClickCancelAsObservable.Subscribe(t => Debug.Log(t + "초 취소"));
}

/// <summary>
/// 롱클릭 판정 코루틴
/// </summary>
private IEnumerator LongClickCoroutine(float threshold,
IObserver<Unit> longClickObserver,
IObserver<float> longClickCancelObserver)
{
var isLongClicked = false;
var count = 0.0f;

while (true)
{
if (Input.GetMouseButton(0))
{
count += Time.deltaTime;
if (!isLongClicked && count > threshold)
{
isLongClicked = true;
longClickObserver.OnNext(Unit.Default);
}
}
else if (Input.GetMouseButtonUp(0))
{
isLongClicked = false;
if (count > 0)
{
longClickCancelObserver.OnNext(count);
count = 0;
}
}
yield return null;
}
}

private void OnDestroy()
{
if (_longClickCoroutine != null)
StopCoroutine(_longClickCoroutine);
}
}

판정 처리를 무한 루프하는 코루틴안에 쓰고, 결과를 스트림으로 외부에 통지하는 형태로 구현하였습니다.

오퍼레이터 체인을 사용하여 흐름을 파악하기 힘든 스트림이 된다면, 포기하고 절차적으로 쓰는 것이 더 좋을 수 있습니다. 잡다한 처리는 전부 코루틴에 장착되어 사용하는 측에서는 알 수 없기 때문에 허용 범위 일수도 있을까 싶습니다.

또한 이러한 범용적으로 사용할 것 같은 처리는 ObservableTrigger로 정의해두면 좋을지도 모르겠습니다.

역주:

Observable.FromCoroutine 을 사용하는 방안을 생각해 보았는데, 해당 메서드는 Observer을 1개 밖에 사용하지 못하는 방식이라, 코루틴 2개(판정, 취소)를 각각 만들어줘야 되는 방식입니다. 위 예처럼 2개 이상의 Observer을 사용하는 경우는 Subject를 만들고 해당 Subject의 OnNext를 호출하는 방식이 더 나은 방법이라고 판판이 되네요.

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

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

UniRx에 대한 기사 요약은 여기


UniRx보다는 Reactive Extensions의 내용 입니다.

UniRx에서 구현을 진행하는데 있어서 "어떤 스트림을 흐르는 메시지 끼리를 비교하고 싶다" 라는 것을 하고 싶을 수 있습니다. 직전의 프레임에서의 플레이어 좌표와 지금 프레임에서의 프레임에서의 플레이어의 좌표를 비교하고 싶은것과 같은, 이러한 처리는 Buffer 오퍼레이터를 사용하면 간단하게 구현할 수 있습니다.

그런 이유로 이번에는 Buffer의 Skip 과 그것을 사용한 구현 사례를 소개하고 싶습니다.

Buffer의 오버로드

Buffer 오퍼레이터는 오버로드가 여러가지가 있는데 그 중 하나인 Skip 수를 지정할 수 있는 것이 있습니다. Skip을 지정하여 Buffer가 방류 한 뒤 어느 타이밍에 다시 방류할지를 지정 할 수 있습니다. Skip을 지정하지 않으면 Buffer 지정한 Count와 같은 값이 Skip로 설정됩니다.

Observable.Range(1, 10)
.Select(x => x.ToString())
.Buffer(2) // 2개 묶음 (방류한 뒤에 2개 건너뛰고 다음을 방류한다. Buffer(2,2)와 동일)
.Subscribe(x =>
{
// buffer의 내용을 표시
Debug.Log(x.Aggregate<string>((p, c) => p.ToString() + ", " + c.ToString()));
});

실행 결과

1,2
3,4
5,6
7,8
9,10

Observable.Range(1, 10)
.Select(x => x.ToString())
.Buffer(2, 1) // 2개 묶음. 방류 후 1개 건너뛰고 방류하다.
.Subscribe(x =>
{
// buffer의 내용을 표시
Debug.Log(x.Aggregate<string>((p, c) => p.ToString() + ", " + c.ToString()));
});

실행 결과

1,2
2,3
3,4
4,5
5,6
6,7
7,8
9,10
10

Observable.Range(1, 10)
.Select(x => x.ToString())
.Buffer(3, 2) // 3개 묶음. 방류 후 2개 건너뛰고 방류하다.
.Subscribe(x =>
{
// buffer의 내용을 표시
Debug.Log(x.Aggregate<string>((p, c) => p.ToString() + ", " + c.ToString()));
});

실행 결과

1,2,3
3,4,5
5,6,7
7,8,9
9,10

이 Buffer의 Skip을 이용하면 쉽게 스트림 메시지의 비교 및 연산을 할 수 있습니다.

예) Buffer를 사용하여 이전 메시지의 차이를 구하기

Buffer(2,1)을 사용하면 쉽게 구현할 수 있습니다. (Zip과 Skip(1)을 이용해서 구현하는 사람도 보았습니다. 하지만 Buffer가 솔직히 더 쉽게 구현할 수 있습니다.)

다만, OnCompleted가 발행되었을 때에 Buffer는 값이 갖추어지지 않아도 방출해 버리는 특성이 있기 때문에, Where에서 필요 수가 갖추어지지 않은 경우 cut하는 등의 처리를 넣어줄 필요가 있습니다.

transform.Postion의 차이를 구한다.

this.UpdateAsObservable()
.Select(_ => transform.position)
.Buffer(2, 1)
.Where(x => x.Count == 2)
.Select(x => x.Last() - x.First())
.Subscribe(x => Debug.Log("Delta: " + x));

예) Buffer을 이용하여 과거 n개의 메시지 값의 평균값을 구한다.

Buffer (n, 1)에 LINQ의 Average를 조합하면 쉽게 얻을 수 있습니다.

지난 10프레임의 Time.deltaTime의 평균 값을 실시간으로 계산

this.UpdateAsObservable()
.Select(_ => Time.deltaTime)
.Buffer(10, 1)
.Select(x => x.Average())
.Subscribe(x => Debug.Log("Average : " + x));

응용: FPS 카운터를 구현하자

정리

Buffer는 단지 값을 막는 유일한 오퍼레이터가 아닙니다. 이와 같이 Skip을 지정할 수 있고, 시간을 구분해 정리하는 범용성이 높은 오퍼레이터이기도 합니다.

Buffer에 국한된 이야기가 아니라, 같은 오퍼레이터라도 오버로드(overload)로 전혀 행동이 달라지거나 하는 것들이 많이 있습니다. 무리하게 오퍼레이터 체인으로 로직을 실현하기 전에, 비슷한 오퍼레이터가 없는지 찾아 그 오버로드를 확인해 보면 좋을 것 같습니다.

추기

Buffer(2,1) 와 비슷한 행동을 하는 Pairwise 는 오퍼레이터도 있습니다.

OnCompleted가 발행되었을 때의 동작이 약간 다른것 이외에는 동일하기 때문에 이전 값만을 사용하고 싶으면 사용하는 것도 좋을지도 모릅니다.

// Pairwise()
Observable.Range(1, 10)
.Pairwise()
.Subscribe(x => Debug.Log($"{x.Previous}, {x.Current}"));

// Buffer (2,1)
Observable.Range(1, 10)
.Select(x => x.ToString())
.Buffer(2, 1)
.Subscribe(x => Debug.Log(x.Aggregate<string>((p, c) => p.ToString() + ", " + c.ToString())));

실행 결과

(Pairwise)
1,2
2,3
3,4
4,5
5,6
6,7
7,8
9,10

(Buffer)
1,2
2,3
3,4
4,5
5,6
6,7
7,8
9,10
10 // ←

· 약 5분
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/10d9112eda30a0ba9278

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

UniRx에 대한 기사 요약은 여기


PhotonRx

PUN의 콜백 통지를 UniRx의 ObservableTrigger 형식으로 취급 할 수 있게 만들어 보았습니다.

PhotonRx

라이센스는 MIT 입니다.

역주: 포톤 v1 기준으로 만들어진 소스입니다. v2에서 사용시에는 해당 소스를 참고하여 변형하면 될 것으로 보입니다.

사용법

using에 PhotonRx를 추가하면, 나머지는 this.xxxAsObservable 형식으로 콜백 통지를 Observable로 취급 할 수 있게 됩니다.

using System;
using PhotonRx; // 추가
using UnityEngine;
using UniRx;

public class PhotonRxSamle : MonoBehaviour {

void Start ()
{
// 플레이어의 입장 이벤트
this.OnPhotonPlayerConnectedAsObservable()
.Subscribe(p => Debug.Log(p.name + "님이 입장했습니다."));

// 플레이어의 퇴장 이벤트
this.OnPhotonPlayerDisconnectedAsObservable()
.Subscribe(p => Debug.Log(p.name+"님이 퇴장했습니다."));
}
}

PhotonRx에 정의되어 있는 xxxAsObservable()는 ObservableTriggerBase와 같은 방식으로 작동 됩니다.

즉, 대상의 GameObject가 삭제되면 자동으로 OnCompleted가 동작하도록 되어 있기 때문에 스트림의 수명 관리도 생각하지 않아도 됩니다.

PhotonRx를 도입해야 하는 이유

  • Photon 콜백이 IDE 보완이 되는 형태로 호출할 수 있게 된다.
  • 형태 안전하게 취급할 수 있게 된다.
  • UniRx의 유연한 비동기 처리에 그대로 올릴 수 있다.

고급 사용법

PhotonRx와 코루틴을 결합하여 비동기 처리를 동기처리처럼 쓸 수 있습니다. (await처럼)

using System;
using System.Collections;
using PhotonRx;
using UnityEngine;
using UniRx;

public class PhotonRxSamle2 : MonoBehaviour
{

private void Start()
{
// 로비에 연결 처리
StartCoroutine(ConnectCoroutine());
}

private IEnumerator ConnectCoroutine()
{
// 로비에 연결 결과를 통지하는 스트림을 생성
// 연결 성공 · 실패 스트림 2 개를 병합하여 두 가지 이벤트 통지의 도달을 기다리는
var loginStream = this.OnJoinedLobbyAsObservable().Cast(default(object))
.Merge(this.OnFailedToConnectToPhotonAsObservable().Cast(default(object)))
.FirstOrDefault() // OnCompleted를 발화하기 위하여
.PublishLast(); // PublishLast는 결과를 캐시하는

// Connect에서 StartAsCoroutine 이전에 스트림 모니터링을 시작
loginStream.Connect();

// 연결 시작
PhotonNetwork.ConnectUsingSettings("0.1");

// 결과 저장을 위한 객체
var result = default(object);

// StartAsCoroutine은 대상 스트림의 OnCompleted가 발행 될 때까지 null를 돌려주는 (코루틴에서 대기)
yield return loginStream.StartAsCoroutine(x => result = x, ex => { });

// 결과의 형태를보고 판정
if (result is DisconnectCause)
{
Debug.Log("연결실패");
//
// 여기에 실패 처리를 쓰는
//
yield break;
}

Debug.Log("연결성공");

// 다음 처리를 계속한다면 여기에 쓰기

}
}

내부는 StartAsCoroutine 입니다.

자세한 내용은 [UniRx] UniRx와 코루틴을 참고하시기 바랍니다.

만든 경위

15/6/19의 UniRx 연구회에서 강연 된 gloops의 모리나가 씨의 젋은 엔지니어에서 본 UniRx를 이용한 게임 개발 이라는 슬라이드에서 ObservableTrigger로 PUN의 콜백을 처리하면 편리하고 행복했다는 내용이 있었습니다.

확실히 Trigger 기반으로 해 버리면 다루기 쉽고 성능도 좋기 때문에 이것은 곧 사용할 수 밖에 없다고 생각하고 구현을 보았습니다.

· 약 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/48b9fa25df64d3c6a392

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


요약

과거에 자신이 쓴 UniRx (Rx 포함)의 포스트 목록을 작성해 보았습니다.

여러분에게 도움이 되면 좋겠다고 생각합니다.

포스트를 쓴 당시와 비교하면 현재는 더 좋은 구현 방법이 있는 포스트도 있습니다.

UniRx 입문 시리즈

과거에 만든 슬라이드

역방향

Rx의 기본 사항

Hot/Cold에 대해

Rx의 까다로운 성격 중 1개인, Observable의 Hot/Cold에 대해서 정리한 것입니다.

UniRx를 중심으로 한 설계를 진행하는 경우 이 성질의 이해는 필수입니다.

기타 Rx 설명

PairWise/Buffer(2,1)에서 끝나는 곳을 Zip과 Skip(1)에서 쓰는 사람이 많다는 인상을 받습니다.

UniRx 기능 소개

Update()를 UniRx로 재 작성

UniRx를 도입하면 가장 먼저 해야 할 것 입니다. Update()의 재 작성 방법에 대해 정리해 보았습니다.

우선 UpdateAsObservableObserveEveryValueChanged 를 사용하면 좋을 것으로 생각합니다.

코루틴과 함께한 예

UniRx와 코루틴의 조합함으로써 진가를 발휘한다고 해도 과언이 아닙니다.

코루틴과의 조합법에 능숙해 지면 좋을것으로 생각합니다.

기타

AddTo는 자주 사용하는 기능이므로 기억해야 합니다.

예제 요약

판정의 개선

솔직히 Raycast를 사용하는게 더 안정적입니다.

코루틴으로 쓰는 것이 복잡하지만 나중에 확장하기 쉽고 좋을지도 모르겠습니다.

PhotonCloud의 활용도를 높이는

PhotonRx는 Photon을 사용하기 쉽게 하기 위한 하나의 해결책 인 것 같습니다.

다른 구현 예

"일단 시도 해 보았다" 정도의 포스트 입니다. 너무 깊게 받아들이지 마세요.

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

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

UniRx에 대한 기사 요약은 여기


이 기사는 좀 오래되기 때문에 아래 새로운 문서를 참조하십시오

UniRx 입문 5부 - 코루틴과 함께 -


UniRx는 코루틴과 결합하면 좀 더 편리해집니다.

이번에는 그 방법을 소개하겠습니다.

코루틴을 Observable로 변환

Observable.FromCoroutine 을 사용함으로써 코루틴을 Observable로 변환 할 수 있습니다.

FromCoroutine 대해서는 @neuecc 씨 본인이 자세히 설명하고 있으므로, 여기를 참고하는 것이 가장 좋을것 같습니다.

Unity의 코루틴의 분해 혹은 UniRx의 MainThreadDispatcher에 대해

FromCoroutine은 특히 코루틴에서 반환하는 Observable을 만드는 방법을 사용할 수 있어 범용성이 높고, 팩토리 메서드나 오퍼레이터를 조합하는 것 보다 더 간결하게 처리할 수 있습니다.

예) 일정 조건 일때만 카운트 다운하는 타이머를 코루틴으로 만들기

반환값이 있는 Observable을 코루틴에서 만들기

private void Start()
{
// player 초기화 같은 로직 실행

// 플레이어의 생존 시간을 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--);
}
yield return new WaitForSeconds(1.0f);
}
observer.OnCompleted();
}

Observable을 코루틴으로 변환

Observable을 코루틴으로 변환하는 경우 StartAsCoroutine를 사용합니다.

private void Start()
{
StartCoroutine(CoroutineA());
}

IEnumerator CoroutineA()
{
// 코루틴의 실행 결과를 저장하는 변수
var result = 0;
// Observable.Range을 코루틴으로 변환한다.
yield return Observable
.Range(0, 10)
.StartAsCoroutine(c => result = c);

Debug.Log("result : " + result);
}

실행 결과

result : 9

StartAsCoroutine은 다음과 같은 특징이 있습니다.

  • OnCompleted가 발행 될 때까지 yield return null을 계속 반복 한다.
  • StartAsCoroutine 인수로 전달 된 함수는 OnCompleted 발행 시에 1번만 실행되고 마지막 OnNext 값이 전달 된다.

StartAsCoroutine의 용도

StartAsCoroutine을 잘 사용하면 비동기 작업이 뒤얽힌 처리를 동기화스럽게 처리 할 수 있게 됩니다.

이른바 Task의 await 처리 같은 것을 제공 할 수 있게 됩니다.

private IEnumerator HeavyTaskCoroutine()
{
// 실행 결과
bool result = false;

// 비동기 처리 대기
// Observable.Start 다른 스레드에서 작업을 수행 한다.
yield return Observable
.Start(() => HeavyTask())
.StartAsCoroutine(x => result = x);

// 실행 결과를 확인한다.
if (result)
{
Debug.Log("Success");
}
else
{
Debug.Log("Failure");
}
}

/// <summary>
/// 실행에 시간이 걸릴 무거운 처리
/// </ summary>
/// <returns> 성공 여부 </ returns>
bool HeavyTask()
{
// 무거운 처리
Thread.Sleep(3000);

// 실행의 성공 여부를 반환 (의사적으로 랜덤하게 true/false를 반환)
var random = new System.Random();
return random.Next() % 2 == 0;
}

정리

UniRx는 코루틴과 결합하면 더 진가를 발휘합니다.

모든 것을 Rx 스트림에서 무리하게 쓰는 것이 아니라, FromCoroutine과 StartAsCoroutine을 잘 활용하면 읽기 쉽고 사용하기 쉬운 코드를 작성할 수 있습니다.

· 약 8분
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/584d5bc706e4b1dca3bb

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

UniRx에 대한 기사 요약은 여기


먼저 [UniRx] UniRx와 코루틴을 읽어 주시면 이해하기 쉬울 것으로 생각 합니다.

시작하기

PhotonCloud는 기본적으로 콜백으로 통지되는 구조로 되어 있습니다.

그 때문에, 처리가 곳곳에 분산 되어, 읽기 힘든 코드가 되는 경향이 있습니다.

이번에는 UniRx의 StartAsCoroutine을 사용하여 Photon 로그인 처리 부분을 동기처리 처럼 써 보겠습니다.

Photon 콜백을 Observable로 변환

우선 Photon 콜백을 Observable로 제공하는 싱글톤을 만듭니다.

싱글톤은 tsubaki 블로그의 SingletonMonoBehaviour를 사용하여 만듭니다.

/// <summary>
/// Photon 콜백을 Observable로 제공하는 싱글 톤
/// </summary>
public class PhotonCallbacks : SingletonMonoBehaviour<PhotonCallbacks>
{
#region Server
/// <summary>
/// 서버 연결 결과를 통지 할 Subject
/// </summary>
private Subject<ConnectSeverResult> _connectToSeverResutlSubject;

/// <summary>
/// 서버 연결의 결과를 통지 할
/// </summary>
public IObservable<ConnectSeverResult> ConnectToSeverObservable
{
get
{
if(_connectToSeverResutlSubject==null) _connectToSeverResutlSubject = new Subject<ConnectSeverResult>();
return _connectToSeverResutlSubject.AsObservable().First(); // OnCompleted를 발행하기위한 First
}
}

/// <summary>
/// 서버에 연결 성공
/// </summary>
private void OnConnectedToPhoton()
{
if(_connectToSeverResutlSubject!=null) _connectToSeverResutlSubject.OnNext(new ConnectSeverSuccess());
}


/// <summary>
/// Photon 서버에 연결해 않았다
/// </summary>
/// <param name='cause'>원인</param>
private void OnFailedToConnectToPhoton(DisconnectCause cause)
{
if (_connectToSeverResutlSubject != null) _connectToSeverResutlSubject.OnNext(new ConnectSeverFail(cause));
}
#endregion

#region Lobby
/// <summary>
/// 로비에 연결 한 것을 통지하는 Subject
/// </summary>
Subject<Unit> _onJoinedLobby;

/// <summary>
/// 로비 입실 성공을 알린다.
/// </summary>
public IObservable<Unit> OnJoinedLobbyAsObservable
{
get
{
if (_onJoinedLobby == null) _onJoinedLobby = new Subject<Unit>();
return _onJoinedLobby.First();
}
}

/// <summary>
/// 로비에 입장 한다.
/// </summary>
private void OnJoinedLobby()
{
if (_onJoinedLobby != null)
_onJoinedLobby.OnNext(Unit.Default);
}
#endregion
}

/// <summary>
/// 서버 연결의 성공 여부 판정 클래스
/// </summary>
public abstract class ConnectSeverResult { }

/// <summary>
/// 성공
/// </summary>
public class ConnectSeverSuccess : ConnectSeverResult{}

/// <summary>
/// 실패
/// </summary>
public class ConnectSeverFailuer : ConnectSeverResult
{
private DisconnectCause disconnectCause;
/// <summary>
/// 실패 원인
/// </summary>
public DisconnectCause Cause { get { return disconnectCause; } }
public ConnectSeverFailuer(DisconnectCause cause)
{
disconnectCause = cause;
}
}

ConnectToSeverObservable 를 Subscribe하여 서버 연결 여부에 성패가 흘러 오게 되어 있습니다.

(성공 여부 판정에 ConnectSeverResult 라는 클래스를 일부러 만들었습니다. 하지만 이 부분은 더 나은 방법이 있나 고민 중입니다.)

역주: 아래는 PhotonCloud v2 버전에 맞게 수정 된 스크립트 입니다.

using System;
using Photon.Realtime;
using UniRx;

// Photon 콜백을 Observable로 제공하는 싱글 톤
public class PhotonCallbacks : PhotonSingletonMonoBehaviourFast<PhotonCallbacks>
{
#region Server
/// <summary>
/// 서버 연결 결과를 통지 할 Subject
/// </summary>
private Subject<ConnectServerResult> _connectToSeverResultSubject;

/// <summary>
/// 서버 연결의 결과를 통지 한다.
/// </summary>
public IObservable<ConnectServerResult> ConnectToSeverObservable
{
get
{
if (_connectToSeverResultSubject == null)
_connectToSeverResultSubject = new Subject<ConnectServerResult>();

return _connectToSeverResultSubject.AsObservable().First(); // OnCompleted를 발행하기 위한 First 선언
}
}

/// <summary>
/// 서버에 접속 성공했다.
/// </summary>
public override void OnConnected()
{
base.OnConnected();

_connectToSeverResultSubject?.OnNext(new ConnectServerSuccess());
}

/// <summary>
/// Photon 서버와 연결이 끊겼다.
/// </summary>
/// <param name = 'cause'> 원인 </param>
public override void OnDisconnected(DisconnectCause cause)
{
base.OnDisconnected(cause);

_connectToSeverResultSubject?.OnNext(new ConnectServerFail(cause));
}
#endregion

#region Lobby
/// <summary>
/// 로비에 연결 한 것을 통지하는 Subject
/// </summary>
private Subject<Unit> _onJoinedLobby;

/// <summary>
/// 로비 입장 성공을 알린다.
/// </summary>
public IObservable<Unit> OnJoinedLobbyAsObservable
{
get
{
if (_onJoinedLobby == null)
_onJoinedLobby = new Subject<Unit>();

return _onJoinedLobby.First();
}
}

/// <summary>
/// 로비에 입장 한다.
/// </summary>
public override void OnJoinedLobby()
{
base.OnJoinedLobby();

_onJoinedLobby?.OnNext(Unit.Default);
}
#endregion
}

using Photon.Realtime;

/// <summary>
/// 서버 연결의 성공 여부 판정 클래스
/// </summary>
public abstract class ConnectServerResult { }

/// <summary>
/// 성공
/// </summary>
public class ConnectServerSuccess : ConnectServerResult { }

/// <summary>
/// 실패
/// </summary>
public class ConnectServerFail : ConnectServerResult
{
/// <summary>
/// 실패 원인
/// </summary>
public DisconnectCause Cause { get; }

public ConnectServerFail(DisconnectCause cause) => Cause = cause;
}

수정 부분

  • PhotonCloud v2 부터는 PhotonCloud의 콜백은 2종류. PhotonSingletonMonoBehaviourFast 스크립트 생성해서 포톤 전용 싱글톤을 만들었다. (SingletonMonoBehaviourFast 와 구조는 동일하고, MonoBehaviour 대신에 MonoBehaviourPunCallbacks를 상속 받는 구조)
  • 기존 콜백 네이밍이 변경되어 해당 부분은 수정.
    • OnConnectedToPhoton → OnConnected
    • OnFailedToConnectToPhoton → OnDisconnected
    • OnJoinedLobby → OnJoinedLobby

코루틴과 StartAsObservable을 사용하여 로그인 처리

PhotonCallbacks가 준비되면 코루틴 내에서 처리를 진행하면 됩니다.

다음은 Button을 눌렀을 때 로그인을 시도하는 코드 입니다.

using System.Collections;
using Photon.Pun;
using UniRx;
using UnityEngine;
using UnityEngine.UI;

public class ConnectToPhotonServerSample : MonoBehaviour
{
private PhotonCallbacks _callbacks;

private void Start()
{
_callbacks = PhotonCallbacks.Instance;

// 버튼이 눌려 졌을 때 로그인 프로세스 시작
GetComponent<Button>()
.OnClickAsObservable()
.Subscribe(_ => StartCoroutine(LoginTask()));
}

/// <summary>
/// Photon 로그인 처리를 한다.
/// </summary>
private IEnumerator LoginTask()
{
Debug.Log("서버에 연결 시작");

// 연결 시작
PhotonNetwork.GameVersion = "0.1";
PhotonNetwork.ConnectUsingSettings();

// 성공 여부 판정 저장 용
ConnectServerResult serverResult = null;

// 로그인 처리의 종료를 대기
yield return _callbacks
.ConnectToSeverObservable
.StartAsCoroutine(x => serverResult = x);

// if 문에서 성공 여부를 판정 할 수 있다
if (serverResult is ConnectServerFail)
{
Debug.LogError("로그인 실패");
Debug.LogError((ConnectServerFail) serverResult);
// 결과가 실패하면 종료
yield break;
}

// 로그인 성공 후 계속 진행
Debug.Log("로그인 성공");

// 로비 입실을 대기 한다
yield return _callbacks
.OnJoinedLobbyAsObservable
.StartAsCoroutine();

Debug.Log("로비 입실 완료");

// 다음 로그인 후 처리를 진행 한다.
}
}

정리

StartAsCoroutine을 사용함으로써 코루틴 중에서 Task의 await 처리를 비슷하게 사용할 수 있기 때문에 비동기 처리를 동기 처리 처럼 쓸 수 있었습니다.

로그인 메인 처리 부분은 동기 처리로 아주 읽기 쉽게 만들어졌지만, 대신 뒤죽박죽한 부분이 PhotonCallbacks 내부에 들어간 느낌 입니다.

아직 이러한 방법으로 구현을 시작한지 얼마 되지 않아 앞으로도 이러한 방법으로 계속 진행 할지는 모르겠지만, 잘 되면 다시 포스트로 정리하고 싶습니다.

· 약 9분
karais89

환경

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

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

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

UniRx에 대한 기사 요약은 여기


이전 Rx의 Hot과 Cold 내용으로 Observable의 Cold와 Hot 성질에 대해 설명했습니다.

이번에는 보다 구체적으로, 어떤 상황에서 Hot 변환을 하는지를 설명하고 싶습니다.

Hot 변환 한 포인트

여러 상황이 있지만 가장 Hot 변환이 중요해지는 상황은 하나의 스트림을 여러 번 Subscribe하는 경우 입니다. 실제 코드를 보면서 설명 하겠습니다.

예) 입력된 문자열이 특정 키워드와 일치하는지 검사

Hot 변환이 필요한 예로 "입력된 키 입력을 감지하고 4 문자의 특정 키워드가 입력되었는지를 알아 내는 스트림"을 만들어 보겠습니다.

준비

우선은 준비 단계로 입력된 키 정보를 4글자씩 내놓는 스트림을 만듭니다.

var keyBufferStream
= Observable.FromEvent<KeyEventHandler, KeyEventArgs>(
h => (sender, e) => h(e),
h => KeyDown += h,
h => KeyDown -= h)
.Select(x => x.Key.ToString()) // 입력 키를 문자로 변환
.Buffer(4, 1) // 4 개씩 정리
.Select(x => x.Aggregate((p, c) => p + c)); // 문자에서 문자열로 변환

// 결과를 표시하고 보니
keyBufferStream.Subscribe(Console.WriteLine);

실행결과 예(ABCDEFGH 키 입력 결과)

ABCD
BCDE
CDEF
DEFG
EFGH

이같이 keyBufferStream은 입력 키가 4자씩으로 뭉치고 흐르는 스트림입니다.

역주: 아래는 유니티에서 실행 가능한 예제 입니다.

var keyBufferStream = this.UpdateAsObservable()
.Where(_ => Input.anyKeyDown) // 아무 버튼 눌렀을 때
.Where(_ => !(Input.GetMouseButtonDown(0) || Input.GetMouseButtonDown(1) || Input.GetMouseButtonDown(2))) // 마우스는 무시
.Select(_ => Input.inputString) // 버튼 스트링
.Buffer(4, 1) // 4 개씩 정리
.Select(x => x.Aggregate((p, c) => p + c)); // 문자에서 문자열로 변환

// 결과 표시
keyBufferStream.Subscribe(Debug.Log);

Aggregate는 Linq 에서 지원 하는 메서드이며, 집계 연산자 입니다. 마지막 값을 돌려주는 메소드 입니다.

keyBufferStream을 사용하여 "HOGE" 또는 "FUGA"의 입력을 감시하자

그럼 이 keyBufferStream을 사용하여 "HOGE"와 "FUGA"를 감시해 봅시다.

Where 사이 HOGE와 FUGA에서 2회 Subscribe 합니다.

keyBufferStream.Where(x => x == "HOGE")
.Subscribe(_ => Debug.Log("Input HOGE"));

keyBufferStream.Where(x => x == "FUGA")
.Subscribe(_ => Debug.Log("Input FUGA"));

실행 결과 (HOGEFUGA 입력 한 결과)

Input HOGE
Input FUGA

각각의 문자열에 반응하는 스트림을 만들어 Subscribe 할 수 있었습니다.

만.. 이 스트림에는 커다란 문제가 있습니다.

무엇이 문제인가?

상기 스트림은 무엇이 문제인가? 그것은 keyBufferStream이 Cold Observable로 형성되는 것이 문제 입니다. 이전의 포스트에서도 설명했지만, (Cold Observable은 분기하지 않습니다.) Subscribe 할 때마다 매번 새로운 스트림을 생성하는 특성이 있습니다.

따라서 상기와 같은 작성을 해 버리면 다음과 같은 문제가 발생할 수 있습니다.

  • 뒤에서 다중 스트림이 생성되어 버립니다. 메모리와 CPU를 낭비합니다.
  • Subscribe 한 시점에서 따라 흘러 나오는 결과가 다릅니다. (참고 Cold Observable의 성질)
    • 역주: Cold Observable은 Subscribe 한 순간부터 오퍼레이터가 작동하게 됩니다. Subscribe 전에 온 메시지는 모든 처리 조차 되지 않고 소멸 됩니다.

스트림이 2개로 흐른다는 증거

var keyBufferStream
= Observable.FromEvent<KeyEventHandler, KeyEventArgs>(
h => (sender, e) => h(e),
h => KeyDown += h,
h => KeyDown -= h)
.Select(x => x.Key.ToString())
.Buffer(4, 1)
.Do(_=> Console.WriteLine("Buffered")) // Buffer가 OnNext를 방출한 타이밍에 출력된다.
.Select(x => x.Aggregate((p, c) => p + c));

keyBufferStream
.Where(x => x == "HOGE")
.Subscribe(_ => Console.WriteLine("Input HOGE"));

keyBufferStream
.Where(x => x == "FUGA")
.Subscribe(_ => Console.WriteLine("Input FUGA"));

실행 결과(AAAA와 Buffer가 1번만 움직이도록 키 입력)

Buffered
Buffered // Buffer는 1회만 흐르고 있을텐데 2번 출력되고 있다 = 스트림이 2개로 흐르고 있다.

Hot Observable이 스트림의 근원인 FromEvent 밖에 없기 때문에, Subscribe 할 때마다 FromEvent로부터 새롭게 스트림이 생성되어 버리는 움직임이 되고 있습니다.

문제의 해결책 "Hot 변환"

여기에서 첫번째 "Hot 변환은 하나의 스트림을 동시에 여러 Subscribe하는 경우에 사용한다" 라는 이야기로 돌아갑니다.

즉 Hot 변환하여 스트림의 분기점을 만들어 여러 Subscribe 했을 때 스트림을 하나로 통합 할 수 있게 되는 것입니다.

Hot 변환 된 예

var keyBufferStream
= Observable.FromEvent<KeyEventHandler, KeyEventArgs>(
h => (sender, e) => h(e),
h => KeyDown += h,
h => KeyDown -= h)
.Select(x => x.Key.ToString())
.Buffer(4, 1)
.Select(x => x.Aggregate((p, c) => p + c))
.Publish() // Publish에서 Hot 변환(Publish가 대표하여 Subscribe 해 준다)
.RefCount(); // RefCount은 Observer가 추가되었을 때 자동 Connect 해 주는 오퍼레이터.

keyBufferStream
.Where(x => x == "HOGE")
.Subscribe(_ => Console.WriteLine("Input HOGE"));

keyBufferStream
.Where(x => x == "FUGA")
.Subscribe(_ => Console.WriteLine("Input FUGA"));

실행 결과(HOGEFUGA 입력)

Input HOGE
Input FUGA

Hot 변환 방식에는 여러 가지가 있지만 가장 쉬운 것이 Publish()와 RefCount()를 결합 하는 것 입니다.

이번에는 Hot 변환의 필요성에 대해 설명하고 싶기 때문에 Publish와 RefCount의 상세한 설명은 생략하겠습니다.(자세한 설명은 여기)

역주: UniRx에서는 Publish()와 RefCount()의 결합인 Share() 오퍼레이터를 제공 합니다. 두개를 사용해야 될 경우에는 Share()를 사용하시면 됩니다.

정리

  • 스트림을 의도적으로 분기 하고 싶을 때 Hot 변환을 수행 한다.
  • 스트림을 생성하여 반환하는 속성과 함수를 정의하면 끝에 Hot 변환을 하는 것이 안전하다.
  • Hot 변환을 잊어 버리면 메모리나 CPU가 낭비되거나 Subscribe 타이밍이 어긋날 수 있다.
  • Hot 변환 오퍼레이터는 몇 개 있지만, Publish() + RefCount()의 조합이 편리하다 (만능은 아니다)

참고 Introduction Rx - Hot and Cold observables

· 약 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/49ed5a052e8be4386c9e

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


Unity 보다는 C#의 이야기 입니다. C# 메서드의 반환 값에 하나의 유형만 지정할 수 없습니다.

역주: C# 7.0 부터는 Tuple라는 스펙이 추가되어 반환 값에 여러가지 유형을 지정할 수 있습니다.

그래서 여러 매개 변수를 메서드로 반환하는 경우 어떻게 하면 좋은가를 소개하고 싶습니다.

스스로 새로운 클래스나 구조체를 정의한다.

가장 무난하고 전통적인 방법. 스스로 그러한 여러 유형을 내부에 있는 클래스나 구조체로 만들어 버린다. 용도에 맞게 일일이 클래스를 정의하는 것은 상당히 귀찮은 것이 단점이지만, 코드의 가독성은 상당히 높아진다.

Option형이나 Case Class 같은 것이 C#에도 있으면 더 편할 것 같습니다.

struct LoadDataModel
{
public bool IsSuccess { get; set; }
public string Data { get; set; }

public LoadDataModel(bool isSuccess, string data)
{
IsSuccess = isSuccess;
Data = data;
}
}

private void Start()
{
var loadData = LoadData();
if (loadData.IsSuccess)
{
Debug.Log(loadData.Data);
}
}

/// <summary>
/// 데이터를 로드 한다.
/// </summary>
/// <returns> </returns>
private LoadDataModel LoadData()
{
try
{
// 뭔가를 로드 하는 처리
var data = "로드 된 데이터";
return new LoadDataModel(true, data);
}
catch (Exception e)
{
return new LoadDataModel(false, null);
}
}

out 키워드를 사용

Physics.Raycast 등에서도 등장 out을 사용하는 형태.

메서드의 인수 정의에 out을 지정하여 그 인수를 출력하는 변수를 선언 할 수 있습니다.

단지 out을 사용하면 코드의 가독성이 떨어져서, 저는 별로 좋아하지 않습니다.

private void Start()
{
string loadData;
var isSuccessed = LoadData(out loadData);
if (isSuccessed)
{
Debug.Log(loadData);
}
}

/// <summary>
/// 데이터를 로드 한다.
/// </summary>
/// <param name="data">로드 성공시 값</param>
/// <returns>로드 성공 여부</returns>
private bool LoadData(out string data)
{
try
{
// 뭔가 로드 처리
data = "로드 된 데이터";
// 성공하면 true 반환
return true;
}
catch (Exception e)
{
data = null;
// 예외가 나오면 false 반환
return false;
}
}

역주: C# 7.0 부터 inline out 기능이 추가되어 조금 더 가독성 있는 코드를 작성 할 수 있게 되었습니다.

private void Start()
{
if (LoadData(out var loadData))
{
Debug.Log(loadData);
}
}

Tuple을 사용한다. - UniRx

Tuple(튜플)을 사용하는 방법. 그러나 C# 표준 Tuple은 Unity에서 사용할 수 없습니다. UniRx는 UniRx.Tuple로 호환되는 것을 만들었습니다. Tuple을 사용한다면 UniRx를 도입하고 UniRx.Tuple을 사용합시다.

protected void Start()
{
var result = LoadData();
if (result.Item1)
{
var loadData = result.Item2;
Debug.Log(loadData);
}

}

private Tuple<bool, string> LoadData()
{
try
{
// 여기에서 로드 처리
return new Tuple<bool, string>(true,("로드된 데이터"));
}
catch
{
return new Tuple<bool, string>(false, null);
}
}

역주: net 4.0 이상 버전에서는 아예 UniRx.Tuple는 사용하지 못하고, C#에서 기본적으로 제공하는 Tuple을 사용해야 합니다.

Tuple을 사용한다 - C# 7.0 이상

private void Start()
{
var result = LoadData();
if (result.Item1)
{
var loadData = result.Item2;
Debug.Log(loadData);
}

// 위와 같은 형태, 명시적으로 변수명 선언
var (isSuccesses, loadData2) = LoadData();
if (isSuccesses)
{
Debug.Log(loadData2);
}
}

private (bool isSuccessed, string data) LoadData()
{
try
{
// 여기서 로드 처리
return (true, "로드 된 데이터");
}
catch
{
return (false, null);
}
}

C# 7.0 부터 제공되는 Tuple 기능을 사용하는 방식입니다. Tuple을 사용해야 되는 경우라면 C#에서 제공해주는 Tuple을 사용하면 됩니다.

정리

클래스나 구조체를 정의해서 사용하는 것이 가장 간단한 설계라고 생각 합니다.

out이나 Tuple은 남용하면 코드의 가독성과 유지 보수성이 떨어지므로, 사용할때는 주의해서 사용하도록 합시다.

· 약 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/59d10ddec2e89b86600c

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

UniRx에 대한 기사 요약은 여기


Rx의 Repeat는 자주 사용하는 것에 비해, 초심자가 어떻게 동작 하는지 파악하기 힘든 오퍼레이터라고 생각합니다.

그래서 Repeat에 대해 조사해 정리해 보았습니다.

Rx의 "Repeat"란?

즉 Repeat는 "OnCompleted가 왔을 때 다시 Subscribe 해주는 오퍼레이터" 입니다. (더 정확히 말하면, OnCompleted가 왔을 때 같은 스트림을 생성하고 Concat으로 뒤에 연결해주는 오퍼레이터 입니다.)

드물게 "흘러온 메시지를 재현하고 반복 해주는 오퍼레이터"라고 착각하는 사람이 있지만 그렇지 않습니다.

예를 들어 다음의 코드를 참조하십시오.

var random = new Random();

// 난수를 하나 반환 스트림
Observable.Create<int>(observer =>
{
observer.OnNext(random.Next());
observer.OnCompleted();
return () => { };
})
.Repeat(3)
.Subscribe(x => Debug.WriteLine(x), () => Debug.WriteLine("OnCompleted"));

역주: 아래는 UniRx 위 동작과 동일한 구현을 한 예시 입니다. (UniRx에서는 Repeat 오퍼레이터에 인자 값을 넣는 기능이 없네요.) 그리고 Repeat를 사용하면, 에러가 발생 (NullException, Stack OverFlow 등) 하여 RepeatUntilDestroy로 변경 하였습니다.

var random = new System.Random();

// 난수를 1개 반환하는 스트림
Observable.Create<int>(observer =>
{
observer.OnNext(random.Next());
observer.OnCompleted();
return Disposable.Empty;
})
.RepeatUntilDestroy(gameObject)
.Take(3)
.Subscribe(x => Debug.Log(x), () => Debug.Log("OnCompleted!"));

실행결과

30200140
1282005623
1140074942
OnCompleted!

3회 모두 다른 값이 OnNext에 흘러 왔습니다.

이것은 Repeat 타이밍에서 Observable.Create가 다시 실행되고 있기 때문 입니다.

이렇게 Repeat는 OnCompleted를 감지 한 순간에 다시 Subscribe하고 Concat으로 스트림 뒤에 새로운 스트림을 다시 연결 해 주는 오퍼레이터 입니다.

Repeat는 값을 캐시하고 반복해서 같은 값을 흘리는 기능을 가지고 있지 않습니다.

Repeat의 용도

용도 1) 스트림을 반복 Subscribe하기

OnCompleted가 발행되었을 때 다시 한번 같은 스트림을 Subscribe하는 간단한 방법입니다.

마우스 드래그 모니터링을 반복

var mouseMove = this.UpdateAsObservable()
.Select(_ => Input.mousePosition);
var mouseDown = this.OnMouseDownAsObservable();
var mouseUp = this.OnMouseUpAsObservable();

mouseMove.SkipUntil(mouseDown)
.TakeUntil(mouseUp)
.RepeatUntilDestroy(gameObject)
.Subscribe(pos => Debug.Log(pos.x));

용도 2) 오퍼레이터 상태를 초기화 하기

아까도 설명했지만, Repeat는 OnCompleted가 왔을 때 Subscribe를 다시 하는 오퍼레이터 입니다.

다시 SubScribe 해준다는 것이 핵심입니다.

Rx 대부분의 오퍼레이터는 Subscribe시 생성되는 성질이 있습니다. 따라서 "다시 Subscribe 한다 = 오퍼레이터를 모두 초기화 한다"라고 생각할 수 있습니다.

5초 지나면 카운터를 다시 0으로 초기화

Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1))
.Take(5)
.Repeat()
.Subscribe(time => Debug.Log(time));

클릭하면 타이머를 다시 0으로 초기화

var mouseClick = this.UpdateAsObservable()
.Where(_ => Input.GetMouseButtonDown(0));

Observable.Timer(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(1))
.TakeUntil(mouseClick)
.Repeat()
.Subscribe(time => Debug.Log(time));

재설정 이벤트가 오면 Buffer를 지운다

hogeStream.Buffer(10)
.TakeUntil(resetStream)
.Repeat()
.Subscribe(data => Debug.WriteLine(data.Count));

TakeUntil + Repeat 또는 First + Repeat를 스트림 중간에 끼워 주는 것으로 언제라도 스트림을 초기화 할 수 있습니다.

Repeat를 사용하는데 있어서 주의해야 할 점

무한 Repeat는 상당히 위험하다는 것을 인식해야 합니다.

무한 Repeat는 주의하지 않으면 프리징이나, stack overflow를 일으키게 됩니다.

위험한 예) 팩토리 메서드의 무한 Repeat

팩토리 메서드 중 Subscribe 직후에 OnNext/OnCompleted를 발행하는 것이 몇 개 있습니다. 이렇게 Subscribe 직후 OnCompleted를 반환하는 Observable의 경우 Repeat가 무한히 반복 하여 프리징이나 stack overflow가 발생하여 죽어 버립니다.

Return은 무한 Repeat는 위험하다.

Observable.Return(0)
.Repeat()
.Subscribe(x => Debug.WriteLine(x));

그러나 모든 팩토리 메서드가 위험하다는 것은 아닙니다.

예를 들어 Observable.Timer는 무한 Repeat 문제 없이 사용할 수 있습니다.

(그러나 Dispose를 빼먹을 수 있으니 주의가 필요 합니다.)

위험한 예 2) 스트림의 근원에서 OnCompleted가 발행되는 경우

Subscribe하고 있는 스트림의 근원에서 OnCompleted가 발행된 경우 또는 Hot 변환 스트림에서 OnCompleted가 발행된 경우 무한 Repeat가 발생합니다.

Subject의 무한 Repeat

var subject = new Subject<int>();
subject.Materialize().Repeat().Subscribe(x => Debug.WriteLine(x));

subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3);
subject.OnCompleted();

실행 결과

OnNext(1)
OnNext(2)
OnNext(3)
OnCompleted()
OnCompleted()
OnCompleted()
OnCompleted()
OnCompleted()
.
.
(이하 무한 OnCompleted())

스트림의 근원이 OnCompleted가 되어 버리면 여러 번 Subscribe해도 즉시 OnCompleted가 반환 되어버립니다. 그 때문에 Repeat를 끼우고 있으면 무한히 Repeat를 시도 하고 죽게 됩니다.

따라서 Repeat를 사용하는 경우 스트림의 근원에서 OnCompleted가 발행되는 이전에 Dispose를 호출하는 등의 스트림의 수명 관리를 제대로 할 필요가 있습니다.

정리

Repeat는 OnCompleted가 왔을 때 Subscribe를 다시 해주는 오퍼레이터이며, 값을 유지하고 반복해준다는 기능은 없는 오퍼레이터 입니다.

또한 Repeat를 사용하는 경우 OnCompleted의 발행 타이밍에 주의를 기울일 필요가 있고, 방심하면 곧바로 프리징 또는 stack overflow를 발생 시킬 수 있습니다.

보충) Observable.Return

다음 코드와 실행 결과를 참조하십시오.

var random = new Random();

// 난수를 반환
Observable.Return(random.Next())
.RepeatUntilDestroy(gameObject)
.Take(3)
.Subscribe(x => Debug.Log(x), () => Debug.Log("OnCompleted"));

실행 결과

873345220
873345220
873345220
OnCompleted

Observable.Return을 Repeat하면 Observable.Create 때와는 달리 항상 같은 값이 반복되어 버립니다.

이것은 Observable.Return이 지연 평가가 아닌 것이 원인이며, Repeat에 기인하는 것은 아닙니다.

만약 Observable.Return을 지연 평가 한 후 Repeat하려는 경우(정말 필요하다면??)는 Observable.Defer로 Observable.Return을 감싸면 됩니다.

var random = new Random();
Observable.Defer(() => Observable.Return(random.Next()))
.RepeatUntilDestroy(gameObject)
.Take(3)
.Subscribe(x => Debug.Log(x), () => Debug.Log("OnCompleted"));

실행 결과

1413311669
1173910711
1011315106
OnCompleted

Defer로 포장하여 Repeat 다시 Subscribe시까지 평가를 지연시킬 수 있습니다.

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

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


Unity로 게임 개발을 하고 있으면 "어떤 처리를 일정시간 후나 특정 타이밍에 실행하고 싶다"라고 하는 일이 자주 있기 때문에, 방법을 정리해 보았습니다.

Unity의 표준 기능만 사용하는 경우

처리를 n초 후에 실행하고 싶다

Invoke를 사용

private void Start()
{
// DelayMethod을 3.5 초 후에 호출
Invoke("DelayMethod", 3.5f);
}

private void DelayMethod()
{
Debug.Log("Delay call");
}

아마 가장 간단한 방법일 것입니다.

Invoke를 사용하는 것으로 지정한 메서드를 n초 후에 실행시킬 수 있습니다.

그러나 메서드를 문자열로 지정하지 않으면 안되고, 메서드에 인수를 전달할 수 없는 등 쓰기가 불편한 부분이 있습니다.

코루틴을 사용

private void Start()
{
// 3.5 초 후에 실행
StartCoroutine(DelayMethod(3.5f, () =>
{
Debug.Log("Delay Call");
}));
}

/// <summary>
/// 전달 된 처리를 지정 시간 이후에 실행 한다.
/// </summary>
/// <param name="waitTime">지연시간[밀리초]</param>
/// <param name="action">수행할 작업</param>
/// <returns></returns>
private IEnumerator DelayMethod(float waitTime, Action action)
{
yield return new WaitForSeconds(waitTime);
action();
}

코루틴과 WaitForSeconds를 함께 쓰는 방식.

Invoke와 달리 타입에 안전하고, 인수도 줄 수 있어 이쪽을 사용하는 것이 더 좋다고 생각 합니다.

MonoBehaviour에 확장 메서드를 추가하면 유용 합니다.

n 프레임 후에 실행하고 싶다

코루틴을 사용

private void Start()
{
// 5 프레임 후에 실행
StartCoroutine(DelayMethod(5, () =>
{
Debug.Log("Delay Call");
}));
}

/// <summary>
/// 전달된 처리를 지정 프레임 이후에 실행
/// </summary>
/// <param name="delayFrameCount">지연할 프레임</param>
/// <param name="action">수행 할 작업</param>
/// <returns></returns>
private IEnumerator DelayMethod(int delayFrameCount, Action action)
{
for (int i = 0; i < delayFrameCount; i++)
{
yield return null;
}

action();
}

코루틴에서 n초 기다리는 것과 크게 다르지 않습니다.

이것도 확장 메서드로 추가하면 유용 합니다.

UniRx를 사용하는 경우

n 초 후에 실행하고 싶다

Observable.Timer을 사용

// 단지 호출만 하는 경우
// 100 밀리 초 후에 Log를 출력한다.
Observable.Timer(TimeSpan.FromMilliseconds(100))
.Subscribe(_ => Debug.Log("Delay call"));

// 매개 변수를 전달하는 경우
// 현재 플레이어의 좌표를 500 밀리 초 후에 표시
var playerPosition = transform.position;
Observable.Timer(TimeSpan.FromMilliseconds(500))
.Subscribe(_ => Debug.Log("Player Position : " + playerPosition));

Timer을 사용하는 경우.

매개 변수를 전달하려고 하는 경우에는 스트림 밖에서 값의 유지가 필요하므로 동시에 여러 타이머에 등록하면 문제가 생길 수 있습니다.

Delay를 사용

// 단지 호출하는 경우
// 100 밀리 초 후에 Log를 출력한다.
Observable.Return(Unit.Default)
.Delay(TimeSpan.FromMilliseconds(100))
.Subscribe(_ => Debug.Log("Delay call"));

// 매개 변수를 전달하는 경우
// 현재 플레이어의 좌표를 500 밀리 초 후에 표시
Observable.Return(transform.position)
.Delay(TimeSpan.FromMilliseconds(500))
.Subscribe(p => Debug.Log("Player Position : " + p));

n 프레임 후에 실행하고 싶다

Observable.TimerFrame를 사용

// 다음 프레임에서 실행
Observable.TimerFrame(1)
.Subscribe(_ => Debug.Log("Next Update"));

// 다음 FixedUpdate에서 실행
Observable.TimerFrame(1, FrameCountType.FixedUpdate)
.Subscribe(_ => Debug.Log("Next FixedUpdate"));

Observable.Timer와 크게 다르지 않다.

DelayFrame를 사용

// 다음 프레임에서 실행
Observable.Return(Unit.Default)
.DelayFrame(1)
.Subscribe(_ => Debug.Log("Next Frame"));

// 다음 FixedUpdate에서 실행
Observable.Return(Unit.Default)
.DelayFrame(1, FrameCountType.FixedUpdate)
.Subscribe(_ => Debug.Log("Next FixedUpdate"));

Delay와 크게 다르지 않다.

다음 프레임에서 실행

다음 프레임에서 실행하고 싶다면 NextFrame를 사용하는 것이 더 스마트한 방법이다.

// 다음 프레임에서 실행
Observable.NextFrame()
.Subscribe(_ => Debug.Log("Next Frame"));

정리

단지 처리를 지연 시키고 싶다면 코루틴을 쓰는 것이 편합니다.

하지만 처리를 지연시킨 후, 아직 뭔가 처리를 계속 해야 되는 경우라면 UniRx를 사용하여 스트림화 하는 것이 여러모로 쉽다고 생각합니다.