好久没有分享博客了,果然燥热的夏天最容易使人懒惰(其实是自己懒)。最近学习了一些新的东西,.net Core、GRPC、响应式编程之类的,会在之后的博客分享中,将这些东西和Unity串起来,一起分享给大家。好了,废话不多说,进入本次分享的主题,Unity响应式编程框架UniRX,可在Unity Asset Store 中下载。
什么是响应式编程呢?响应式编程是一种面向数据流和变化传播的编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
首先看下面这样一个例子,通过点击按钮减少当前的血量值,实时更新当前的血量值并显示在屏幕上,当血量值低于0时,宣告死亡,并禁用攻击按钮。
使用传统命令式编程如下:
using UnityEngine;
using UnityEngine.UI;
public class TraditionCommand : MonoBehaviour
{
public Button button;
public Text Hptext;
public Text btnText;
Enemys enemy = new Enemys(1000);
void Start()
{
Hptext.text = "满血";
button.onClick.AddListener(() =>
{
enemy.CurrentHp -= 100;
Hptext.text = Hptext.text = "当前血量值为:" + enemy.CurrentHp.ToString();
if (enemy.CurrentHp <= 0)
{
Hptext.text = "血量为零,GameOver";
btnText.text = "敌人已死亡";
button.interactable = false;
}
});
}
}
public class Enemys
{
public int CurrentHp { get; set; }
public bool IsDead { get; set; }
public Enemys(int initHp)
{
CurrentHp = initHp;
IsDead = false;
}
}
用响应式编程的方式来改写代码如下:
using UnityEngine;
using UniRx;
using UnityEngine.UI;
public class Test : MonoBehaviour
{
public Button button;
public Text text;
public Text btnText;
Enemy enemy = new Enemy(1000);
void Start()
{
text.text = "满血";
button.OnClickAsObservable().Subscribe(_ =>
{
enemy.CurrentHp.Value -= 100;
text.text = "当前血量值为:" + enemy.CurrentHp.Value.ToString();
});
enemy.IsDead.Where(isDead => isDead == true)
.SubscribeToText(text, _ =>
{
button.interactable = false;
btnText.text = "敌人已死亡";
return "血量为零,GameOver";
});
}
}
public class Enemy
{
public ReactiveProperty<long> CurrentHp { get; set; }
public ReadOnlyReactiveProperty<bool> IsDead { get; set; }
public Enemy(long initHp)
{
CurrentHp = new ReactiveProperty<long>(initHp);
IsDead = CurrentHp.Select(x => x <= 0).ToReadOnlyReactiveProperty();
}
}
两种编程模式均实现了相同的功能;在这个例子中二者的代码量可能相差不多;但仔细研究下,你可能会觉得,响应式编程写出的代码更具有目的性一些。
在传统命令式编程中:
敌人血量的变化,以及血量值得更新显示,等逻辑的处理,均在Button的Click事件中处理。逻辑并不清晰。
在响应式编程中:
1.点击按钮,需要改变当前敌人的血量值,同时更新血量显示。
2.当血量值小于等于0时,需要宣告敌人死亡,并禁用攻击按钮。
这里存在两个主题,一个是攻击按钮,一个是敌人的血量;两者均作为可观察的对象Observable;当二者状态发生改变时,通知观察者Observer,观察者触发相应的订阅Subscribe,执行对应的事件。
使用传统命令式编程:
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class TraditionCommandDoubleClcik : MonoBehaviour
{
public float timeInterval = 0.5f;
public float lastClickTime = 0;
public Action DoubleClickAction;
void Start()
{
lastClickTime = Time.realtimeSinceStartup;
}
void Update()
{
if (Input.GetMouseButtonUp(0))
{
if (Time.realtimeSinceStartup - lastClickTime < timeInterval)
{
DoubleClickAction?.Invoke();
Debug.Log("双击");
}
lastClickTime = Time.realtimeSinceStartup;
}
}
}
使用响应式编程:
using System;
using UnityEngine;
using UniRx;
public class ReactiveDoubleClcik : MonoBehaviour
{
void Start()
{
var clickStream = Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0));
clickStream
.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
.Where(xs => xs.Count >= 2)
.Subscribe(xs =>
{
Debug.Log("双击");
});
}
}
在传统命令式编程中:
双击:鼠标在给定时间内按下两次或者以上的行为。当鼠标第一次抬起时,记录下鼠标第一次抬起时的时间;当鼠标第二次抬起时记录抬起时的时间;用第二次抬起时的时间减去第一次抬起时的时间,如果这个插值在我们给定的限制时间内,那么,就判定为双击。在这个过程中,我们需要分别记录鼠标在不同时刻抬起时的时间并作差值比较。
在响应式编程中
首先将游戏的循环(Update)作为一个事件流,鼠标点击的这个过程也作为一个流来处理。基于时间来合并处理两个流。分析一下这个代码:
var clickStream = Observable.EveryUpdate()
.Where(_ => Input.GetMouseButtonDown(0));
将鼠标按下事件作为一个流。
clickStream.Throttle(TimeSpan.FromMilliseconds(250))
节流阀,指定流的下一次(OnNext)触发在给定时间(TimeSpan.FromMilliseconds(250))到达之后。
clickStream
.Buffer(clickStream.Throttle(TimeSpan.FromMilliseconds(250)))
将给定流事件缓存在一个数组中,定期将观察到的事件缓存到数组中,根据当前数组中缓存的事件流的个数来判断是否满足双击的需求。如下图:
当然,Unity的响应式编程框架UniRX还包含许多有趣的功能,与传统的编程思维方式有些不同,个人认为UniRX这类框架特别适用于处理数据量变化比较频繁,且实时要求性较好的应用;从传统的编程思维中转变过来也需要一定的时间和练习。
官方地址:https://github.com/neuecc/UniRx
参看资料:
1.https://mcxiaoke.gitbooks.io/rxdocs/content/
2.http://reactivex.io/documentation/operators.html
阅读一遍官方例子,了解流式编程的方式;传统的面向对象方式(OOP)————一切皆对象。响应式编程————一切事件皆为流,对流进行组合、过滤、筛选、处理。
更多内容欢迎访问: