2022년 12월 28일 수요일

Unity Zenject 사

Unity Zenject 사

Unity DI 어셋 Zenject 사용

의존성 주입을 보다 쉽게

클래스나 오브젝트간에 서로 참조하는 의존관계를 약하게 하여 프로그램의 유연성과 테스트커버리지를 높이기 위한 방법은 여러가지가 있다.
물론 직접 Interface등을 설계하여 만들수도 있지만, Zenject 같은 DI 라이브러리를 사용하면 좀더 편하게 의존관계를 설정할수있다.
아키텍쳐관점에서 본다면, VIew-Model-Repository 가 서로 강하게 연결되어있으면 코드의 수정이나 재활용시에 매우 손이 많이 가기 때문에 최대한 인터페이스화 하여 설계하여야한다.

Context

SceneContext

SceneContext는 Hiracky 메뉴에서 마우스오른쪽 버튼을 눌러서 생성할수 있다.
화면에 SceneContext는 Scene에서 필요로할 이런저런 것들(Context)을 정의해놓는 곳이다.
일단 뭔지 함보자.
1.Hierachy -> 오른쪽 마우스 -> Zenject -> SceneContext 로 Scene에서 사용할 Context 객체를 만든다.
enter image description here

  1. SceneContext객체를 선택하면 Inspector속성에 스크립트들을 지정하라는 곳이 보인다.
    enter image description here

  2. Unity 의 MonoBehavior가 시작될때, 즉 화면에 씬이 로딩되는 순간에 뭔가를 처리해야 할때는 Mono Installers영역에 해당 스크립트를 넣어주면된다. 지금 예제에서는 모노가 시작될때 디버그 영역에 문자를 출력할 것이기 때문에 해당스크립트를 작성하고 Mono Installers에 지정할것이다.

  3. 적당한 곳에 스크립트(MyInstaller.cs)를 만들고 아래처럼 입력하자.
    아래그림처럼 만든 스크립트를 Mono Installer에 넣어야 도니다.
    enter image description here

public class MyMonoInstaller : MonoInstaller  
{  
  public override void InstallBindings()  
 {  
   Container.Bind<string>().FromInstance("Hello!");  
   //간단히 설명하자면  Container 에서  string형식으로 주입을 요청하는 곳에서는 "Hello" 인스턴스를 생성해서 돌려주도록 Bind를 하자. 라는의미이다.
   Container.Bind<Greeter>().AsSingle().NonLazy();  
   //마찬가지로 Container 에서  Greeter 클래스 형식으로 주입을 요청하는 곳에서는 컨테이너상에 오로지 한개만 존재하는 (AsSingle) 인스턴스를 생성해서 돌려주도록 Bind를 하자. 라는의미이다. 
   //NonLazy가 붙어있으므로 MyMonoInstaller 스크립트가 SceneContext에서 로드됨과 동시에 Greeter 객체가 생성된다.
 }
}  
  
public class Greeter  
{  
  public Greeter(string message)  
 {  
   Debug.Log(message);  
 }
}
  1. 위에서 만든 스크립트를 적절한 GameObject나 또는 SceneContext등에 붙여주고 나면, SceneContext의 Mono Installers 부분에 해당 객체목록이 보인다.
    enter image description here
  2. 플레이 해보면 "Hello"문자열이 Console에 출력된다. ( 출력이 안되었다면, 스크립트오류를 확인하거나, 위의 Auto Run 이 체크되어있나를 확인한다.)
  3. 정리해 보자면 씬을 플레이 하는 순간 SceneContext 객체에 지정된 스크립트들이 실행되는데, 이때 MonoBehavior를 상속받아 실행되는 Mono Installers 부분에서는 MyMonoInstaller에 정의된 스크립트를 실행하도록 하는것이다.
  4. 정리하자면
    1. SceneContet를 Hierarchy에 추가한다.
    2. MonoInstaller를 확장하는 새로운 스크립트를 만들어서 현재 씬에서 주입이 필요한 모델 또는 객체 등을 Zenject의 주입패턴(Bind)에 맞게 주입을 하는 내용을 입력한다.
    3. 해당객체를 각각의 스크립트 등에서 new선언없이 사용한다.
  5. 조금 바꾸어 보자.
    Scene에 아무 객체나 만들고, 아래와 같은 스크립트를 지정한다.
public class monoin : MonoBehaviour  
{
	[Inject]  Greeter _greeter;  
	[Inject] string msg;  
	private void Start()  
	{  
	  _greeter.Greeting(msg);  
	}
}
// Greeter도 수정해보자.

public class Greeter  
{  
  public Greeter(string message)  
 {  
   Debug.Log(message);  
 }
   
public void Greeting(string say)  
{  
  Debug.Log(say);  
}
}

위에서 string 타입의 값은 무조건 "Hello!"를 되돌려 주게 되어있으므로, Hello가 Greeter생성시 한번, Greeting호출시 한번 더 호출된다.

  1. 인터페이스를 Bind 하도록 바꾸어 보자.
    인터페이스를 공유하는 두개의 클래스를 zendject의 bind id에 따라 구분해서보여주자.
// MonoInstaller에 지정해준 스크립트는 다음과 같다.
public class MyMonoInstaller : MonoInstaller  
{  
  public override void InstallBindings()  
 {  Container.Bind<string>().FromInstance("Hello!");  
  Container.Bind<IGreeter>()  
 .WithId("FromGreeter")  
 .To<Greeter>()  
 .AsSingle()  
 .NonLazy()  
 .IfNotBound();  
  Container.Bind<IGreeter>()  
 .WithId("FromMyGreeter")  
 .To<MyGreeter>()  
 .AsSingle()  
 .NonLazy()  
 .IfNotBound();  
 }}  
  
public class Greeter : IGreeter  
{  
  public string Greeting(string say)  
 {  
  return say + " ^_^";  
 }}   
public class MyGreeter : IGreeter  
{  
  public string Greeting(string say)  
 {  return say+ " @_@ My";  
 }}
 
// 호출쪽에서는 다음과 같다.
[Inject(Id = "FromGreeter")] IGreeter _greeter;  
[Inject (Id = "FromMyGreeter")] IGreeter _myGreeter;  
[Inject] string msg;  
private void Start()  
{  
  Debug.Log(_greeter.Greeting(msg));  
  Debug.Log(_myGreeter.Greeting(msg));  
  //Use When SetActive(true) and enabled = true  
}
물론 두개의 다른(^^, @@) 가 출력된다.

사례1. 두개의 씬을 이동할때 공유된 Model을 이용하자.

씬이 바뀌어도 유지되야 되는 변수, 재화,에너지 등이 있다. 이를 씬을 로드할때마다 불러오고/검증하는것은 불필요한 리소스낭비이다.
따라서 두개의 씬이 같은 모델을 공유하고 같은 데이터를 참고하도록 하면 되는 데 이때 Zenject의 LoadScene의 extraBinding을 이용하여 씬이동시에 공유할 인스턴스를 넘겨주면 된다.

1. 먼저 두개의 씬 A,B를 만들자

각 씬에는 서로 씬이동하는 버튼과 텍스트, 공격 버튼을 배치해서 각 씬에서 공격버튼을 눌렀을때 텍스트가 변경되도록 하자.
씬을 이동하더라도 텍스트의 숫자는 같은 데이터를 참고하므로 초기화 되지 않도록 한다.

2. 각 씬에 SceneContext를 추가한다. Zenject를 이용하려면 추가해야한다.

UniRx

UniRx

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가 여러가지 형식으로 발행을 할때, 발행내용(형식)에 따라서 각기 다른 구독자에게 내용을 전달하는 등의 구체적인 작업이 가능하다.

간단한 발행-구독 관계의 예이다.

//UniRx의 Subject 객체를 생성, 발행형식은 string이다.  
var sub = new Subject<string>();  
  
//Subscribe함수를 이용하여 발행처가 발행한 값(string)을 receivvedText에 받아서 필요한 처리를 함.  
sub.Subscribe(receivvedText => Debug.Log(receivvedText));  
  
//발행처(Subject)인 sub 의 OnNext함수로 발행. sub.OnNext("abcd");  
sub.OnNext("efg");  
  
//abcd 와 efg 가 출력된다.


/* 구독자와 발행처가 각각의 클래스에 존재할때는 구독을 하기위해서 Subject자체를 이용하는 것은 좋지않다. 
이때 이용하는것이 IObservable 인터페이스이다. 
발행처의 Subject변수는 private 로 하고, IObservable 인터페이스를 이용하여 공개를 하고 이용하여 발행처의 존재를 몰라도 구독을 신청할수 있도록 해준다.
Class A {
	private  Subject<int>  subject  =  new  Subject<int>();  

	public  IObservable<int>  OnTimeChanged  {  
	    get  {  return  subject;  }  
	}
}

Class B {
	timeCounter.OnTimeChanged.Subscribe
	void b {
		timeCounter.OnTimeChanged.Subscribe(count  =>  {
			Debug.Log(count);
		});
	}	
}
*/

Operator

Rx 에서 발행메시지를 효율적으로 제어하는 여러가지 기능들이 있는데 이를 오퍼레이터라고 한다.
오퍼레이터를 이용하여 구독자는 발행메시지를 필터링 하거나 발행메시지를 추가로 가공하여 구독메시지의 형태를 따로 만들수도 있다.
오퍼레이터수가 하도 많아서 다 기재하기는 힘드니까 몇몇 대표적인 것들만 예를 들겠다.
필터계열

  • Where (x => x == "Player)
    (발행받은 메시지 => {true, false 로 해당메시기가 원하는 메시지 인지 필터링}).
  • Select
    Javascript나 Kotlin 에서 map 으로 불리는 것과 동일한 기능을 한다. 각각의 객체를 반복하며 필요한 처리를 한 결과객체를 돌려준다.
  • Distinct
    같은 메시지가 반복될때, 한개만 발행하도록 한다.
  • Buffer
    버퍼를 지정하고 버퍼에 메시지가 찰때까지 발행하지 않는다. 메시지 개수가 다 차면 발행한다…
//필터링하여 구독 신청
sub.Where(count => count > 3) //3보다 메시지만 모아서 
  .Select(count => count * 10)   //모두 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){
  //WaitForSeconds로 기다림
  yield return new WaitForSeconds(waitTime);

  //전달받은 람다 실행
  action();
}

UniRx의 마법을 보자

Observable.Timer(TimeSpan.FromSeconds(2)).Subscribe(_=>Debug.Log("2초 되었다."));
//단, Timer는 UniRx에서 간편하게 제공하는 시간 오퍼레이터이다.(반칙인가?)

UniRx의 스트림 관련 기능이 제일 빛나는 예제는 역시 버튼의 중복 클릭, 동반클릭 처리이다.
UniRx에는 여러가지 오퍼레이터가 있는데 그중에 Buffer, Skip, ThrottleFirst 등 순서대로 발생하는 이벤트를 스트림에 흘려보내 그중에 조건에 맞는 상태만 간단하게 골라서 처리할수 있도록 해준다.
버튼을 두번 클릭해야 되는 처리를 한다고 생각하자. 일단 버튼클릭에 대한 count변수를 넣어서 2번이 되었는지 if문등으로 체크를 해야한다.
UniRx의 마법을 보자

GetComponent<Button>()
  .OnClickAsObservable() // OnClickAsObservable 이것은 UniRx에서 간단하게 제공해주는 클릭옵서저, 물론 직접 만들어도 된다.
  .Buffer(2) //버퍼오퍼레이터는 지정한 숫자만큼이 되야 발행메시지를 얻을수있다.
  .Subscribe(_=>Debug.Log("두번 눌렀네."));

두개의 버튼이 클릭이 되었는지 판단

button2.OnClickAsObservable()  
 .Zip(button.OnClickAsObservable(), (b1, b2) => "Two Clicked")  
 .First() // 현재의 Obsevable 을 Single 로 바꾸어 onSuccess로 바꾸고 종료한다.  
  .Repeat() //다시 클릭을 받기 위해서  onComplete 된 현재의 Obserable을 반복시킨다.  
  .Subscribe(msg => { Debug.Log(msg); });

마우스로 객체 회전제어

