目录
对于 UI 的增强
UniRx 的Reactive Property 是可以完全实现⼀种 MVC 的变种(MVP),并且是⾮常明确的。这样在开发的时候就不⽤再去纠结怎么实现了。这个概念,就让 UGUI 的开发简化了很多。除此之外,还⽀持了⾮常多的 UGUI 控件。
所有的 UGUI 控件⽀持列出如下 :
Button mButton;
Toggle mToggle;
Scrollbar mScrollbar;
ScrollRect mScrollRect;
Slider mSlider;
InputField mInputField;
void Start()
{
mButton.OnClickAsObservable().Subscribe(_ => Debug.Log("On Button Clicked"));
mToggle.OnValueChangedAsObservable().Subscribe(on => Debug.Log("Toggle " + on));
mScrollbar.OnValueChangedAsObservable().Subscribe(scrollValue =>Debug.Log("Scrolled " + scrollValue));
mScrollRect.OnValueChangedAsObservable().Subscribe(scrollValue =>Debug.Log("Scrolled " + scrollValue);
mSlider.OnValueChangedAsObservable().Subscribe(sliderValue =>Debug.Log("Slider Value " + sliderValue));
mInputField.OnValueChangedAsObservable().Subscribe(inputText =>Debug.Log("Input Text: " + inputText));
mInputField.OnEndEditAsObservable().Subscribe(result =>Debug.Log("Result :" + result));
}
当然除了 Observable 增强,还⽀持了 Subscribe 的增强。
⽐如 SubscribeToText
Text resultText = GetComponent<Text>();
mInputField.OnValueChangedAsObservable().SubscribeToText(resultText);
这段代码实现的功能是,当 mInputField 的输⼊值改变,则会⻢上显示在 resultText 上。
也就是完成了, mInputField 与 resultText 的绑定。除此之外还⽀持, SubscribeToInteractable,之前有过示例。
Unity的生命周期与Trigger
生命周期
对于 Unity 的 Observable 增强,在之前就接触过了。Observable.EveryUpdate 就是⽀持的 Unity 的 API。单单 Update 就是⽀持⾮常多细分类型的 Update 事件捕获:
Observable.EveryFixedUpdate().Subscribe(_ => {});
Observable.EveryEndOfFrame().Subscribe(_ => {});
Observable.EveryLateUpdate().Subscribe(_ => {});
Observable.EveryAfterUpdate().Subscribe(_ => {});
除了 Update 还⽀持其他的事件,⽐如 ApplicationPause, Quit 等。
Observable.EveryApplicationPause().Subscribe(paused => {});
Observable.EveryApplicationFocus().Subscribe(focused => {});
Observable.EveryApplicationQuit().Subscribe(_ => {}):
有了以上这些,就不⽤再去创建⼀个单例类去实现⼀个如 “应⽤程序退出事件监听” 这种逻辑了。
Trigger
Observable.EveryUpdate 这个 API 有的时候在某个脚本中实现,需要绑定 MonoBehaviour 的⽣命周期(主要是 OnDestroy),当然也有的时候是全局的,⽽且永远不会被销毁的。需要绑定 MonoBehaviour ⽣命周期的 EveryUpdate,之前说过需要⼀个 AddTo 来进⾏绑定。但其实这个功能有更简洁的实现方法:
this.UpdateAsObservable()
.Subscribe(_ => {});
这种类型的 Observable 是什么呢?是 Trigger,即触发器。
触发器,字如其意,当某个事件发⽣时,则会将该事件发送到 Subscribe 函数中,而这个触发器,本身是⼀个功能脚本,这个脚本挂在 GameObject 上,来监听 GameObject 的某个事件发⽣,事件发⽣则会回调给注册它的 Subscribe 中。
触发器的操作和其他的事件源 (Observable) 是⼀样的,都⽀持 Where、 First、 Merge 等操作符。
Trigger 类型的 Observable 和我们之前讲的所有的 Observable 在表现上有⼀点⼀点不⼀样:
- Trigger ⼤部分都是都是 XXXAsObsrevable 命名形式的。
- 在使⽤ Trigger 的 GameObject 上都会挂上对应的 Observable XXXTrigger.cs 的脚本。
Trigger 在此之前我们是接触过的。
AddTo 这个 API 其实是封装了⼀种 Trigger: ObservableDestroyTrigger。
就是当 GameObject 销毁时获取事件的⼀个触发器。⼀般的 Trigger 都会配合 MonoBehaviour ⼀起使⽤。
⽐如 ObservableDestroyTrigger 的使⽤代码如下:
this.OnDestroyAsObservable()
.Subscribe(_ => {});
除了 Destroy 还有⾮常多的 Trigger。⽐如各种细分类型的 Update:
this.FixedUpdateAsObservable().Subscribe(_ => {});
this.LateUpdateAsObservable().Subscribe(_ => {});
this.UpdateAsObservable().Subscribe(_ => {});
还有各种碰撞的 Trigger:
this.OnCollisionEnterAsObservable(collision => {});
this.OnCollisionExitAsObservable(collision => {});
this.OnCollisionStayAsObservable(collision => {});
同样 2D 的碰撞也⽀持
this.OnCollision2DEnterAsObservable(collision2D => {});
this.OnCollision2DExitAsObservable(collision2D => {});
this.OnCollision2DStayAsObservable(collision2D => {});
⼀些脚本的参数监听:
this.OnEnableAsObservable().Subscribe(_ => {});
this.OnDisableAsObservable().Subscribe(_ => {});
除了MonoBehaviour,Trigger也⽀持了其他组件类型,⽐如 RectTransform、Transform、UIBehaviour 等等。
详情可以查看 ObservableTriggerExtensions.cs 和 ObervableTriggerExtensions.Component.cs 中的API。
UI Trigger
这里的重点是 UIBehaviour ,它是 UGUI 所有控件的基类,所有的 UGUI 控件会继承 UIBehaviour 的⽀持。
例如,所有的 Graphic 类型都⽀持 OnPointerDownAsObservable、 OnPointerEnterAsObservable、OnPointerEnterAsObservable 等 Trigger。所有的在 Inspector 上显示, Raycast Target 选定的都是 Graphic 类型,包括Image、 Text 等都是,也就是说 Image、 Text 全部⽀持 OnPointerDownAsObservable、 OnPointerEnterAsObservable 等Trigger。
如果想⾃⼰去接收⼀个 OnPointerDown 事件,那就需要实现⼀个 IPointerDownHandler 接⼝,而 UniRx 则把所有的 IXXXHandler 接⼝都做成 Trigger了,这样再也不⽤ UIEventListener.Get(gameObejct).onClick 这种⽅式了,因为这种⽅式问题很多。
UniRx 的实现⾮常细,也就是⼀个 IXXXHandler 就是⼀个 Trigger。
⽤的⽐较多的⼏个 Trigger:
mImage.OnBeginDragAsObservable().Subscribe(dragEvent => {});
mGraphic.OnDragAsObservable().Subscribe(dragEvent => {});
mText.OnEndDragAsObservable().Subscribe(dragEvent => {});
mImage.OnPointerClickAsObservable().Subscribe(clickEvent => {});
⾮常⽅便,除了常⽤的⼏个 Trigger 之外 还有⾮常多的实⽤的 Trigger。⽐如: OnSubmitAsObservable、 OnDropAsObservable 等等。具体可以参考 ObservableTriggerExtensions.Component.cs,只要能想到的基本上 UniRx 都会⽀持。但是要使用各种 Trigger 类型,就要导⼊命名空间:
using UniRx.Triggers;
下面一个小例子,对一张图片添加几个事件:
Image mImage;
void Start()
{
mImage = transform.Find("Image").GetComponent<Image>();
mImage.OnBeginDragAsObservable()
.Subscribe(_ =>
{
Debug.Log("begin drag");
});
mImage.OnDragAsObservable()
.Subscribe(_ =>
{
Debug.Log("dragging");
});
mImage.OnEndDragAsObservable()
.Subscribe(_ =>
{
Debug.Log("end drag");
});
mImage.OnPointerClickAsObservable()
.Subscribe(_ =>
{
Debug.Log("on pointer click");
});
}
Coroutine 的操作
UniRx 对 Unity Coroutine 的⽀持,可以将⼀个 Coroutine 转化为事件源(Observable)。如:
IEnumerator CoroutineA()
{
yield return new WaitForSeconds(1.0f);
Debug.Log("A");
}
private void Start()
{
Observable.FromCoroutine(_ => CoroutineA())
.Subscribe(_ =>
{
// do something
});
}
也⽀持将 Observable 转化为⼀个 Coroutine 中的 yield 对象。如:
IEnumerator Delay1Second()
{
yield return Observable.Timer(TimeSpan.FromSeconds(1.0f)).ToYieldInstruction();
Debug.Log("delay 1 seconds");
}
void Start()
{
StartCoroutine(Delay1Second());
}
FromCoroutine 和 ToYieldInstruction 实现了 Observable 与 Coroutine 之间的互相转化。
⽽在之前说过, Observable 是⼀条事件流。 UniRx 的操作符,⽐如 Merge 可以处理多个流,可以将流进⾏合并。除了合并也⽀持别的操作,⽐如 顺序 (依赖) 执⾏ Coroutine,并⾏执⾏ Coroutine 等等。在之后,通过学习新的操作符,可以让 Coroutine 更加强⼤。
WhenAll:Coroutine 的并⾏操作
WhenAll 的意思是,当所有的事件流都结束,就会触发 Subscribe 注册的回调。
使⽤ WhenAll 可以实现 Coroutine 的并⾏操作。如:
IEnumerator A()
{
yield return new WaitForSeconds(1.0f);
Debug.Log("A");
}
IEnumerator B()
{
yield return new WaitForSeconds(2.0f);
Debug.Log("B");
}
void Start()
{
var aStream = Observable.FromCoroutine(_ => A());
var bStream = Observable.FromCoroutine(_ => B());
Observable.WhenAll(aStream, bStream)
.Subscribe(_ =>
{
Debug.Log("when all 处理了");
});
}
可以看到输出结果的先后顺序:
WhenAll 和 Merge 是同类型的,是处理多个流的操作符。
当然除了并⾏实现 Coroutine 之外,还可以实现:所有的按钮都点击过⼀次的逻辑。如:
void Start()
{
var aStream = mButtonA.OnClickAsObservable().First();
var bStream = mButtonB.OnClickAsObservable().First();
var cStream = mButtonC.OnClickAsObservable().First();
Observable.WhenAll(
aStream,
bStream,
cStream)
.Subscribe(_ =>
{
Debug.Log("All clicked");
}).AddTo(this);
}
当点击完, A、 B、 C 按钮之后,才会有输出。
事件流的结束 OnCompleted
UniRx 的结束事件。
有的事件流是有结束事件的,⽐如 Timer、 First、 Coroutine 等。但有的则没有,⽐如 EveryUpdate 等。使⽤ Subscribe API 进⾏订阅的时候,第⼀个参数是 OnNext 回调的注册,这也是我们⼤部分情况下使⽤的回调。第⼆个参数则是 OnComplete。
Observable.Timer(TimeSpan.FromSeconds(1.0f)).Subscribe(_ =>
{
Debug.Log("OnNext:after 1 second");
}, () =>
{
Debug.Log("OnCompleted");
});
从输出结果可以看到,Timer 是会结束的,另外一个,如:
Observable.EveryUpdate().First().Subscribe(_ =>
{
Debug.Log("OnNext:First");
}, () =>
{
Debug.Log("OnCompleted");
}).AddTo(this);
这样也是会结束的,以为它取得的只是第一帧,但是当把First()去掉的时候,就会发现,它就不会结束了。
再看一个协程的例子:
void Start()
{
Observable.FromCoroutine(A)
.Subscribe(_ =>
{
Debug.Log("OnNext:");
}, () =>
{
Debug.Log("OnCompleted:");
});
}
IEnumerator A()
{
yield return new WaitForSeconds(2.0f);
}
两秒后,会看到两个输出结果,证明它也结束了。
Start:让多线程更简单
在 Unity 中我们⼀般⽤ Thread.Start 开启⼀个线程,但是当逻辑⾮常复杂的时候多线程⾮常难以管理。而 UniRx 改善了这⼀种状况。⼀个”当所有线程运⾏完成后,在主线程执⾏某个任务” 这个功能,使⽤ UniRx 可以这样来实现:
void Start()
{
var threadAStream = Observable.Start(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(1));
return 10;
});
var threadBStream = Observable.Start(() =>
{
Thread.Sleep(TimeSpan.FromSeconds(3));
return 10;
});
Observable.WhenAll(threadAStream, threadBStream)
.ObserveOnMainThread()
.Subscribe(results =>
{
Debug.LogFormat("{0}:{1}", results[0], results[1]);
});
}
三秒钟后,会有结果输出。
在这⾥有两个新的 API,⼀个是 Observable.Start,这个 API 的意思是开启⼀个线程流。另一个是 ObserveOnMainThread,这个 API 的意思是把 WhellAll 的结果转到主线程上。这样 Subscribe ⾥的回调就可以使⽤Unity 的 API 了(Unity 的很多 API 不可以在其他线程中使⽤ )。使⽤ UniRx 来处理线程逻辑⾮常简单。线程和 Coroutine (协程)都可以使⽤ WhenAll 这种操作符。
ObservableWWW ⽹络请求操作
以往我们不管使⽤ WWW 还是 UnityWebRequest 都要使⽤ Coroutine 去驱动。但是使⽤协程写出来的代码,需要⼀堆判断,而 UniRx 则是保持以往的简练⻛格提供了对⽹络请求的⽀持。代码如下:
void Start()
{
ObservableWWW.Get("http://sikiedu.com")
.Subscribe(responseText =>
{
Debug.Log(responseText);
}, e =>
{
Debug.LogError(e);
});
}
会在编辑器中,输出这个网页的html源码。
ObservableWWW 同样⽀持 WhenAll 操作符。如:
void Start()
{
var sikieduStream = ObservableWWW.Get("http://sikiedu.com");
var qframeworkStream = ObservableWWW.Get("http://qframework.io");
Observable.WhenAll(sikieduStream, qframeworkStream)
.Subscribe(responseTexts =>
{
Debug.Log(responseTexts[0].Substring(0, 100));
Debug.Log(responseTexts[1].Substring(0, 100));
});
}
除了 Get 也⽀持了 Post,还有 GetWWW 和 PostWWW 这种的辅助封装,还有 GetAndGetBytes 和PostAndGetBytes。这里列举一个文件下载的例子:
void Start()
{
var progressObservable = new ScheduledNotifier<float>();
//前面为文件地址,后面为进度指定了一个参数
ObservableWWW.GetAndGetBytes("http://liangxiegame.com/media/QFramework_v0.0.9.unitypackage", progress:progressObservable)
.Subscribe(bytes =>
{
//do something 在文件下载后
});
progressObservable.Subscribe(progress =>
{
Debug.LogFormat("进度为:{0}", progress);
});
}
在编辑器中会输出,下载的进度,从零开始到最后下载完成:
ObservableWWW 的 API 都可以传进去⼀个 ScheduledNotifier() ,⽤来监听下载进度。Subscribe 之后传回来的值则是,当前的进度。⽽且 ObservableWWW 的 Get 和 Post 请求都可以⾃⼰传对应的 header 和 WWWForm。除了常⽤的 Get 和 Post 请求,也对 AssetBundle 的加载也做了简单的封装。提供了诸如 ObservableWWW.LoadFromCacheOrDownload 这样的 API。如果想深⼊了解,可以参考 ObservableWWW.cs
ReactiveCommand
ReactiveCommand 的定义:
public interface IReactiveCommand<T> : IObservable<T>
{
IReadOnlyReactiveProperty<bool> CanExecute { get; }
bool Execute(T parameter);
}
它提供了两个 API:
• CanExecte
• Execute
Execute ⽅法是被外部调⽤的。也就是这个 Command 的执⾏。只要外部调⽤,Execute 就会执⾏。而 CanExecute 则是内部使⽤的,并且对外部提供了只读访问。
当 CanExecute 为 false 时,在外部调⽤ Execute 则该 Command 不会被执⾏。
当 CanExecute 为 true 时,在外部调⽤ Execute 则该 Command 会被执⾏。
它的 false 或 true ,有其他的其他的 Observable 来决定。
看下面这段代码:
void Start()
{
var reactiveCommand = new ReactiveCommand();
reactiveCommand.Subscribe(_ =>
{
Debug.Log("Execute");
});
reactiveCommand.Execute();
reactiveCommand.Execute();
reactiveCommand.Execute();
}
因为新创建的 ReactiveCommand 默认 CanExecute 为 true,所以执行三次,会输出三次Log。
只要调⽤ Execute。 Command 就会通知 Subscribe 的回调(因为 CanExecute 为 true)。CanExecute 的开启关闭是由 Observable (事件源)决定的。再来看下面这段代码:
void Start()
{
var mouseDownStream = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonDown(0)).Select(_ => true);
var mouseUpStream = Observable.EveryUpdate().Where(_ => Input.GetMouseButtonUp(0)).Select(_ => false);
var isMouseUp = Observable.Merge(mouseUpStream, mouseDownStream);
var reactiveCommand = new ReactiveCommand(isMouseUp, false);
reactiveCommand.Subscribe(_ =>
{
Debug.Log("reactive command executed;");
});
Observable.EveryUpdate().Subscribe(_ =>
{
reactiveCommand.Execute();
});
}
运行后的结果是,当按下⿏标左键时会持续输出 ,当抬起⿏标时,则停⽌输出。
当然 ReactiveCommand 也是可以被订阅(Subscribe) 的,在订阅之前呢,也可以使⽤ Where 等操作符进⾏事件操作。来看:
void Start()
{
var reactiveCommand = new ReactiveCommand<int>();
reactiveCommand.Where(x => x % 2 == 0).Subscribe(x => Debug.LogFormat("{0} 是 偶数", x));
reactiveCommand.Where(x => x % 2 != 0).Timestamp().Subscribe(x => Debug.LogFormat("{0} 是 奇数 {1}", x.Value, x.Timestamp));
reactiveCommand.Execute(10);
reactiveCommand.Execute(11);
}
结果如下:
ReactiveCollection
ReactiveCollection 类似于 List。
我们可以使⽤如下的操作符:
ObserverAdd // 当 新的 Item 添加则会触发
ObserverRemove // 删除
ObserverReplace // 替换(Update)
ObserverMove // 移动
ObserverCountChanged // 数量有改变(Add、 Remove)
来看一下代码:
ReactiveCollection<int> mAges = new ReactiveCollection<int> { 1, 2, 3, 4, 5 };
void Start()
{
mAges.ObserveAdd()
.Subscribe(addAge =>
{
Debug.LogFormat("add:{0}", addAge);
});
mAges.ObserveRemove()
.Subscribe(removedAge =>
{
Debug.LogFormat("remove:{0}", removedAge);
});
mAges.ObserveCountChanged()
.Subscribe(count =>
{
Debug.LogFormat("count:{0}", count);
});
foreach (var age in mAges)
{
Debug.Log(age);
}
mAges.Add(6);
mAges.Remove(2);
}
输出结果为:
可以看到,每一次的 添加、删除、数量改变,都会有输出。
ReactiveDictionary
ReactiveDictionary 功能与 Dictionary ⼀样。
同样地,它也⽀持了几个操作符:
ObserverAdd // 当 新的 Item 添加则会触发
ObserverRemove // 删除
ObserverReplace // 替换(Update)
ObserverMove // 移动
ObserverCountChanged // 数量有改变(Add、 Remove)
来看一下代码:
ReactiveDictionary<string, string> mLanguageCode = new ReactiveDictionary<string, string>
{
{"cn","中文"},
{"en","英文"}
};
void Start()
{
mLanguageCode.ObserveAdd().Subscribe(addedLanguage => Debug.LogFormat("add:{0}", addedLanguage));
mLanguageCode.ObserveRemove().Subscribe(removedLanguage => Debug.LogFormat("remove:{0}", removedLanguage));
mLanguageCode.ObserveCountChanged().Subscribe(count => Debug.LogFormat("count:{0}", count));
mLanguageCode.Add("jp", "日文");
mLanguageCode.Remove("en");
}
输出结果为:
每当涉及到对应操作,也都会有输出。
AsyncOperation 加载场景
我们在异步加载资源或者异步加载场景的时候往往会⽤到 AsyncOperation。UniRx 对 AsyncOperation 做了⽀持。使得加载操作可以很容易地监听加载进度。
void Start()
{
//Resources.LoadAsync<GameObject>("TestCanvas").AsAsyncOperationObservable()
//.Subscribe(resourceRequest =>
//{
// Instantiate(resourceRequest.asset);
//});
var progressObservable = new ScheduledNotifier<float>();
SceneManager.LoadSceneAsync(0).AsAsyncOperationObservable(progressObservable)
.Subscribe(_ =>
{
Debug.Log("load done");
});
progressObservable.Subscribe(progress =>
{
Debug.LogFormat("加载了:{0}", progress);
});
}
输出的结果就是: