上節回顧
上一篇文章中,我們介紹了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變換,把這些概念理解透徹,會大大提升開發效率。
更多內容,歡迎關注: