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


这是Dictionary的Reactive版,他的行为几乎和ReactiveCollection一样,参考如上。

UniRx的工厂方法


UniRX为构建流源提供了一系列的工厂方法。这时,我们可以很容易的创建一些复杂的流,仅仅通过Subject时无法实现的。如果你在Unity中使用UniRx,你可能不会很频繁的使用到UniRx提供的工厂方法,但是在某些地方,它是必须的。由于这一类方法较多,我们提取几个使用比较频繁的介绍一下。

Observable.Create

Obseervable.Create是一个静态方法,你可以自由的创建一个发布值的流。例如,在一些程序中,将处理调用规则的细节隐藏在这个方法中,使用这个方法,只在流中检索结果值。Observable.Create接收一个参数Func 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变换,把这些概念理解透彻,会大大提升开发效率。

更多内容,欢迎关注


UniRx入门系列(三)_第1张图片

你可能感兴趣的:(UniRx)