UniRx 란
한글UniRx해설
https://moongtaeng.net/moniwiki/UniRx
UniRx는 기존의 Rx라이브러리를 neuecc 가 유니티용으로 만든 Reactive Extensions for Unity 라이브러리이다.
Reactive Extensions for Unity, 프로그램은 리액티브(?) 하게 짤수 있도록 도와주는 라이브러리이다.
기존의 방식은 클릭이벤트, 다운로드 이벤트등을 필요한 부분에서 호출하여 처리하여 다시 그 값을 콜백등에 반영하도록 하여 화면의 갱신등을 하는데 반해 리액티브하게 짠다는 의미는 데이터를 중심으로 어떤 변화가 있을때 관련 프로세서가 자동으로 이루어 지도록 개발하는방식이다.
Rx 라이브러리의 요점은 다음과 같다.
・MicrosoftResearch가 개발하고 있던 C#용 비동기 처리용 라이브러리이다.
・Observer 패턴을 베이스로 설계되어 있다
・이벤트, 시간에 관련된 처리, 실행 타이밍이 중요한 처리를 간단하게 기술할 수 있고 완성도가 높아서 Java, JavaScript Swift 등 다양한 언어로 포팅 되었다.
UniRx는 이 Rx를 베이스로 Unity에 이식된 라이브러리이며, 본가.NET Rx와 비교해 이하와 같은 차이가 있습니다.
・Unity의 C #에 최적화되어 작성되었습니다.
・Unity 의 사이틀에 맞게 개발에 편리한 기능과 오퍼레이터가 추가 구현되어 있다.
・ReactiveProperty 등이 추가되었습니다.
・성능 튜닝이 잘되었있고 원래의 .NET Rx보다 메모리 성능이 우수하다.
기본적으로 C#에서 구현된 event 와 호환되는 기술로서, UniRx는 event를 좀더 사용하기 편하게 개선된라이브러리라고 생각해도된다.
C# event
클래스내에서 이벤트 정의 및 등록 -> 사용측 클래스에서 이벤트의 구체적인 활동을 구현 -> 클래스에서 이벤트 통지(발행) -> 사용측 클래스에서 구현된 기능수행
UniRx
클래스내에서 주제를 등록 -> 사용측 클래스에서 구둑을 등록 -> 클래스에서 주제에 대한 내용을 발행 -> 사용측 클래스에서 발행내용을 수집하여 활용
정의(Subject) 와 구독(Subscribe)
Subject는 이벤트를 발행하는 주체로서, 쉽게 생각하면 뉴스발행처이다.
Subscribe는 이벤트 구족자로서 발행받은 내용을 알아서 사용하고 처리하는 역활이다.
Subject는 IObserver 와 IObservable로 구현(또는 구성) 된다.
- IObserver 는 -er 이 붙은것처럼, 발행자의 행동을 정의한다. 3가지 행동이 있다.
- OnCompleted : 발행이 끝날때 발행자가 하는행동
- OnError : 발행시 에러가 발생했을때
- OnNext : 발행이 되어서 구독자에게 내용을 발행하는 행동
- IObservable 은 -able에서 알수있듯이, Subject를 발행이 가능한 상태를 따로 정의한다. Subject가 여러가지 형식으로 발행을 할때, 발행내용(형식)에 따라서 각기 다른 구독자에게 내용을 전달하는 등의 구체적인 작업이 가능하다.
간단한 발행-구독 관계의 예이다.
var sub = new Subject<string>();
sub.Subscribe(receivvedText => Debug.Log(receivvedText));
sub.OnNext("efg");
Operator
Rx 에서 발행메시지를 효율적으로 제어하는 여러가지 기능들이 있는데 이를 오퍼레이터라고 한다.
오퍼레이터를 이용하여 구독자는 발행메시지를 필터링 하거나 발행메시지를 추가로 가공하여 구독메시지의 형태를 따로 만들수도 있다.
오퍼레이터수가 하도 많아서 다 기재하기는 힘드니까 몇몇 대표적인 것들만 예를 들겠다.
필터계열
-
- Where (x => x == "Player)
- (발행받은 메시지 => {true, false 로 해당메시기가 원하는 메시지 인지 필터링}).
-
- Select
- Javascript나 Kotlin 에서 map 으로 불리는 것과 동일한 기능을 한다. 각각의 객체를 반복하며 필요한 처리를 한 결과객체를 돌려준다.
-
- Distinct
- 같은 메시지가 반복될때, 한개만 발행하도록 한다.
-
- Buffer
- 버퍼를 지정하고 버퍼에 메시지가 찰때까지 발행하지 않는다. 메시지 개수가 다 차면 발행한다…
sub.Where(count => count > 3)
.Select(count => count * 10)
.Subscribe(count => Debug.Log(count));
sub.OnNext(1)
sub.OnNext(3)
sub.OnNext(2)
sub.OnNext(4)
30
40
그래서, Unity 에는 어떻게 활용하는데?
UniRx를 사용하면 Update 에서 조건문으로 이런 저런 처리를 하던거를 좀더 간단하게 할수 있다. 또한 버튼의 까다로운 이벤트처리도 간단하게 할수 있다.
충돌제어
UniRx로 GameObject에 onCollisionEnter를 코드로추가
using UniRx.Triggers;
ball.gameObject.OnCollisionEnterAsObservable().Subscribe(collision => { 충돌처리 });
충돌종류
→ 入ったとき(OnTriggerEnterAsObservable)
→ 入ってるとき(OnTriggerStayAsObservable)
→ 出たとき(OnTriggerExitAsObservable)
입력이벤트 제어
예를 들어보자.
프리펩이 화면에 나온후 2초후 뭔가액션.일때.
private void Start(){
StartCoroutine(DelayMethod(2f, () =>{
Debug.Log("2초 되었다.");
}));
}
private IEnumerator DelayMethod(float waitTime, Action action){
yield return new WaitForSeconds(waitTime);
action();
}
UniRx의 마법을 보자
Observable.Timer(TimeSpan.FromSeconds(2)).Subscribe(_=>Debug.Log("2초 되었다."));
UniRx의 스트림 관련 기능이 제일 빛나는 예제는 역시 버튼의 중복 클릭, 동반클릭 처리이다.
UniRx에는 여러가지 오퍼레이터가 있는데 그중에 Buffer, Skip, ThrottleFirst 등 순서대로 발생하는 이벤트를 스트림에 흘려보내 그중에 조건에 맞는 상태만 간단하게 골라서 처리할수 있도록 해준다.
버튼을 두번 클릭해야 되는 처리를 한다고 생각하자. 일단 버튼클릭에 대한 count변수를 넣어서 2번이 되었는지 if문등으로 체크를 해야한다.
UniRx의 마법을 보자
GetComponent<Button>()
.OnClickAsObservable()
.Buffer(2)
.Subscribe(_=>Debug.Log("두번 눌렀네."));
두개의 버튼이 클릭이 되었는지 판단
button2.OnClickAsObservable()
.Zip(button.OnClickAsObservable(), (b1, b2) => "Two Clicked")
.First()
.Repeat()
.Subscribe(msg => { Debug.Log(msg); });
마우스로 객체 회전제어
void Start ()
{
this.OnMouseDownAsObservable()
.SelectMany(_ => this.UpdateAsObservable())
.TakeUntil(this.OnMouseUpAsObservable())
.Select(_ =>
new Vector2(Input.GetAxis("Mouse X"),Input.GetAxis("Mouse Y")))
.RepeatUntilDestroy(this)
.Subscribe(move =>
{
this.transform.rotation =
Quaternion.AngleAxis(move.y * rotationSpeed * Time.deltaTime, Vector3.right) *
Quaternion.AngleAxis(-move.x * rotationSpeed * Time.deltaTime, Vector3.up) *
transform.rotation;
;
});
변수 값이변경될때를 감지해보자
변수형식 Int, Float, String, Bool, 또는 사용자 클래스 의 속성을 바인드하여 값이 변경될때 적절한 처리를 한다든지 해보자.
예를 들어 공격을 당할때 HP가 줄어들어서 피를 토하게 한다든가, 잠시 유형모드를 킨다든가 할수 있다.
ReactiveProperty 로 Hp 값의 변화를 감지하자.
Class A {
public ReactiveProperty<int> myHp = new ReactiveProperty<int>(1);
public A() {
myHp.Subscribe( x =>
Debug.Log(x)
);
}
}
Class B {
private readonly A _a = new A();
void Start()
{
_a.myHp.Value++;
}
}
ReactiveProperty 로 Hp 값의 변화를 감지하여 10이상일떄는 true를 감지해보자
- MP의 숫자에 따라서 공격아이템을 다르게 해보자
- HP가 줄어들면(공격을당하면) 잠시 무적상태를 ON으로 하자
Select 오퍼레이터를 이용하면 원래의 값을 이용하여 다른 형식의 데이터로 변환된 Observable을 반환해줄수있다.
isHpFull은 hp가 10이상일때만 true이고 나머진 false이다.
ReadOnlyReactiveProperty<bool> isHpFull = myHp
.Select(hp => (hp>10))
.ToReadOnlyReactiveProperty();
isHpFull은 hp가 10이상일때만 true이고 나머진 false이다.
ReadOnlyReactiveProperty<String> isHpFull = myHp
.Select(hp => "myHp" + hp)
.ToReadOnlyReactiveProperty();
isHpFull.Subscribe(str => Debug.Log(str));
ReactiveProperty 로 두값의 변화중 최종합만을 감지해보자
- 두개의 아이템을 획득해야 특수공격이 가능하도록 해보자.
- 오른쪽, 왼쪽이던 공격하면 전체의 HP가 줄어들게 해보자.
두합이 100이 될때까지 아이템을 찾는 다는 combineLatest 오퍼레이터를 활용하자. 또는
var a = new ReactiveProperty<int>(1);
var b = new ReactiveProperty<int>(2);
var c = Observable.CombineLatest(a, b, (x, y) => x + y).ToReadOnlyReactiveProperty();
Debug.Log(c);
a.Value= 98;
Debug.Log(c);
b.Value = 0;
Debug.Log(c);
ReactiveProperty 로 캐릭터의 속성값들을 유지한채 캐릭터를 바꾸어보자
Select와 Switch 를 이용하여, Select로 현재선택된 캐릭터의 속성을 읽고, Switch 로 발행처를 바꾸어 캐릭터를 변경해보자.
ReactiveProperty<Charactor> currentCharacter = new ReactiveProperty<Charactor>();
var character1 = new Charactor();
currentCharacter.Value = character1;
ReadOnlyReactiveProperty<int> currentAttack =
currentCharacter .Select(c => (IObservable<int>)c.Attack)
.Switch()
.ToReadOnlyReactiveProperty();
character1.Attack.Value = 100;
Debug.Log(currentAttack.Value);
var character2 = new Charactor();
currentCharacter.Value = character2;
character2.Attack.Value = 200;
Debug.Log(currentAttack.Value);
character2.Attack.Value = 220;
Debug.Log(currentAttack.Value);
character1.Attack.Value = 50;
Debug.Log(currentAttack.Value);
currentCharacter.Value = character1;
Debug.Log(currentAttack.Value);
NPC 제어
WhenAll 오퍼레이터로 NPC들이 자기 할일을 다 끝내면, 완료 라는 메시지를 출력하는 비동기 처리를 해보자
각각의 NPC들을 Observable로 하여 , 각역활이 끝날때 onComplete를 호출하면 완료! 라는 메시지를 출력하자.
var stream2 = Observable.Start(() =>
{
Debug.Log("stream2 start");
});
var stream3 = button.onClick.AsObservable().Take(1);
stream3.Subscribe(x =>
{
Debug.Log("stream3 start");
});
var stream4Wrapper = Observable.Create<Unit>(observer =>
{
var stream4 = button2.onClick.AsObservable();
var subscription = stream4.Subscribe(x =>
{
Debug.Log("stream4 start");
observer.OnCompleted();
}); return subscription;
});
Observable.WhenAll(stream1, stream2, stream3, stream4Wrapper).Subscribe(x => Debug.Log("all done"));
/
카메라 제어
마우스드래그 해서 카메라 이동하기
마우스로 화면을 클릭하여 드래그 하면 이벤트를 감지하여 이전드래그위치와 4발행이후의 위치를 감지하여 카메라를 자연스럽게 이동시킨다.
먼저 카메라에 RidigeBody를 추가하고 Gravity 를 0으로 한다…
public class CameraRx : MonoBehaviour
{
public Camera MainCamera;
void Update()
{#if UNITY_EDITOR
if (Input.GetMouseButtonUp(0))
{ } else if (Input.GetMouseButton(0))
{ MapSwipe();
}#elif UNITY_IOS || UNITY_ANDROID
if (Input.touchCount == 0) {
} else if (Input.touchCount == 1) { MapSwipe(); }#endif
}
public void MapSwipe()
{#if UNITY_EDITOR
var drug = Observable.EveryUpdate().Select(pos => Input.mousePosition);
var stop = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonUp(0));
#elif UNITY_IOS || UNITY_ANDROID
var drug = Observable.EveryUpdate ().Select (pos => Input.GetTouch(0).position);
var stop = Observable.EveryUpdate ().Where(_ => Input.touchCount != 1);#endif
IDisposable onDrug = drug
.Zip(drug.Skip(4), (pos1, pos2) => new { x = pos2.x - pos1.x, z = pos2.y - pos1.y })
.TakeUntil(stop)
.Subscribe(deltaPosition =>
{
Debug.Log(deltaPosition.x + ":" + deltaPosition.z);
MainCamera.gameObject.GetComponent<Rigidbody>().velocity =
new Vector3(deltaPosition.x, 0, deltaPosition.z) * -5;
} );
IDisposable onDrug = drug.Buffer (3)
.TakeUntil(stop)
.Subscribe (colPos => {
float delPosx = colPos.Last().x - colPos.First().x;
float delPosz = colPos.Last().y - colPos.First().y;
float Speed = 0 -(5 * (MainCamera.fieldOfView / 60));
MainCamera.gameObject.GetComponent<Rigidbody>().velocity = new Vector3(delPosx, 0, delPosz) * Speed;
});
}
}
카메라 Pinch를 하기
void Update () {
#if UNITY_EDITOR
if (Input.GetMouseButtonUp(0)) {
}else if(Input.GetKey(KeyCode.Space) && Input.GetMouseButton(0)){
MapPinch();
}
#elif UNITY_IOS || UNITY_ANDROID
if (Input.touchCount == 0) {
} else if (Input.touchCount == 1) {
MapSwipe();
} else if (Input.touchCount >= 2) {
MapPinch();
}
#endif
}
public void MapPinch()
{
#if UNITY_EDITOR
var pinch = Observable.EveryUpdate ().Select (pos_dist => Input.mousePosition.x);
var stop = Observable.EveryUpdate ().Where(_ => Input.GetMouseButtonUp(0));
#elif UNITY_IOS || UNITY_ANDROID
var pinch = Observable.EveryUpdate ()
.Select (pos_dist => Vector2.Distance(Input.GetTouch (0).position, Input.GetTouch (1).position)); var stop = Observable.EveryUpdate ().Where (_ => Input.touchCount != 2);#endif
IDisposable onPinch = pinch.Zip(pinch.Skip(1), (dist1, dist2) => new { diff = dist2 - dist1})
.TakeUntil(stop)
.Subscribe(distanceParam => {
MainCamera.fieldOfView -= distanceParam.diff / 30;
if(MainCamera.fieldOfView < 4){
MainCamera.fieldOfView = 4;
} if(MainCamera.fieldOfView > 70){
MainCamera.fieldOfView = 70;
} }); }
코루틴
Observable을 잉요해서 코루틴에서 응답을 기다리게 해보자.
public class Example : MonoBehaviour { [SerializeField]
private Button _buttonA; [SerializeField]
private Button _buttonB;
public void Start() { StartCoroutine(ExampleCoroutine()); }
private IEnumerator ExampleCoroutine() { Debug.Log( "A가 눌러질 때까지 기다립니다." ); yield return _buttonA .OnClickAsObservable() .FirstOrDefault() .ToYieldInstruction();
Debug.Log( "B가 밀릴 때까지 기다립니다." ); yield return _buttonB .OnClickAsObservable() .FirstOrDefault() .ToYieldInstruction();
Debug.Log( "둘 다 눌렀습니다." ); } }
Observable.FromCoroutine
FromCoroutine을 이용하면 코루틴종료시 onNext와 onCompleted호출되는 옵서버블을 만들수 있다.
public class Example : MonoBehaviour {
public void Start() { Observable.FromCoroutine(ExampleCoroutine) .Subscribe(_ => Debug.Log( "OnNext" ), () => Debug.Log( "OnCompleted" )) .AddTo( this ); }
private IEnumerator ExampleCoroutine() {
yield return new WaitForSeconds( 1.0f ); }
}
public class Example : MonoBehaviour {
public void Start() { Observable.FromCoroutine < int > (ExampleCoroutine) .Subscribe(i => Debug.Log( "OnNext : " + i), () => Debug.Log( "OnCompleted" )) .AddTo( this ); }
private IEnumerator ExampleCoroutine(IObserver< int > observer) {
for ( int i = 0 ; i < 3 ; i++) { yield return new WaitForSeconds( 1.0f ); observer.OnNext(i); } observer.OnCompleted(); }
}
Observable.FromCoroutineValue
Observable.FromCoroutineValue를 사용하면 코루틴 내에서
yield return x로 반환된 값을 OnNext()로 발행할 수 있다.
코루틴의 끝에 도달하면 OnCompleted()도 발행된다.
public class Example : MonoBehaviour {
public void Start() { Observable.FromCoroutineValue< int >(ExampleCoroutine) .Subscribe(i => Debug.Log( "OnNext : " + i), () => Debug.Log( "OnCompleted" )) .AddTo( this ); }
private IEnumerator ExampleCoroutine() {
for ( int i = 0 ; i < 3 ; i++) { yield return i; } }
}
리소스로드
여러개의 내부 리소스를 전부 로드할때 까지 기다리자.
using System;
using UniRx;
using UnityEngine;
public class ResourceLoader : MonoBehaviour
{
private ResourceRequest _resourceRequestBox;
private ResourceRequest _resourceRequestCircle;
void Start()
{
var cubeLoad = Resources.LoadAsync<GameObject>("Prefabs/Cube")
.AsAsyncOperationObservable()
.Last();
var circleLoad = Resources.LoadAsync<GameObject>("Prefabs/Circle")
.AsAsyncOperationObservable().Delay(TimeSpan.FromSeconds(3))
.Last();
var cylinderLoad = Resources.LoadAsync<GameObject>("Prefabs/Cylinder")
.AsAsyncOperationObservable().Delay(TimeSpan.FromSeconds(1))
.Last();
Observable.WhenAll(cubeLoad, circleLoad, cylinderLoad)
.Subscribe(_ =>
{
if (_[0].asset != null)
{ GameObject Cube = Instantiate(_[0].asset) as GameObject;
}
if (_[1].asset != null)
{ GameObject Circle = Instantiate(_[1].asset) as GameObject;
Circle.transform.position += new Vector3(5, 0, 0);
} if (_[2].asset != null)
{ GameObject Cylinder = Instantiate(_[2].asset) as GameObject;
Cylinder.transform.position += new Vector3(10, 0, 0);
} } ); }}
관련 링크
LINQ 백과사전 고라니 유니티2D https://goraniunity2d.blogspot.com/2021/01/linq.html
외부강
공굴리기 유니티 기본 튜토리얼을 UniRx로 다시 쓰기
https://qiita.com/sensuikan1973/items/740ba95f17dc57f9655c
ReactiveCollection으로 아이템명 획득표시
UpdateAsObservable로 획득아이템 뷰에반영, 사용자키보드입력처리,적들이 쫓아오는 처리
ReactiveCollection랑.ObserveAdd().Subscribe으로 값에 여러개 추가
Subject을 이용하여 플레이어 죽음 통지
OnTriggerEnterAsObservable로 충돌처리체크
ReactiveCommand
https://qiita.com/toRisouP/items/c6fba9f01e6d15dabd79
List 감시
https://kan-kikuchi.hatenablog.com/entry/ReactiveCollection_ReactiveDictionary
lifeCycle
https://psh10004okpro.tistory.com/entry/unity-Script-lifecycle-유니티-라이프-사이클-면접문제?category=892860