포스트

Unity | UniRx

Unity | UniRx

https://www.slideshare.net/agebreak/unite17-unirx
<Mhttp://ndcreplay.nexon.com/NDC2014/sessions/NDC2014_0049.html>

💫 머리말


💫 메모


  • List<>.ObserveCountChange
    • List의 Count가 변경될 때마다
  • ReactiveProperty -> 그냥 FieldChangeCallback 이랑 유사한 느낌?
  • 메서드 그룹에서 System.IObserver<?>로 반환할 수 없습니다. -> 그냥 using UniRx;
  • 여러번 쓸 때는 UniRx 한 번만 쓸 때는 UniTask
  • ReactiveProperty.Pairwise
    • 지난 메시지와 현재 메시지를 Pair<T>구조로 합성
    • Previous, Current
  • ~.Forget
    • 비동기 작업 실행하지만 결과를 기다리지는 않도록
  • ReactiveProperty.Value
    • 기존 변수를 reactiveProperty로 변경할때, 기존 Null 비교 체크하는 부분 확인 할 것
    • 실제 Value (ReactiveProperty.Value)가 비교되는지, ReactiveProperty 가 비교되는지

🫧 참고

  • 이재민 UniRx, UniTask 제네릭 메타데이터 ~
  • yoonhada.com/?p=1386

💫 _

마우스의 더블 클릭 판정
구현 하실 수 있나요?

변수 2개, Update

귀찮다.

UniEx를 쓰면 몇 줄로 끝낼 수 있다.

1
2
3
4
5
6
var clickStream = UpdateAsObservable()
	.Where(_ => Input.GetMouseButtonDown(0));

clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(200)))
	.Where(x => x.Count >= 2)
	.SubsribeToText(_text, x => string.Format("DoubleClick detected!\n Count:{0}", x.Count));

💫 UniRx


UniRx 유나이 렉스? 유니 알 엑스?

.NET 버전 문제, .NET 무겁다

Unity C# 에서 사용할 수 있게 만든 가볍고 빠른 Unity 전용 Rx
Unity 전용 Rx 스트림 제공
일본에서 개발

💫 UniRx 뭐가 좋냐


시간의 취급이 굉장히 간단해진다.

시간이 결정하는 처리의 예

  • 이벤트의 기다림
    • 마우스 클릭이나 버튼의 입력 타이밍에 무언가를 처리 한다
  • 비동기 처리
    • 다른 스레드에서 통신을 하거나, 데이터를 로드할 때
  • 시간 측정이 판정에 필요한 처리
    • 홀드, 더블클릭의 판정
  • 시간 변화하는 값의 감시
    • False -> True 가 되는 순간에 1회만 처리하고 싶을 때

Rx를 사용하면 상당히 간결하게 작성 가능하다.

1
2
3
4
5
6
button.onClick
	.AsObservable() // 이벤트를 스트림으로 변경
	.Subscribe(_ => // 스트림의 구독 (최종적으로 무엇을 할것인가를 작성)
	{
		text.text = "Clicked";
	});

💫 Stream


  • [이벤트가 흐르는 파이프] 같은 이미지
    • 어렵게 말하자면, [타임라인에 배열되어 있는 이벤트의 시퀸스]
    • 분기 되거나 합쳐지는게 가능하다
  • 코드 안에서는 IObservable<T>로 취급된다
    • LINQ에서 IEnumerable<T>에 해당
  • (이벤트의 흐름 그 자체가 <스트림>)
    • (중간 중간 이벤트 메시지)

🫧 Message

스트림에 흐르는 이벤트

  • 세 종류
    • OnNext
      • 일반적으로 사용되는 메시지
      • 보통은 이것을 사용한다
    • OnError
      • 에러 발생시에 예외를 통지하는 메시지
    • OnCompleted
      • 스트림이 완료되었음을 통지하는 메시지
  • 버튼은 [클릭 된 타이밍에 이벤트를 스트림에 보낸다] 라고 생각하는 것이 가능하다

🫧 Subscribe (스트림의 구독)

  • 스트림의 말단에서 메시지가 올때 무엇을 할 것인지를 정의 한 다
  • 스트림은 Subscribe 된 순간에 생성 된다
    • 기본적으로 Subscribe하지 않는 한 스트림은 동작하지 않는다
    • Subscribe 타이밍에 의해서 결과가 바뀔 가능성이 있다
  • OnError, OnComplete가 오면 Subscribe는 종료 된다

  • 오버로드 여러 개 (용도에 따라)
    • OnNext 만
    • OnNext & OnCompleted
    • OnNext & OnError & OnCompleted

옵저버 패턴에서 AddListenter 하는 거랑 같다.

🫧 UniRx

1
2
3
button
	.OnClickAsObservable()
	.SubscribeToText(text, _ => "clicked");

UGUI용 Observable과 Subscribe가 준비되어 있다.

🫧 왜 Stream이라는 개념을 만들었나?

이벤트를 변경할 수 있다
이벤트의 투명, 필터링, 합성 등이 가능하다

🫧 Buffer (Operator)

  • 메시지를 모아서 특정 타이밍에 보낸다
    • 방출 조건은 여러가지 지정이 가능하다
      • n개 모아서 보내기
      • 다른 스트림에 메시지가 흐르면, 보내기

예제1. 버튼 3번

1
2
3
4
button
	.OnClickAsObservable()
	.Buffer(3)
	.SubscribeToText(text, _ => "clicked");
  • 굳이 필드 변수 추가 필요 없음
  • 혹은 Skip(2)로도 똑같은 동작을 한다
    • 여기에서는 이해들 돕기위해 Buffer를 사용했지만, n회 후에 동작하는 경우에는 Skip 쪽이 적절하다

🫧 Zip (Operator)

여러 개의 스트림의 메시지가 완전히 모일때까지 기다림

예제2. 버튼 2개 1번씩

1
2
3
4
5
6
button1
	.OnClickAsObservable()
	.Zip(button2.OnClickAsObservable(), (b1, b2) => "Clicked !")
	.Firset() // 1번 동작한 후에 Zip 내의 버퍼를 클리어 한다
	.Repeat()
	.SubscribeToText(text, x _ => text.text + x + "\n");

💫 차이


Rx를 사용하지 않는 종래의 방법에서는…
이벤트를 받은 후에 어떻게 할것인가를 작성 하였다

Rx에서는
이벤트를 받기 전에
무엇을 하고 싶다를 작성한다

[스트림을 가공해서 자신이 받고 싶은 이벤트만 통지 받으면 좋잖아!]

💫 정리


Rx는

  • 스트림을 준비해서
  • 스트림을 오퍼레이터로 가공 해서
  • 최후에 Subscribe 한다

라는 개념으로 사용 된다

💫 Operator


스트림을 조작하는 메소드

무진장 많음

자주 사용하는 오퍼레이터

🫧 Where

조건을 만족하는 메시지만 통과시키는 오퍼레이터
다른 언어에서는 [Filter]라고도 한다

Where(x => x > 10)

🫧 Select

요소의 값을 변경한다
다른 언어에서는 [Map]이라고 한다

Select(x => 10 * x)

🫧 SelectMany

새로운 스트림을 생성하고, 그 스트림이 흐르는 메시지를 본래의 스트림의 메시지로 취급

  • 스트림을 다른 스트림으로 교체하는 이미지 (정밀히 말하면 다름)
  • 다른 언어에서는 [FlatMap]이라고도 한다

🫧 Throttle/ThrottleFrame

도착한 때에 최후의 메시지를 보낸다

  • 메시지가 집중해서 들어 올때에 마지막 이외를 무시한다
  • 다른 언어에서는 [Debounce] 라고도 한다
  • 자주 사용됨

디도스, 유저 입력, 더블 클릭

🫧 ThrottleFirst/ThrottleFirstFrame

최초에 메시지가 올때부터 일정 시간 무시한다

  • 하나의 메시지가 온때부터 잠시 메시지를 무시 한다
  • 대용량으로 들어오는 데이터의 첫번째만 사용하고 싶을 때 유효

🫧 Delay/DelayFrame

메시지의 전달을 연기 한다

🫧 DistinctUntilChanged

메시지가 변화한 순간에만 통지한다

  • 같은 값이 연속되는 경우에은 무시한다

🫧 SkipUntil

지정한 스트림에 메시지가 올때까지 메시지를 Skip한다

  • 같은 값이 연속되는 경우에는 무시한다