void Start ()   
   {  
  
  this.OnMouseDownAsObservable()  
 .SelectMany(_ => this.UpdateAsObservable())  
 .TakeUntil(this.OnMouseUpAsObservable()) // Until the mouse is released  
  .Select(_ =>                                 // get the amount of mouse movement.  
  new Vector2(Input.GetAxis("Mouse X"),Input.GetAxis("Mouse Y")))  
  //.Repeat()                                 // Repeat causes infinite repeat subscribe at GameObject   
                                                       // was destroyed. which leads, if in UnityEditor, Editor goes to freeze.  
  .RepeatUntilDestroy(this) // Since the stream is completed TakeUntil re Subscribe  
  .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 값의 변화를 감지하자.

  • HP, MP 소모를 바로바로 감지하자.
// 정수값 myHp 가 변경될때 마다 로그를 찍는다.
Class A {
	public ReactiveProperty<int> myHp = new ReactiveProperty<int>(1);  
	public A() {
		myHp.Subscribe( x =>  
		    Debug.Log(x)  
		);   
		/*
		myHp
		.Where(hp => (hp>5)) //Where추가하면 5이상일때만 출력된다.
		.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))  // 값이 10이상일때는  true를 반환하여 준다.
 .ToReadOnlyReactiveProperty();

isHpFull은 hp가 10이상일때만 true이고 나머진 false이다.

ReadOnlyReactiveProperty<String> isHpFull = myHp    
 .Select(hp => "myHp" + hp) // myHp 1 처럼 문자열을 리턴한다.
 .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);  //3
a.Value= 98;  
Debug.Log(c);  //100
b.Value = 0;  
Debug.Log(c); //98

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); // 100  
  
var character2 = new Charactor();  
currentCharacter.Value = character2;  //자동진행되던 동료캐릭터를 선택
character2.Attack.Value = 200;  
Debug.Log(currentAttack.Value); // 200  
  
character2.Attack.Value = 220;  
Debug.Log(currentAttack.Value); // 220  
  
character1.Attack.Value = 50;  //자동진행되던 마법사 캐릭터가 데미지를 심각하게 입음
Debug.Log(currentAttack.Value); // 50  
  
currentCharacter.Value = character1;  // 마법사 캐릭터를 선택함.
Debug.Log(currentAttack.Value); // 50

NPC 제어

WhenAll 오퍼레이터로 NPC들이 자기 할일을 다 끝내면, 완료 라는 메시지를 출력하는 비동기 처리를 해보자

각각의 NPC들을 Observable로 하여 , 각역활이 끝날때 onComplete를 호출하면 완료! 라는 메시지를 출력하자.

// 특정 비동기 처리 스트림 2  
var stream2 = Observable.Start(() =>  
{  
  Debug.Log("stream2 start");  
});  
  
// 버튼을 클릭했을 때 흐르는 스트림  
var stream3 = button.onClick.AsObservable().Take(1);  
stream3.Subscribe(x =>  
{  
  Debug.Log("stream3 start");  
});  
  
// 버튼2을 클릭했을 때 흐르는 스트림  
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"));

//*** Merge + FOrEachAsynce로 하면 각각의 로드가 끝난후에 메시지를 띄울수 있다.
  
    .Merge(stream1, stream2, stream3, stream4Wrapper)  
 .ForEachAsync(x => Debug.Log("Loaded : " + x))

카메라 제어

마우스드래그 해서 카메라 이동하기

마우스로 화면을 클릭하여 드래그 하면 이벤트를 감지하여 이전드래그위치와 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 ); }
}
//OnNext를 원하는 타이밍에 발행하고 싶은 경우는 아래와 같이 한다.
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()  
 {  // Observable for AsyncOperation  
  var cubeLoad = Resources.LoadAsync<GameObject>("Prefabs/Cube")  
 .AsAsyncOperationObservable()  
 .Last();  
  
  // Observable for AsyncOperation  
  var circleLoad = Resources.LoadAsync<GameObject>("Prefabs/Circle")  
 .AsAsyncOperationObservable().Delay(TimeSpan.FromSeconds(3))  
 .Last();  
  
  // Observable for AsyncOperation  
  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