上次我们介绍了几种构建流的方式。这节,我想把重点放在更具实用性的转换Unity的Updaet方法上。在本节中,我们将介绍如何使用UniRx将Unity中的Update转化为UniRx中的Observable.
有两种方法可以将Unity Update方法转化为流:
上述两个方法在使用和操作上是相同的,但是其内部实现有很大的不同,下面我解释一下他们各自的用法和工作原理
1.using UniRx.Triggers;
2.this.UpdateAsObservable()
using UnityEngine;
using UniRx;
using UniRx.Triggers;
public class TestUniRX : MonoBehaviour
{
void Start()
{
this.UpdateAsObservable()
.Subscribe(_ =>
{
Debug.Log("Update");
});
}
}
以上方法可以将Update事件转换成UniRx的流来使用。当流的GameObject 被销毁时会自动触发OnCompleted,此时,流的生命周期管理变得更加容易。
using UniRx;
using UniRx.Triggers;
public class TestUniRX : MonoBehaviour
{
void Start()
{
this.UpdateAsObservable()
.Subscribe(
onNext: _ =>
{
Debug.Log("Update");
},
onCompleted: () =>
{
Debug.Log("OnCompleted");
}
);
this.OnDestroyAsObservable()
.Subscribe(_ =>
{
Debug.Log("OnDestroy");
});
Destroy(gameObject, 2.0f);
}
}
UpdateAsObservable流是ObservableUpdateTrigger组件中具有实体的流。在调用UpdateAsObservable时,UniRx会自动触发相关GameObject的ObservableUpdateTrigger组件,利用这个组件发出相应的事件。
/// Update is called every frame, if the MonoBehaviour is enabled.
public static IObservable<Unit> UpdateAsObservable(this Component component)
{
if (component == null || component.gameObject == null) return Observable.Empty<Unit>();
return GetOrAddComponent<ObservableUpdateTrigger>(component.gameObject).UpdateAsObservable();
}
using System; // require keep for Windows Universal App
using UnityEngine;
namespace UniRx.Triggers
{
[DisallowMultipleComponent]
public class ObservableUpdateTrigger : ObservableTriggerBase
{
Subject<Unit> update;
/// Update is called every frame, if the MonoBehaviour is enabled.
void Update()
{
if (update != null) update.OnNext(Unit.Default);
}
/// Update is called every frame, if the MonoBehaviour is enabled.
public IObservable<Unit> UpdateAsObservable()
{
return update ?? (update = new Subject<Unit>());
}
protected override void RaiseOnCompletedOnDestroy()
{
if (update != null)
{
update.OnCompleted();
}
}
}
}
当调用UpdateAsObservable时,UniRx将添加一个ObservableUpdateTrigger组件到调用的GameObject对象上,并在执行ObserverUpdateTrigger中的Update时,将Update的行为作为UniRx的事件发布。注意一下两点:
1.Observable.EveryUpdate()
Subscribe(所经过的帧数)
using UnityEngine;
using UniRx;
public class UpdateSample : MonoBehaviour
{
void Start()
{
Observable.EveryUpdate()
.Subscribe(
_ => Debug.Log("Update!")
);
}
}
其使用方式和上面说过的UpdateAsObservable基本一样。然而,需要注意一点的是,Observable.EveryUpdate()并不会自动发布OnComplete.他不像UniRx。Triggers中的 this.UpdateAsObservable() 那样,生命周期和GameObject 的生命周期有关联。所以,当你使用Observable.EvenryUpdate时你需要自己进行流体的生命周期管理。
未手动释放流,就算当前GameObject被销毁,流依然执行:
using UnityEngine;
using UniRx;
public class TestUniRX : MonoBehaviour
{
void Start()
{
Observable.EveryUpdate()
.Subscribe(frameCount =>
{
Debug.Log(frameCount);
});
Destroy(gameObject, 2.0f);
}
}
在执行过程中,将流与当前GameObject生命周期关联,当前对象被释放时,流也被释放:
using UnityEngine;
using UniRx;
public class TestUniRX : MonoBehaviour
{
void Start()
{
Observable.EveryUpdate()
.Subscribe(frameCount =>
{
Debug.Log(frameCount);
}).AddTo(gameObject);
Destroy(gameObject, 2.0f);
}
}
Observable.EveryUpdate是利用UniRx提供的功能之一的微协程来运行的,其工作原理相对复杂一些。简单来说,就是当每次执行Observable.EveryUpdate时。他就会启动一个协程,除非你手动去终止这个协程,否则,协程会一直执行下去;所以说,管理好刘德生命周期很重要。然而,另外一方面,使用Observable.EveryUpdate有以下好处:
另外MainThreadDispatcher是由一个单例创建的的GameObject.在使用UniRx的过程中,你可能会发现,有些东西是由UniRx生成的,这些东西对于UniRx来说也是必要的,所以不要随意删除他们。
虽然二者的操作相似,但是内部实现有很大的不同,你应该清楚的了解每一个操作的工作原理,并根据你自己的实际情况来选择使用合适的方法。
一些适合使用UpdateAsObservable的地方:
一些适合使用Observable.EveryUpdate的地方:
因为使用了UniRx中的微协程,当你需要使用大量Update时调用时,它的性能表现比原生Unity提供的Update调用要好的多。
当然,选择使用哪种方式,全凭你自己的喜好。有人说,Observable.EvenryUpdate的表现更为出色,但是会经常忘记去手动释放它。假如你出了一个bug,流回停止,这是很好的;可怕的是,出现了一个bug,流并没有停止,而是继续向后传递,这是非常让人苦恼的。因此,不管性能上有多大差异,我个人还是比较建议使用UpdateAsObservable.
在我看来,最因该使用UniRx的一个理由就是可以流化Update,将Update流化之后,你就可以:
UniRx中提供了大量与时间相关的操作符,所以,只需要使用UniRx提供的操作符,就可以很简洁的描述与时间相关的游戏逻辑。举个例子,一个射击游戏,当你点击发射按钮时,你可以在一定时间间隔内执行攻击动作。比如,射击游戏中的子弹发射,你按下了一个按钮,每隔n秒发射一次子弹。我们使用Unity的操作方式来实现的话,相对会比较繁琐,需要记录一些具体的时间点。但是,如果使用UniRx,实现如下:
using UniRx;
using UniRx.Triggers;
public class TestUniRX : MonoBehaviour
{
private float interValSeconds = 0.25f;
void Start()
{
this.UpdateAsObservable()
.Where(_ => Input.GetKey(KeyCode.LeftControl))
.ThrottleFirst(TimeSpan.FromSeconds(interValSeconds))
.Subscribe(_ => Attack());
}
private void Attack()
{
Debug.Log("Attack");
}
}
使用UniRx就可以向使用LINQ一样简洁的处理游戏逻辑。在大多数情况下,我们会在Update中塞入大量的判断逻辑,使得Update的逻辑变得杂乱无章,使用UniRx来处理,就可以避免这样的情况。
下方代码尝试描述一个常见的控制角色移动、跳跃、攻击的逻辑;在不适用UniRx的情况下,代码如下:
using UnityEngine;
public class TestUniRX : MonoBehaviour
{
private CharacterController characterController;
private bool isJumping;
void Start()
{
characterController = GetComponent<CharacterController>();
}
void Update()
{
if (!isJumping)
{
var inputVector = new Vector3(
Input.GetAxis("Horizontal"),
0,
Input.GetAxis("Vertical")
);
if (inputVector.magnitude > 0.1f)
{
var dir = inputVector.normalized;
Move(dir);
}
if (Input.GetKeyDown(KeyCode.Space) && characterController.isGrounded)
{
Jump();
isJumping = true;
}
}
else
{
if (characterController.isGrounded)
{
isJumping = false;
PlaySoundEffect();
}
}
}
private void PlaySoundEffect()
{
Debug.Log("播放音效");
}
private void Jump()
{
Debug.Log("跳跃");
}
private void Move(Vector3 dir)
{
Debug.Log("Move");
}
}
使用UniRx实现上面描述的逻辑功能:
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,在Update中将会充斥着大量的判断逻辑,大量if语句的出现,会扰乱变量范围。但是,如果我们换成UniRx流来流化Update,我们就可以将处理划分逻辑单元来并排描述,并且变量的作用范围也被封闭在了流中。通过将Update流化,这样我们就可以用合适的单元来分段描述处理过程,同时也可以清楚的看到变量的作用范围。
void Start()
{
var characterController = GetComponent<CharacterController>();
characterController
.ObserveEveryValueChanged(x => x.isGrounded)
.Where(x => x)
.Subscribe(_ => Debug.Log("在地面"))
.AddTo(gameObject);
Observable.EveryUpdate()
.Select(_ => characterController.isGrounded)
.DistinctUntilChanged()
.Where(x => x)
.Subscribe(_ => Debug.Log("着地"))
.AddTo(gameObject);
}
上回描述了,ObserveEveryValueChanged是相当于… . . . . . (Observable) . . (EveryUpdate + Select + DistinctUntilChanged)的省略记法,事实上,这个解释不太正确。
ObserveEveryValueChanged 持有待监视对象的弱引用。换句话说,在ObserveEveryValueChanged的监视不会被计入GC的引用计数。另外,当被监视的对象被GC回收时,ObserveEveryValueChanged会自动发布OnCompleted.
更多内容欢迎访问: