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>
에 해당
- LINQ에서
- (이벤트의 흐름 그 자체가 <스트림>)
- (중간 중간 이벤트 메시지)
🫧 Message
스트림에 흐르는 이벤트
- 세 종류
- OnNext
- 일반적으로 사용되는 메시지
- 보통은 이것을 사용한다
- OnError
- 에러 발생시에 예외를 통지하는 메시지
- OnCompleted
- 스트림이 완료되었음을 통지하는 메시지
- OnNext
- 버튼은 [클릭 된 타이밍에 이벤트를 스트림에 보낸다] 라고 생각하는 것이 가능하다
🫧 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));
🫧 값의 변화를 감지하기
플레이어가 지면에 떨어지는 순간에 이펙트를 발생
지면에 떨어진 순간의 감지 방법
- CharacterController.isGrounded를 매프레임 체크
- 현재 프레임의 값을 필드 변수에 저장
- 매프레임에 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);
- 버튼이 클릭되면
- HTTP로 텍스쳐를 다운로드 해서
- 그 결과를 Sprite로 변화해서
- 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초 후에 다시 리트라이 시도한다던가
- 유저들로부터 요청이 있을때 처리를 한다던가
- 방정보 리스트가 갱신될 때 통지 한다던지
- 코드가 명시적이 된다
- 스트림의 소스를 만드는법
- Observable의 팩토리 메소드 사용
- 기존의 이벤트 등으로부터 변환
Subject<T>
계를 사용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이 생성된다
- UpdateAsObservable
- Observable.EveryUpdate의 주의점
- Destroy때에 OnCompleted가 발생되지 않는다
- UpdateAsObservable 같은 느낌으로 사용하면 함정에 빠진다
.Select(_ => transform.position)
같은 걸 쓰면, gameObject가 파괴되면 Null이 되어 예외가 발생
- Destroy때에 OnCompleted가 발생되지 않는다
- 수명 관리의 간단한 방법
- AddTo
- 특정 gameObject가 파괴되면 자동 Dispost되게 해준다
- OnCompleted가 발행되는 것은 아니다
1 2 3
Observable.EveryUpdate() .Subscribe(_ => Debug.Log("Update")) .AddTo(gameObject);
- AddTo
🫧 컴포넌트를 스트림으로 연결하기
스트림으로 연결
- 컴포넌트를 스트림으로 연결하는 것으로 Observer 퍁턴한 설계로 만들 수 있다
- 전체가 이벤트 기반이 되게 할 수 있따
- 더불어 Rx는 Observer패턴 그 자체
타이머 예제
- 타이머의 카운트를 화면에 표시 한다
- UniRx를 사용하지 않고 구현
- 매 프레임, 값이 변경되었는지를 확인 한다 (매 프레임 값을 체크해야만 한다)
- 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: