UniRx入門系列 (三)

原文鏈接:https://qiita.com/toRisouP/items/86fea641982e6e16dac6

上節回顧


上一篇文章中,我們介紹了OnNext、OnError、OnCompleted和IDisposable的用途,還介紹瞭如何管理流的生命週期,下面我將介紹一下流的來源。

什麼是流的來源(消息的發佈者)


UniRX中的流由以下三個部分組成:

  • 發佈消息的源(如Subject)
  • 傳遞消息的操作符(Where、Select等等)
  • 消息的訂閱(Subscribe)
    剛開始接觸UniRx的人,大多會有些疑惑,流是如何創建的。接下來我們將討論如何創建一個流。

可能作爲流源的一些東西


可以使用UniRx提供的流源,也可以自己實現自己需要的流源。如果你想使用UniRx提供的流源,那麼你可以通過以下幾種方式來創建。


  • 使用Subject系列
  • 使用ReactiveProperty
  • UniRx提供的方法
  • 使用UniRx.Trigger系列
  • 使用UniRx提供的協程
  • 使用UniRx轉換後的UGUI事件
    我們逐一解釋一下。

Subject系列


Subject 在之前已經出現過很多次了。但他是使用這個Subject系列的基礎。如果你想創建一個你自己的流,併發布一些消息,你可以繼續使用Subject。對應的,Subject有一些衍生的用法,它們有各自的用法;最好是依據不同的用途來選擇合適的用法,下面做一個簡單的說明:

  • Subject 最基礎的一項,OnNext執行後,發佈對應的值。
  • BehaviorSubject 緩存最後發佈的值,執行到Subscribe時,發佈當前值,也可以設置初始值
  • 緩存之前發佈過的所有的值,當Subscribe時,將緩存的值彙總併發布

AsyncSubjecr 在沒有執行OnNext的情況下,向內部緩存值;並在執行OnCompleted時,只發布最後一個OnNext的值。AsyncSubject和Future和Promise一樣。如果你想在結果出來的時候獲取它,你可以使用異步的方式來處理它。

ReacriveProperty系列


ReactivePropert
是爲一個普通變量添加一些Subject的功能(具體的實現過程也是這樣的),我們可以像定義變量那樣來定義和使用它,如下:

void Start(){
    var rp = new ReactiveProperty<int>(10);
    rp.Value=20;
    var currentVlue=rp.Value;
    rp.Subscribe(x=>Debug.Log(x));
    rp.Value=30; 
}

輸出如下:

20
30

另外,ReactiveProperty可以像Unity中的publish變量一樣,顯示在Inspector面板中;當然,要這樣做的話,你應該使用爲不同類型定義的Reactiveproperty而不是使用ReactiveProperty的泛型版本。另外,我們將在下一節中詳解ReactiveProperty在mv®p模式中的價值,所以,務必提前掌握好ReactiveProperty。

public class TestReactiveProperty : MonoBehaviour {

	private IntReactiveProperty playerHealyh=new IntReactiveProperty(100);

	void Start () {
		playerHealyh.Subscribe(x=>Debug.Log(x));
	}
}

ReactiveCollection


ReactiveCollection與ReactiveProperty類似,它是內置了一個通知狀態變化功能的List,ReactiveCollection可以像List一樣使用,更棒的是,ReactiveCollection可以用來對List狀態的變化進行Subscribe,所支持的狀態變化訂閱如下:

  • 添加元素時
  • 刪除元素時
  • 集合數量變化時
  • 集合元素變化時
  • 元素移動時
  • 清除集合時
void Start () {
		var collection = new ReactiveCollection<string> ();

		collection.ObserveAdd ()
			.Subscribe (x => {
				Debug.Log (string.Format ("Add {0}={1}", x.Index, x.Value));
			});

		collection.ObserveRemove ()
			.Subscribe (x => {
				Debug.Log (string.Format ("Remove {0}={1}", x.Index, x.Value));
			});
		collection.Add ("Apple");
		collection.Add ("Baseball");
		collection.Add ("Cherry");
		collection.Remove ("Apple");
	}

輸出如下:

Add [0] = Apple
Add [1] = Baseball
Add [2] = Cherry
Remove [0] = Apple

ReactiveDictionary<T1,T2>


這是Dictionary的Reactive版,他的行爲幾乎和ReactiveCollection一樣,參考如上。

UniRx的工廠方法


UniRX爲構建流源提供了一系列的工廠方法。這時,我們可以很容易的創建一些複雜的流,僅僅通過Subject時無法實現的。如果你在Unity中使用UniRx,你可能不會很頻繁的使用到UniRx提供的工廠方法,但是在某些地方,它是必須的。由於這一類方法較多,我們提取幾個使用比較頻繁的介紹一下。

Observable.Create

Obseervable.Create是一個靜態方法,你可以自由的創建一個發佈值的流。例如,在一些程序中,將處理調用規則的細節隱藏在這個方法中,使用這個方法,只在流中檢索結果值。Observable.Create接收一個參數Func<IObserver, IDisposable> subscribe,返回IObservable,使用如下:

void Start () {
		Observable.Create<int> (observer => {
			Debug.Log ("Start");
			for (int i = 0; i < 100; i += 10) {
				observer.OnNext (i);
			}
			Debug.Log ("Finished");
			observer.OnCompleted ();
			return Disposable.Create (() => {
				Debug.Log ("Dispose");
			});
		}).Subscribe (x => Debug.Log (x));
	}

執行輸出如下:

Start
0
10
20
30
40
50
60
70
80
90
100
Finished
Disposable

Observable.Start

Observable.Start是一個工廠方法,在不同的線程上運行給定的塊,並且只發佈一個結果值。你可以使用這個方法異步執行一些操作,然後在你希望獲得結果通知時使用它們。

void Start () {
		Observable.Start (() => {
				var req = (HttpWebRequest) WebRequest.Create ("https://www.baidu.com");
				var res = (HttpWebResponse) req.GetResponse ();
				using (var reader = new StreamReader (res.GetResponseStream ())) {
					return reader.ReadToEnd ();
				}
			}).ObserveOnMainThread ()
			.Subscribe (x => Debug.Log (x));
	}

注意,Observable.Start時在另外一個線程上執行,然後直接在該線程中執行Subscribe,所以,在非線程安全的Unity中會出現問題。所以我們需要將消息從當前線程傳遞到主線程中,請使用ObserveOnMainThread操作符。使用這個操作符之後,他將轉換到Unity的主線程上運行。

Observable.Timer/TimeFrame


Observable.Timer是在一定時間後發佈消息的一個工廠方法。如果使用真實時間,請使用Observable.Timer方法;如果使用Unity的幀數制定,阿麼使用TimeFrame方法。Timer和TimeFrame的行爲根據你指定參數的不同而不同。如果你只指定一個參數,那麼,你會以一個OneShot動作結束;如果你指定了兩個參數。你會定期發佈信息(間隔時間);你還可以通過指定調度器來指定其運行在指定的線程上。另外,類似,還存在Observable.Interval/IntervalFrame方法。

void Start () {
		Observable.Timer (TimeSpan.FromSeconds (5))
			.Subscribe (_ => Debug.Log ("流失了5秒"));

		Observable.Timer (TimeSpan.FromSeconds (5), TimeSpan.FromSeconds (1))
			.Subscribe (_ => Debug.Log ("5秒之後,每間隔1妙發佈一次"))
			.AddTo (gameObject);
	}

如果使用Timer TimerFrame定期執行的話,一定要記得Dispose,如果你已經不需要定期執行的這項操作了,但是你依然沒有將其Dispose,就會導致內存泄漏或者NullReferenceException.

UniRx.Trigger系列


要使用UniRx.Trigger,先導入using UniRx.Trigger。其將Unity的回調函數轉化爲UniRx中的IObservable,可以用操作UniRx的方式來操作Unity回調函數。因爲Triggers數量衆多,這裏不做過多介紹,請參考官方文檔。由於Unity提供的大多數回調函數都可以被當做流來獲取,而且當GameObject被髮布時,流會自動發佈,所以這裏不同擔心流的生命週期。

void Start () {
		var isForceEnabled = true;
		var rigidBody = GetComponent<Rigidbody> ();
		this.FixedUpdateAsObservable ()
			.Where (_ => isForceEnabled)
			.Subscribe (_ => rigidBody.AddForce (Vector3.up));

		this.OnTriggerEnterAsObservable ()
			.Where (x => x.gameObject.tag == "WarpZone")
			.Subscribe (_ => isForceEnabled = true);

		this.OnTriggerExitAsObservable ()
			.Where (x => x.gameObject.tag == "WarpZone")
			.Subscribe (_ => isForceEnabled = false);
	}

使用Triggers將Unity的回調函數變成了一個流,那麼把所有的事件處理都彙總到Awake/Start中就成了可能,下一節中我們再詳述。

協程轉換

事實上,Unity中的協程和UniRx的可轉化型時比較好的,UniRx提供了一些方法,使得IObservable和Unity協程的轉化變得相當容易。如果你想從Unity協程轉化爲IObservable,你可以利用Observable.Fromecoroutine來實現。某些情況下,與其通過操作鏈中複雜的操作符來構建複雜的流,不如使用協程來實現,這種方式實現更簡單、容易;下一節中,我們會詳細的介紹UniRx和Unity中協程的結合,所以,只在這裏做簡單的介紹。

public class Timer : MonoBehaviour {

	public bool IsPaused { get; private set; }
	void Start () {
		Observable.FromCoroutine<int> 
		(observer => GameTimerCoroutine (observer, 60))
			.Subscribe (t => Debug.Log (t));
	}

    private IEnumerator GameTimerCoroutine(IObserver<int> observer, int initialCount)
    {
       var current=initialCount;
	   while(current>0){
		   if (!IsPaused){
			   observer.OnNext(current--);
		   }
		   yield return new WaitForSeconds(1);
	   }
	   observer.OnNext(0);
	   observer.OnCompleted();
    }
}

UGUI事件轉換

UniRx爲UGUI提供了獨特的實現,結合之前說過的ReactiveProprtty,可以非常便捷的描述View(視圖)和Model(模型)之間的關係。本節暫時不介紹mv®p模式,只介紹UGUI事件到UniRx的轉換,如下所示:

   void Start()
   {
       var button=GetComponent<Button>();
       button.OnClickAsObservable()
       .Subscribe(x=>Debug.Log("點擊了當前按鈕"));

       var inputField=GetComponent<InputField>();
       inputField.OnValueChangedAsObservable().Subscribe(x=>Debug.Log(x));
       inputField.OnEndEditAsObservable().Subscribe(x=>Debug.Log(x));

       var slider=GetComponent<Slider>();
       slider.OnValueChangedAsObservable().Subscribe(x=>Debug.Log(x));

       inputField.onValueChanged.AsObservable().Subscribe();
       inputField.OnValueChangedAsObservable();
       inputField.onValueChanged.AsObservable();
   }

Observable.EvenryUpdate

Observable.EveryUpdate 以 Time.deltatime 的時間間隔更新流。之前說 UniRx.Triggers和UpdateAsObservabel 的行爲與 GameObject 相關聯,當對象被 Destroy 時發佈 OnCompleted。但 Observable.EvenryUpdate 的行爲卻不與 GameObject 的行爲相關聯,當 GameObject 對象被銷燬時, Observable.EveryUpdate 並不會停止,除非你手動釋放。

ObserveEveryValueChanged

ObserveEveryValueChanged 在流源中是一個比較特殊的存在,它被定義爲類(class)的擴展方法。通過使用這個方法,你可以在每一幀中監控任何對象的參數,並創建一個變化發生時的通知流。

void Start()
    {
        var characterController = GetComponent<CharacterController>();

        characterController
        .ObserveEveryValueChanged(character => character.isGrounded)
        .Where(x => x)
        .Subscribe(_ => Debug.Log("落地"))
        .AddTo(this.gameObject);

        Observable.EveryUpdate()
        .Select(_ => characterController.isGrounded)
        .DistinctUntilChanged()
        .Where(x => x)
        .Subscribe(_ => Debug.Log("落地"))
        .AddTo(this.gameObject);
    }

總結:


使用多種方法創建流

  • Subject 使用Subject的一系列方法
  • ReactiveProperty 使用ReactiveProperty系列
  • 使用UniRx.Trigger 轉化的Unity回調方法
  • 使用UniRx轉化Unity協程
  • 使用UniRx轉化的UGUI事件

個人,開發中會經常的UniRx.Trigger、ReactiveProperty、UGUI變換,把這些概念理解透徹,會大大提升開發效率。

更多內容,歡迎關注


在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章