
머리말
_
마우스의 더블 클릭 판정
구현 하실 수 있나요?
변수 2개, Update
귀찮다.
UniEx를 쓰면 몇 줄로 끝낼 수 있다.
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를 사용하면 상당히 간결하게 작성 가능하다.
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
button
.OnClickAsObservable()
.SubscribeToText(text, _ => "clicked");
UGUI용 Observable과 Subscribe가 준비되어 있다.
왜 Stream이라는 개념을 만들었나?
이벤트를 변경할 수 있다
이벤트의 투명, 필터링, 합성 등이 가능하다
Buffer (Operator)
- 메시지를 모아서 특정 타이밍에 보낸다
- 방출 조건은 여러가지 지정이 가능하다
- n개 모아서 보내기
- 다른 스트림에 메시지가 흐르면, 보내기
- 방출 조건은 여러가지 지정이 가능하다
예제1. 버튼 3번
button
.OnClickAsObservable()
.Buffer(3)
.SubscribeToText(text, _ => "clicked");
- 굳이 필드 변수 추가 필요 없음
- 혹은 Skip(2)로도 똑같은 동작을 한다
- 여기에서는 이해들 돕기위해 Buffer를 사용했지만, n회 후에 동작하는 경우에는 Skip 쪽이 적절하다
Zip (Operator)
여러 개의 스트림의 메시지가 완전히 모일때까지 기다림
예제2. 버튼 2개 1번씩
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
더블 클릭 판정
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로 변화할 때에 이펙트를 재생한다
UpdateAsObservable()
.Select(_ => _characterController.isGrounded) // 이벤트 값을 isGrounded로 변경
.DustinctUntilChanged() // 값이 변화한 순간에만 통지
.Where(x => x) // True일 때만 통지
.Subscribe(_ => _effect.Play());
characterController
.ObserveEveryValueChanged(x => x.isGrounded)
.Where(x => x)
.Subscribe(_ => _effect.Play());
값의 변화를 다듬기
isGrounded의 변화를 다듬기
isGrounded의 정밀도의 개선
- 곡면을 이동하게 되면 True/False가 격렬하게 변환하다
- 이 값의 변화를 UniRx로 정제해 보자
isGrounded의 변화를 Throttle로 정제
- DistinctUntilChanged와 같이 쓰면 OK
UpdateAsObservable()
.Select(_ => _characterController.isGrounded)
.DistinctUntilChanged() // 값에 변화가 있는 경우에만 통지
.ThrottleFrame(5) // 5프레임 동안 변화가 없을 때에만 통지
.Subscribe(x => throttledIsGounded = x); // isGrounded가 6프레임 이상 안정된 때에 통지
WWW를 사용하기 쉽게
Unity의 WWW
Unity가 준비한 HTTP 통신용 모듈
- 코틴으로 사용할 필요가 있다
- 그래서 사용 편의성이 좋지는 않다
ObservableWWW
WWW를 Observable로써 취급할 수 있게 한것
- Subscribe된 순간에 통신을 실행한다
- 나중에 알아서 뒤에서 통신한 결과를 스트림에 보내 준다
- 코루틴을 사용하지 않아도 된다
ObservableWWW.Get("http://unity3d.com")
.Subscribe(x => Debug.Log(x));
_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로 표시한다
_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);
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));
});
동시에 통신해서 모든 데이터가 모이면 처리를 진행한다.
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 메시지가 날라간다
// 초기화
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가 발행되는 것은 아니다
Observable.EveryUpdate() .Subscribe(_ => Debug.Log("Update")) .AddTo(gameObject);
- AddTo
컴포넌트를 스트림으로 연결하기
스트림으로 연결
- 컴포넌트를 스트림으로 연결하는 것으로 Observer 퍁턴한 설계로 만들 수 있다
- 전체가 이벤트 기반이 되게 할 수 있따
- 더불어 Rx는 Observer패턴 그 자체
타이머 예제
- 타이머의 카운트를 화면에 표시 한다
- UniRx를 사용하지 않고 구현
- 매 프레임, 값이 변경되었는지를 확인 한다 (매 프레임 값을 체크해야만 한다)
- UniRx의 스트림으로 구현
- UniRx를 사용하지 않고 구현
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);
}
_timerComponent.Timer.SubscribeToText(_text);
스트림으로 연결하는 메리트
Observer 패턴이 간단히 구현 가능하다
- 변화를 폴링 Polling하는 구현이 사라진다
- 필요한 타이밍에 필요한 처리를 하는 방식으로 작성하는 것이 좋다
기존의 이벤트 통지구조보다 간단
- C#의 Event는 준비단계가 귀찮아서 쓰고 싶지 않다
- Unity의 SendMessage는 쓰고 싶지 않다
- Rx라면 Observable를 준비하면 OK! 간단 !
UGUI와 조합해서 사용하기
코루틴과 조합하기
UniRx는 쓸 때의 마음가짐
- [모든것은 스트림] 이라는 세계관을 가지자
- 스트림화 가능한 곳을 찾아서 스트림으로 바꾸면 편리해진다
- 다만 과용하는 것은 금물
- Rx는 만능이 아니라는 것을 알자
- 하고 싶은 것이 선언적으로 잘되지 않는 부분도 당연히 있다
- 그런 경우에는 코투린과 조햅해서 사용하면 유용하다
- 설계를 의식하자
- 무조건 쓰고보자라는 설계로 UniRx를 사용하는것은 상당히 위험
- Observer패턴의 장단점을 활용하자
이후 내용 생략.
메모
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 가 비교되는지