UniRx精講(一):UniRx簡介&定時功能實現

涼鞋的筆記發表於2020-06-12

1.UniRx 簡介

UniRx 是一個 Unity3D 的程式設計框架。它專注於解決時間上非同步的邏輯,使得非同步邏輯的實現更加簡潔和優雅。

簡潔優雅如何體現?

比如,實現一個“只處理第一次滑鼠點選事件”這個功能,使用 UniRx 實現如下:

Observable.EveryUpdate()
			.Where(_ => Input.GetMouseButtonUp(0))
			.First()
			.Subscribe(_ => { // do something   });

程式碼做的事情很簡單:

  1. 開啟一個 Update 的事件監聽。
  2. 每次 Update 事件被呼叫時,進行滑鼠是否抬起的判斷。
  3. 如果判斷通過,則進行計數,並且只獲取第一次點選事件。
  4. 訂閱/處理事件。

如果在 Unity 中,使用傳統的方式實現如上功能,首先要建立一個成員變數來記錄點選次數/是否點選過,然後在指令碼中建立一個 Update 方法來監聽滑鼠抬起的事件。

如果在 Update 方法中,除了實現滑鼠事件監聽這個功能之外,還要實現其他的功能。那麼 Update 裡就會充斥著大量的狀態判斷等邏輯。程式碼非常不容易閱讀。

而 UniRx 提供了一種程式設計思維,使得平時一些比較難以實現的非同步邏輯(比如以上這種),使用 UniRx 輕鬆搞定,並且不失程式碼的可讀性。

當然 UniRx 的強大不僅僅如此。

它還可以:

  • 優雅實現 MVP(MVC)架構模式。
  • 對 UGUI/Unity API 提供了增強,很多需要寫大量程式碼的 UI 邏輯,使用 UniRx 優雅實現。
  • 輕鬆完成非常複雜的非同步任務處理。
  • ......

最最重要的是,它可以提高我們的編碼效率,同時還給我們的大腦提供一個強有力的程式設計模型。而 UniRx 本身的原始碼非常值得研究學習,就連大名鼎鼎的 uFrame 框架,在 1.6 版本之後,使用 UniRx 進行了大幅重構,其事件/資料繫結層使用 UniRx 強力驅動。

筆者的 QFramework 也同樣引入了 UniRx,有一大批的框架使用者都是因為支援了 UniRx 慕名而來。

為什麼要用 UniRx ?

UniRx 就是 Unity 版本的 Reactive Extensions,Reactive Extensions 中文意思是:響應式擴充套件,響應式指的是觀察者和定時器,擴充套件指的是 LINQ 的操作符。Reactive Extensions 以擅長處理時間上非同步的邏輯、以及極簡的 API 風格而聞名。

我們都知道,遊戲很多的系統都是在時間上非同步的,所以 Unity 開發者所需要實現的非同步邏輯是非常多的。這也是為什麼 Unity 官方在引擎層實現了 Coroutine(協程)這樣的概念。

在遊戲中,像動畫的播放、聲音的播放、網路請求、資源載入/解除安裝、Tween 動畫、場景過渡等都是在時間上非同步的邏輯。甚至是遊戲迴圈(Every Update、OnCollisionEnter 等)、感測器資料(Kinect、Leap Motion、VR Input 等)都是時間上非同步的邏輯(事件)。

當我們在專案中實現以上的邏輯的時候,往往使用的方式是用大量的回撥實現,最終隨著專案的擴張會導致傳說中的”回撥地獄”。

相對較好的方法則是使用訊息/事件進行實現,結果導致“訊息滿天飛”,導致程式碼非常難以閱讀。

以上的任務使用 Coroutine 也是非常不錯的,但是 Coroutine 在 Unity 使用的時候,需要定義一個方法。寫起來是非常程式導向的。當邏輯稍微複雜一點,就很容易造成 Coroutine 巢狀 Coroutine,程式碼是非常不容易閱讀的(強耦合)。

而 UniRx 的出現剛好解決了這個問題,它介於回撥和事件之間。

它有事件的概念,只不過它的事件是像流水一樣流過來,而我們要做的則是簡單地對這些事件進行組織、變換、過濾、合併就可以得到我們想要的結果了。

它也用掉了回撥,只不過它的回撥是在事件經過組織之後,只需要呼叫一次就可以進行事件處理了。

它的原理和 Coroutine (迭代器模式)、LINQ 非常相似,但是比 Coroutine 強大得多。

UniRx 將時間上非同步的事件轉化為響應式的事件序列,通過 LINQ操作可以很簡單地組合起來。

為什麼要用 UniRx? 答案就是遊戲本身有大量的在時間上非同步的邏輯,而 UniRx 恰好擅長處理這類邏輯,使用 UniRx 可以節省我們的時間,同時讓程式碼更簡潔易讀。

Rx 只是一套標準,其他的語言也有實現,如果在 Unity 中熟悉了這套標準,那麼在未來,大家在做別的語言的專案的時候,很容易獲得 Rx 的能力。

專欄內容:

  1. 實踐並講解開發中最常用的 UniRx API。
  2. 對 UniRx 進行一個全方面的簡介。
  3. 在每個階段結束後就會結合所學的知識進行專案實踐。
  4. UniRx 操作符大全。
  5. UniRx 原始碼閱讀。
  6. UniRx 背後的設計原理及設計模式學習。
  7. LINQ、Coroutine 底層原理剖析。
  8. BindingsRx、uFrame 原始碼閱讀。

OK,讓我們從下一篇開始,感受一下 UniRx 的魅力吧。

知識地圖

image.png

2.定時功能實現

在 Unity 開發中,延時是我們經常要實現的功能,這個功能對於有點經驗的開發者來說不難。

最常見的實現方式如下:

using UnityEngine;

public class CommonDelayExample : MonoBehaviour
{
	private float mStartTime;

	void Start()
	{
		mStartTime = Time.time;
	}

	void Update()
	{
		if (Time.time - mStartTime > 5)
		{
			DoSomething();
			// 避免再次執行
			mStartTime = float.MaxValue;
		}
	}

	void DoSomething()
	{
		Debug.Log("DoSomething");
	}
}

這是很多初學者剛入門時候的實現方式,而初學者們隨著深入學習後來接觸到了 Coroutine(協程),使用 Coroutine 實現定時功能會更容易,而且也是更好的選擇,實現如下:

using System;
using System.Collections;
using UnityEngine;

public class CoroutineDelayExample : MonoBehaviour
{
	void Start()
	{
		StartCoroutine(Timer(5, DoSomething));
	}

	IEnumerator Timer(float seconds, Action callback)
	{
		yield return new WaitForSeconds(seconds);
		callback();
	}

	void DoSomething()
	{
		Debug.Log("DoSomething");
	}
}

協程已經把程式碼精簡了很多,不過接下來有更厲害的,使用 UniRx。

程式碼如下:

Observable.Timer(TimeSpan.FromSeconds(5)).Subscribe(_ => { /* do something */ });

當然以上程式碼是沒有和 MonoBehaviour 進行生命週期繫結的,也就是說當 MonoBehaviour 銷燬了之後,這個定時邏輯可能還在執行,這樣就會有造成空指標異常的風險。

要解決也很簡單,程式碼如下:

Observable.Timer(TimeSpan.FromSeconds(5))
		.Subscribe(_ => { /* do something */ })
		.AddTo(this);

只要加上一個 AddTo(this) 就可以了。
這樣,當 this(MonoBehaviour) Destroy 的時候,這個延時邏輯也會銷燬掉,從而避免造成空指標異常。

三行程式碼,寫下來大約 20 秒的時間,就搞定了一個實現起來比較麻煩的邏輯。

今天的內容就這些。

知識地圖

image.png

更多內容

相關文章