🫧 TakeUntil

지정한 스트림에 메시지가 오면, 자신의 스트림에 OnCompleted를 보내서 종료한다

🫧 Repeat

스트림이 OnCompleted로 종료될 때에 다시 한번 Subscribe를 한다

🫧 SkipUntil + TakeUntil + Repeat

자주 사용되는 조합

  • 이벤트 A가 올때부터 이벤트 B가 올떄까지 처리를 하고 싶을때 사용

예제. 드래그로 오브젝트를 회전시키기
MouseDown이 올 때부터 Mouse Up이 올떄까지 처리할 때

🫧 First

스트림에 최초로 받은 메시지만 보낸다

  • OnNext 직후에 OnCompleted도 보낸다

💫 Example


🫧 더블 클릭 판정

1
2
3
4
5
6
var clickStream = UpdateAsObservable() // Update에서
	.Where(_ => Input.GetMouseButtonDown(0)); // Where 클릭이 된 프레임만 통과

clickStream.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(200)))
	.Where(x => x.Count >= 2)
	.SubsribeToText(_text, x => string.Format("DoubleClick detected!\n Count:{0}", x.Count));

🫧 값의 변화를 감지하기

플레이어가 지면에 떨어지는 순간에 이펙트를 발생
지면에 떨어진 순간의 감지 방법

  1. CharacterController.isGrounded를 매프레임 체크
  2. 현재 프레임의 값을 필드 변수에 저장
  3. 매프레임에 False->True로 변화할 때에 이펙트를 재생한다
1
2
3
4
5
UpdateAsObservable()
	.Select(_ => _characterController.isGrounded) // 이벤트 값을 isGrounded로 변경
	.DustinctUntilChanged() // 값이 변화한 순간에만 통지
	.Where(x => x) // True일 때만 통지
	.Subscribe(_ => _effect.Play());
1
2
3
4
characterController
	.ObserveEveryValueChanged(x => x.isGrounded)
	.Where(x => x)
	.Subscribe(_ => _effect.Play());

🫧 값의 변화를 다듬기

isGrounded의 변화를 다듬기
isGrounded의 정밀도의 개선

  • 곡면을 이동하게 되면 True/False가 격렬하게 변환하다
  • 이 값의 변화를 UniRx로 정제해 보자

isGrounded의 변화를 Throttle로 정제

  • DistinctUntilChanged와 같이 쓰면 OK
1
2
3
4
5
UpdateAsObservable()
	.Select(_ => _characterController.isGrounded)
	.DistinctUntilChanged() // 값에 변화가 있는 경우에만 통지
	.ThrottleFrame(5) // 5프레임 동안 변화가 없을 때에만 통지
	.Subscribe(x => throttledIsGounded = x); // isGrounded가 6프레임 이상 안정된 때에 통지

🫧 WWW를 사용하기 쉽게

Unity의 WWW
Unity가 준비한 HTTP 통신용 모듈

  • 코틴으로 사용할 필요가 있다
  • 그래서 사용 편의성이 좋지는 않다

ObservableWWW
WWW를 Observable로써 취급할 수 있게 한것

  • Subscribe된 순간에 통신을 실행한다
  • 나중에 알아서 뒤에서 통신한 결과를 스트림에 보내 준다
  • 코루틴을 사용하지 않아도 된다
1
2
ObservableWWW.Get("http://unity3d.com")
	.Subscribe(x => Debug.Log(x));
1
2
3
4
5
_button.OnClickAsObservable()
	.First() // 버튼을 연타해도 통신은 1회만 하도록 First를 사용
	.SelectMany(_ => ObservableWWW.GetWWW(resourceURL)) // 클릭 스트림을 ObservableWWW로 덮어쓴다
	.Select(www => Sprite.Create(www.texture, new Rect(0, 0, www.texture.width, www.texture.height), Vector2.zero))
	.Subscribe(sprite => _image.sprite = sprite, Debug.LogError);
  1. 버튼이 클릭되면
  2. HTTP로 텍스쳐를 다운로드 해서
  3. 그 결과를 Sprite로 변화해서
  4. Image로 표시한다
1
2
3
4
5
_button.OnClickAsObservable()
	.First()
	.SelectMany(_ => ObservableWWW.GetWWW(resourceURL).Timeout(TimeSpan.FromSeconds(3))) // 타임아웃이 필요하다면 여기에 오퍼레이터를 추가한다
	.Select(www => Sprite.Create(www.texture, new Rect(0, 0, www.texture.width, www.texture.height), Vector2.zero))
	.Subscribe(sprite => _image.sprite = sprite, Debug.LogError);
1
2
3
4
5
6
7
8
9
10
var parallel = Observable.WhenAll(
	ObservableWWW.Get("http://unity3d.com"),
	ObservableWWW.Get("http://unity3d.com/jp")
);

parallel.Subscribe(xs =>
{
	Debug.Log(xs[0].Substring(0, 100));
	Debug.Log(xs[1].Substring(0, 100));
});

동시에 통신해서 모든 데이터가 모이면 처리를 진행한다.

1
2
3
4
5
var resourcePathURL = "http://unity3d.com";

ObservableWWW.Get(resourcePathURL)
	.SelectMany(resourceURL => ObservableWWW.Get(resourceURL))
	.Subscribe(Debug.Log);

앞의 통신 결과를 사용해서 다음 통신을 실행한다

  • 서버에 [리소스의 URL]을 물어보고, 서버에서 가르쳐준 URL로부터 데이터를 다운로드 한다.

WWW -> 비동기
근데 우리는 WWW을 쓰기 위해 비동기를 또 다시 동기로 만들어줄 필요가 있다
앞에 결과를 받아와야지만 다음을 실행할 수 있다
Coroutine을 사용해서
그래서 UniRx에서는 이런 것을 쉽게 해주는 것이다

🫧 기존 라이브러리를 스트림으로 변환

PhotonCloud
통지가 전부 콜백이어서 미묘하게 사용하기에 나쁘다
UniRx를 통해 콜백이 스트림으로 변환된다 (복잡한 콜백은 숨기자)

  • 콜백으로부터 스트림을 변경하는 메리트
    • 코드가 명시적이 된다
      • Photon의 콜백은 SendMessage로 호출된다
      • Observable의 제대로 정의해서 사용하면 명시적이 된다
    • 다양한 오퍼레이터에 의한 유연한 제어가 가능해지게 된다
      • 로그인에 실패하면 3초 후에 다시 리트라이 시도한다던가
      • 유저들로부터 요청이 있을때 처리를 한다던가
      • 방정보 리스트가 갱신될 때 통지 한다던지
  • 스트림의 소스를 만드는법
    1. Observable의 팩토리 메소드 사용
    2. 기존의 이벤트 등으로부터 변환
    3. Subject<T> 계를 사용
    4. ReactiveProperty<T> 계를 사용

ReactiveProperty

  • Subscribe가 가능한 변수
  • Observable로서 Subscribe가 가능하다
  • 값이 쓸때에 OnNext 메시지가 날라간다
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 초기화
reactiveProperty<int> reativeInt = new ReactiveProperty<int>(0);

// Observable로서 Subscribe 가능
reativeInt
	.AsObservable()
	.Subscribe(x => Debug.Log(x));

// Get으로 현재 값을 읽어오기 가능
var currentValue = reativeInt.Value;

// Set으로 값 변경 가능
// 동시에 그때의 값이 OnNext에 본내다
reativeInt.Value = 10;

모든 구조를 이벤트 구조로 만든다
나는 구현부를 만드는 사람, 이벤트 발생 부분을 작업하는 사람을 완벽하게 분리할 수 있다
옵저버 패턴와 이벤트 Driven이 모든 게임 개발에 프레임워크화 돼요
그렇게 되면 객체들간의 의존성이 떨어진다 (Decoupling)

객체들간의 의존성을 없애기위한
이벤트 Driven으로 바꾸고
이벤트를 오퍼레이터를 이용해서 가공까지 할 수 있다

💫 마무리

🫧 _

  • Update()를 없애기
    • Update()를 Observable로 변환해서 Awake/Start()에서 모아서 작성하기
    • 기존: 하고 싶은 것을 순차적으로 작성해야 해서, 흐름이 따라가기 힘들다
    • New: 작접을 병렬로 작성할 수 있어서 읽기가 쉽다
    • 메리트
      • 작업별로 병렬로 늘어서 작성하는 것이 가능
        • 작업별로 스코프가 명시적이게 된다
        • 기능추가, 제거, 변경이 용이해지게 된다
        • 코드가 선언적이 되어서, 처리의 의도가 알기 쉽게 된다
      • Rx의 오퍼레이터가 로직 구현에 사용 가능
        • 복잡한 로직을 오퍼레이터의 조합으로 구현 가능하다
    • 변경하는 3가지 방법
      • UpdateAsObservable
        • 지정한 gameObject에 연동된 Observable가 만들어진다
        • gameObject의 Destroy때에 OnCompleted가 발행된다
      • Observable.EveryUpdate
        • gameObject로부터 독립된 Observable가 만들어진다
        • MonoBehaviour에 관계 없는 곳에서도 사용 가능
      • ObserveEveryValueChanged
        • Observable.EveryUpdate의 파생 버전
        • 값의 변화를 매프레임 감시하는 Observable이 생성된다
    • Observable.EveryUpdate의 주의점
      • Destroy때에 OnCompleted가 발생되지 않는다
        • UpdateAsObservable 같은 느낌으로 사용하면 함정에 빠진다
        • .Select(_ => transform.position) 같은 걸 쓰면, gameObject가 파괴되면 Null이 되어 예외가 발생
    • 수명 관리의 간단한 방법
      • AddTo
        • 특정 gameObject가 파괴되면 자동 Dispost되게 해준다
        • OnCompleted가 발행되는 것은 아니다
          • 1
            2
            3
            
            Observable.EveryUpdate()
            	.Subscribe(_ => Debug.Log("Update"))
            	.AddTo(gameObject);
            

🫧 컴포넌트를 스트림으로 연결하기

스트림으로 연결

  • 컴포넌트를 스트림으로 연결하는 것으로 Observer 퍁턴한 설계로 만들 수 있다
    • 전체가 이벤트 기반이 되게 할 수 있따
    • 더불어 Rx는 Observer패턴 그 자체

타이머 예제

  • 타이머의 카운트를 화면에 표시 한다
    • UniRx를 사용하지 않고 구현
      • 매 프레임, 값이 변경되었는지를 확인 한다 (매 프레임 값을 체크해야만 한다)
    • UniRx의 스트림으로 구현
1
2
3
4
5
6
7
8
9
10
11
private readonly ReactiveProperty<int> _timer = new ReactiveProperty<int>(0);

public ReadOnlyReactiveProperty<int> Timer => _timer.ToReadOnlyReactiveProperty();

private void Start()
{
	// Observable.Interval(TimeSpan.FromSeconds(1))
	Observable.Timer(TimeSpan.FromSeconds(1))
		.Subscribe(_ => _timer.Value++)
		.AddTo(gameObject);
}
1
_timerComponent.Timer.SubscribeToText(_text);

스트림으로 연결하는 메리트

  • Observer 패턴이 간단히 구현 가능하다
    • 변화를 폴링 Polling하는 구현이 사라진다
    • 필요한 타이밍에 필요한 처리를 하는 방식으로 작성하는 것이 좋다
  • 기존의 이벤트 통지구조보다 간단
    • C#의 Event는 준비단계가 귀찮아서 쓰고 싶지 않다
    • Unity의 SendMessage는 쓰고 싶지 않다
    • Rx라면 Observable를 준비하면 OK! 간단 !

🫧 UGUI와 조합해서 사용하기

TODO:

🫧 코루틴과 조합하기

TODO:

💫 UniRx는 쓸떄의 마음가짐

  • [모든것은 스트림] 이라는 세계관을 가지자
    • 스트림화 가능한 곳을 찾아서 스트림으로 바꾸면 편리해진다
    • 다만 과용하는 것은 금물
  • Rx는 만능이 아니라는 것을 알자
    • 하고 싶은 것이 선언적으로 잘되지 않는 부분도 당연히 있다
    • 그런 경우에는 코투린과 조햅해서 사용하면 유용하다
  • 설계를 의식하자
    • 무조건 쓰고보자라는 설계로 UniRx를 사용하는것은 상당히 위험
    • Observer패턴의 장단점을 활용하자

💫 __

TODO:

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.