# UniRx 初体验

查看源图像

# 链式编程

所谓链式编程 —— 使用. . . 像是链条一样进行编程。

🔖 下面是一个延迟调用的示例:

public class UniRxTest : MonoBehaviour
{
   private void Start()
   {
      Debug.Log("开始时间:"+Time.realtimeSinceStartup);
      Observable.Timer(TimeSpan.FromSeconds(2)).Subscribe((x) =>
      {
         Debug.Log("现在时间:" + Time.realtimeSinceStartup);
      }).AddTo(this);
      Observable.EveryUpdate().Where(_ => Input.GetKeyDown(KeyCode.A)).Subscribe(_ =>
      {
         Debug.Log("点击了");
      }).AddTo(this);
   }
}

# Ui 数据绑定

原版本不支持绑定 TextMeshPro

UI数据绑定
using System;
using UniRx;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// 数据绑定测试
/// </summary>
public class RxBingData : MonoBehaviour
{
    public Text titleTex;
    public Button clBtn;
    public IModel model;
    private void Awake()
    {
        model = new GameData();
        model.Name.SubscribeToText(titleTex);
        clBtn.OnClickAsObservable().Subscribe(_ =>
        {
            model.Name.Value = "以分行诉分行诉客户方";
        });
        Observable.EveryUpdate().Where(_ => Input.GetKeyDown(KeyCode.A)).Subscribe(_ =>
        {
            model.Name.Value = "1111";
        }).AddTo(this);
    }
}
public interface IModel
{
    StringReactiveProperty Name { get; }
}
public class GameData : IModel
{
    public StringReactiveProperty Name { get; } = new StringReactiveProperty("校长");
}

动画

# 绑定 TextMeshPro

通过扩展方法增加支持

public static class UnityUiExtensions
{
    public static IDisposable SubscribeToText(this IObservable<string> source, TextMeshProUGUI text)
    {
        return source.SubscribeWithState(text, (x, t) => t.text = x);
    }
        
    public static IDisposable SubscribeToText<T>(this IObservable<T> source, TextMeshProUGUI text)
    {
        return source.SubscribeWithState(text, (x, t) => t.text = x.ToString());
    } 
}

动画

# 绑定 ScripTableObject

在此我开始思考是否可以绑定 ScripTableObject?💭

结果是:YES

Asset设定
[CreateAssetMenu]
public class GameSetting : ScriptableObject,IAudioSetting
{
    public StringReactiveProperty ProfileName => profileName;
    public FloatReactiveProperty BgmVolume => bgmVolume;
    public StringReactiveProperty profileName;
    public FloatReactiveProperty bgmVolume;
}
public interface IAudioSetting
{
    StringReactiveProperty ProfileName { get; }
    FloatReactiveProperty  BgmVolume { get; }
}
bing数据
gameSetting.profileName.SubscribeToText(title);
        gameSetting.bgmVolume.SubscribeToText(titleTex);
        
        clBtn.OnClickAsObservable().Subscribe(_ =>
        {
            gameSetting.profileName.Value = "clicked Data";
        });
        
        Observable.EveryUpdate().Where(_ => Input.GetKeyDown(KeyCode.A)).Subscribe(_ =>
        {
            gameSetting.profileName.Value = "keyDown A";
        }).AddTo(this);

动画

数据实时绑定

🏷通过这种方式很快捷方便的就实现了数据的绑定,同时没有污染数据接口

# Subject 和 OnNext

Subject 既可以作为观察者也可以作为被观察对象

image-20220921103234529

private void Start()
    {
        var subject = new Subject<string>();
        
        // 注册处理方式
        subject.Subscribe(m => print($"接收到:{m}"));
        // 传入参数进行处理
        subject.OnNext("你好");
    }

Subject 实现了 ISubject 接口,其中 ISubject 接口声明如下:

public interface ISubject<TSource, TResult> : IObserver<TSource>, IObservable<TResult>
    {
    }
    public interface ISubject<T> : ISubject<T, T>, IObserver<T>, IObservable<T>
    {
    }

在其中实现了 IObserver接口,这是一个微软官方的观察者模式的实现接口,可参考 参考资料2

IObserver
namespace System
{
  /// <summary > 提供用于接收基于推送的通知的机制。</summary>
  /// <typeparam name="T"> 提供通知信息的对象。</typeparam>
  public interface IObserver<in T>
  {
    /// <summary > 向观察者提供新数据。</summary>
    /// <param name="value"> 当前通知信息。</param>
    void OnNext(T value);
    /// <summary > 通知观察者提供程序遇到错误情况。</summary>
    /// <param name="error"> 提供有关错误的附加信息的对象。</param>
    void OnError(Exception error);
    /// <summary > 通知观察者提供程序已完成发送基于推送的通知。</summary>
    void OnCompleted();
  }
}

# 事件的过滤和筛选

private void Start()
    {
        var subject = new Subject<string>();
        
        // 注册处理方式
        subject.Subscribe(m => print($"接收到:{m}"));
        subject.Subscribe(m => print("处理方式二|" + m));
        
        subject.Where(x=>x=="你好").Subscribe(m => print($"接收到特殊:{m}"));
        
        // 传入参数进行处理
        subject.OnNext("你好");        
        subject.OnNext("1");
        subject.OnNext("2");
        subject.OnNext("3");
    }

subject.Where(x=>x=="你好").Subscribe(m => print($"接收到特殊:{m}")); 使用 Where 操作符对输入参数进行了过滤,让其只处理特定的输入信息 😆 。

📖PS: 可以定义自定义的过滤运算符来指定过滤操作。

# UniRX 流

调用 OnError、OnCompleted 将会终止流,并销毁流对象。(后续监听被终止)

var subject = new Subject<string>();
        
        // 注册处理方式
        subject.Subscribe(m => print($"接收到:{m}"));
        // 传入参数进行处理
        subject.OnNext("你好");        
        subject.OnNext("1");
        subject.OnCompleted();  // 流被终止,后续不再执行
        subject.OnNext("2");
        subject.OnNext("3");

# 空参数

需要使用空参数的情况可以如下操作:

var sub = new Subject<Unit>();
        sub.Subscribe(_ => print("触发事件"));
        sub.OnNext(Unit.Default);
  • 🔖对于不再使用的流要及时 OnCompleted 获得 Dispose 来释放内存以免出现空引用或内存泄漏。
  • 🔖可以使用 AddTo() 将流的生命周期绑定到指定对象。对象被销毁,流也会被销毁。

# 流的来源

官方提供了创建流的几种方式:

  • Subject
  • ReactiveProperty
  • ReactiveCollection
  • ReactiveDictionary
  • UniRx 提供的方法
  • UniRx.Trigger
  • UniRx 的协程
  • UniRx 转换后的 UGUI 事件

# ReactiveCollection

ReactiveCollection 与 ReactiveProperty 类似,它是内置了一个通知状态变化功能的 List,ReactiveCollection 可以像 List 一样使用,更棒的是,ReactiveCollection 可以用来对 List 状态的变化进行 Subscribe,所支持的状态变化订阅如下:

  • 添加元素时
  • 删除元素时
  • 集合数量变化时
  • 集合元素变化时
  • 元素移动时
  • 清除集合时
var list = new ReactiveCollection<string>(){"a","b","c"};
        list.ObserveAdd().Subscribe(x =>
        {
            print($"新增元素:{x}");
        });
        list.ObserveRemove().Subscribe(x =>
        {
            print($"删除元素:{x}");
        });
        list.ObserveMove().Subscribe(x =>
        {
            print($"移动元素:{x}");
        });
        list.ObserveReset().Subscribe(_ =>
        {
            print($"重制");
        });
        //...
        
        list.Add("AA");
        list.RemoveAt(0);
        list.Clear();

# UniRx 的工厂方法

# UniRx.Trigger 系列
private void Start()
    {
        this.OnTriggerEnterAsObservable().Subscribe(x =>
        {
            print($"{x.gameObject.name} 进入触发区域");
        });
        this.OnTriggerExitAsObservable().Subscribe(x =>
        {
            print($"{x.gameObject.name} 离开触发区域");
        });
    }

动画

# 在 Player 中的应用

private void Start()
{
    // 间隔 2S 攻击
    this.UpdateAsObservable().Where(_ => Input.GetKeyDown(KeyCode.A)).ThrottleFirst(TimeSpan.FromSeconds(2f))
        .Subscribe(_ => Attack());
}
public void Attack()
{
    print("Attack");
}

# 使用 Unirx 设计 PlayerController

using UnityEngine;
using UniRx.Triggers;
using UniRx;
public class TestUniRX : MonoBehaviour
{
    private CharacterController characterController;
    private BoolReactiveProperty isJumping = new BoolReactiveProperty();
    void Start()
    {
        characterController = GetComponent<CharacterController>();
		this.UpdateAsObservable()
    	.Where(_ => !isJumping.Value)
    	.Select(_ => new Vector3(Input.GetAxis("Horizontal"), 0, Input.GetAxis("Vertical")))
    	.Where(x => x.magnitude > 0.1f)
    	.Subscribe(x => Move(x.normalized));
	
    	this.UpdateAsObservable()
    	.Where(_ => Input.GetKeyDown(KeyCode.Space) 
    	    && !isJumping.Value && characterController.isGrounded)
    	.Subscribe(_ =>
    	{
    	    Jump();
    	    isJumping.Value = true;
    	});
	
    	characterController
    	.ObserveEveryValueChanged(x => x.isGrounded)
    	.Where(x => x && isJumping.Value)
    	.Subscribe(_ => isJumping.Value = false)
    	.AddTo(gameObject);
	
    	isJumping.Where(x => !x)
    	.Subscribe(_ => PlaySoundEffect());
	}
	private void PlaySoundEffect()
	{
	    Debug.Log("播放音效");
	}
	
	private void Jump()
	{
	    Debug.Log("Jump");
	}
	
	private void Move(Vector3 normalized)
	{
	    Debug.Log("Move");
	}
}

# 针对协程流

UniRx 提供一种微协程的协程实现方式,性能比原生协程更佳。

# 串联协程

void Start()
{
    Observable.FromCoroutine(CoroutineA)
        .SelectMany(CoroutineB)
        .Subscribe(_=>Debug.Log("CoroutineA 和CoroutineB 执行完成"));
}
IEnumerator CoroutineA()
{
    Debug.Log("CoroutineA 开始");
    yield return new WaitForSeconds(3);
    Debug.Log("CoroutineA 完成");
}
IEnumerator  CoroutineB()
{
    Debug.Log("CoroutineB 开始");
    yield return new WaitForSeconds(10);
    Debug.Log("CoroutineB 完成");
}

# 并联协程汇总再处理

void Start()
    {
        Observable.WhenAll(
            Observable.FromCoroutine<string>(o => CoroutineA(o)),
            Observable.FromCoroutine<string>(o => CoroutineB(o))
        ).Subscribe(xs =>
        {
            foreach (var item in xs)
            {
                Debug.Log("result:" + item);
            }
        });
    }
    IEnumerator CoroutineA(IObserver<string> observer)
    {
        Debug.Log("CoroutineA 开始");
        yield return new WaitForSeconds(3);
        observer.OnNext("协程A 执行完成");
        Debug.Log("A 3秒等待结束");
        observer.OnCompleted();
    }
    IEnumerator CoroutineB(IObserver<string> observer)
    {
        Debug.Log("CoroutineB 开始");
        yield return new WaitForSeconds(1);
        observer.OnNext("协程B 执行完成");
        Debug.Log("B 1秒等待结束");
        observer.OnCompleted();
    }

# 个人总结

unirx 提供了一种响应式的流编程模式,我觉得 UniRx 最强之处是以下几点:

  • 万事万物皆可以为流
  • 提供一种微协程方式,线程回溯
  • 观察者模式 —— 数据绑定和监听(可用于 UI)
  • 装饰者模式 —— 对操作和流进行灵活裁剪控制(Linq)

🔖 UniRx 是非常优秀的 Unity 框架!在之后我会在项目中尝试去使用它。

# 参考资料

  1. UniRx 入门系列一・开发文档集合 (ronpad.com)
  2. IObserver 接口 (System) | Microsoft Learn
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Fasty 微信支付

微信支付

Fasty 支付宝

支付宝

Fasty 贝宝

贝宝