|
1.什么是双向数据绑定
简单来说,就是多个单位对同一个数据实体存在实际的影响,举个非常简单的案例,就比如下面的颜色选择器,颜色数据是一份Vector4类型的数据,它只有一份,但是它可以被多个选择器控制,色相选择器可以控制,RGB通道数值也可以控制,而且每个控制器对这个数据的影响都会传播到其他的选择器。那么我们可以说,这些单位对同一个数据实体都存在实际的影响,(所谓影响就是读取或者写入)之间就形成了双向数据绑定。
2.为什么需要双向数据绑定
对于很多数据来说并不需要实现双向的数据绑定,比如对于玩家的金币数量,启动商店时可以读取金币的数量,交易完毕之后再写入数量。此时能够对金币数量产生影响的单位只有一个。
那么更多的,在游戏开发中,需要双向数据绑定的地方更多的是一些需要多个单位读取同一个对象、或者当某一个值需要被【观察】的时候。
所谓观察特指当某些单位对这个值的临界状态非常敏感,例如某些敌人AI会根据玩家的血量或其他的什么数值来调整自己的AI决策
比如玩家的血条,玩家的血条对于多个单位来说都是一个需要输出的值,血条需要被一些实体单位观察,也需要被输出到屏幕上,也会影响某些后期处理效果。那么肯定希望有个简单的方案,当玩家的血条改变的时候,其他所有的需要改变的单位都能同时改变自己的渲染参数或者逻辑参数。
那么对于我们来说,除了实现双向数据绑定的功能,这种相对更为抽象的系统应该是可以满足大部分的程度的需要的,所以要保持它的简单易用与低耦合。
3.实现双向数据绑定的原理
标题是简单的双向数据绑定,所以我们的原理会相对来说非常简单,这个原理来源于CS服务器架构,也就是由Server-Client构成的交互模式。服务器会提供部分API供客户端访问,Client可以通过向Server发送请求来实现对Server的控制。
如果我们要实现聊天的功能,简单来说就是ClientA给Server发送了一条消息,要求Server把这个消息广播给其他的Client,那么接受到ClientA的消息和ClientA的ID之后,Server会给所有不是ClientA的其他Client广播这条消息。
这种广播机制正好可以满足双向数据绑定的实现要求。
4.代码实战
由于数据接收端不确定到底是什么,也许是一个Unity的UI组件,或者是一个类或者一个状态机,所以显然,对于接收端来说,我们需要定义相关的接口,对任何需要观察一个数据或者调整数据的实体单位来说,只需要继承该接口,并实现即可。
public interface IDataReceiver<T>{
int receiverId{get;set;} // 接收器ID
}
同时,对于一个数据来说,它首先需要维护一个泛型数据,其次就是它必须维护一组IDataReceiver<T>,所以我们定义一个叫做TwoWayData<T>的类来表示这个数据
public class TwoWayData<T>{
private T data; // 所维护的数据
private List<IDataaReceiver<T>> receivers; //所有已经绑定的接收器
}
那么首先第一个要实现的功能就是TwoWayData<T>可以把当前的数据广播到其他所有的接收器,当然为了避免被广播的对象再次将数据传播回来,我们需要用ID来区分发起广播的接收器。
所以我们先给IDataReceiver<T>增加一个函数用于写入这个接收器的值,对于不同的接收器来说,如何利用这个值肯定是不一样的,所以也符合接口的设计。
public interface IDataReceiver<T>{
int receiverId{get;set;} // 接收器ID
void receive(T value); // 覆写这个接收器的数据
void unbind(); // 当解除绑定时,执行该函数
}
所以这样,TwoWayData<T>可以利用receive函数来给其他接收器广播当前的值。
public class TwoWayData<T>{
private T data; // 所维护的数据
private List<IDataaReceiver<T>> receivers; //所有已经绑定的接收器
public TwoWayData(T dft){
this.data = dft;
receivers = new List<IDataReceiver<T>>();
}
public void broadcast(int id, T value){
this.data = value;
foreach(IDataReceiver<T> receiver in receivers){
if(receiver.receiverId != id){
receiver.receive(value);
}
}
}
}
Ok,完成之后,这个架构就已经成形了,轻量级架构就是非常的好用。接着我们开始增加一些新的函数用于绑定和解绑接收器。
public class TwoWayData<T>{
private T data; // 所维护的数据
private List<IDataaReceiver<T>> receivers; // 所有已经绑定的接收器
private Queue<int> recycleIds; // 用于存放所有已经解绑的ID号
public TwoWayData(T dft){
this.data = dft;
receivers = new List<IDataReceiver<T>>();
}
public void broadcast(int id, T value){
/* 将当前的值广播给除id之外的其他接受器 */
this.data = value;
foreach(IDataReceiver<T> receiver in receivers){
if(receiver.receiverId != id){
receiver.receive(value);
}
}
}
public Action bind(IDataReceiver<T> receiver){
/* 给当前数据绑定一个新的接收器,返回解绑该receiver的委托 */
receiver.receiverId = nextId(); // 发放一个新的Id号
receivers.Add(receiver); // 增加一个接收器
receiver.receive(data); // 设定当前的值
return () => unbind(receiver); // 返回解绑委托
}
public void unbind(IDataReceiver<T> receiver){
/* 解绑给定的接收器 */
recycleIds.Enqueue(receiver.receiverId); // 回收接收器的ID
receiver.unbind();
receivers.Remove(receiver);
}
public void nextId(){
/* 当绑定一个新的接收器时,该函数用于计算一个新的ID号给目标接收器 */
return recycleIds.Count > 0 ? recycleIds.Dequeue() : receivers.Count;
}
}
Ok,大概就是这样,还有最后一个功能点,接收器不能单纯的接收数据,有的时候接收器也需要改写数据,于是我们还需要给IDataReceiver<T>增加一个write函数,用于覆写当前的数据
public interface IDataReceiver<T>{
int receiverId{get;set;} // 接收器ID
Action<int, T> write{get;set;} // 覆写数据
void receive(T value); // 覆写这个接收器的数据
void unbind(); // 当解除绑定时,执行该函数
}
那么同时,在绑定和解绑的时候,我们需要给write一个值。
public Action bind(IDataReceiver<T> receiver){
/* 给当前数据绑定一个新的接收器,返回解绑该receiver的委托 */
receiver.receiverId = nextId(); // 发放一个新的Id号
receiver.write = broadcast;
receivers.Add(receiver); // 增加一个接收器
receiver.receive(data); // 设定当前的值
return () => unbind(receiver); // 返回解绑委托
}
public void unbind(IDataReceiver<T> receiver){
/* 解绑给定的接收器 */
recycleIds.Enqueue(receiver.receiverId); // 回收接收器的ID
receiver.write = null;
receiver.unbind();
receivers.Remove(receiver);
}
以上就是全部的代码内容了,接着我们来增加两个实体单位测试一下。首先增加一个静态类,用于存放目标数据。
public static class TwoWayTest{
public static TwoWayData<float> myvalue = new TwoWayData<float>(0);
}
然后增加用两种不同的UI来作为接收器,一个是Text,一个是Slider。
public class TextReceiver : MonoBehaviour, IDataReceiver<float>{
[SerializeField] private string receiverName;
private Text output;
private void Start(){
output = GetComponent<Text>();
TwoWayTest.myvalue.bind(this);
}
// ------ 实现接收器 ------
public int receiverId{get;set;}
public Action<int, float> write{get;set;}
public void receive(float value){
output.text = $&#34;{receiverName}: {value.ToString()}&#34;;
}
public void unbind(){}
}
public class SliderReceiver : MonoBehaviour, IDataReceiver<float>{
private Slider slider;
private void Start(){
slider = GetComponent<Slider>();
slider.onValueChanged.AddListener((float v)=>write(receiverId, v));
TwoWayTest.myvalue.bind(this);
}
// ------ 实现接收器 ------
public int receiverId{get;set;}
public Action<int, float> write{get;set;}
public void receive(float value){
slider.value = value;
}
public void unbind(){
slider.onValueChanged.RemoveAllListeners();
}
}
注意,在Slider中,我们手动将接收器的write委托连接到了onValueChanged上,所以解绑的时候,需要清空所有的监听器,这也是为什么接收器需要定义unbind函数。
下面是最终的效果:
https://www.zhihu.com/video/1463941873034911744 |
本帖子中包含更多资源
您需要 登录 才可以下载或查看,没有账号?立即注册
×
|