在前面的文章中写过如何搭建SignalR服务端和客户端,也写过如何将Unity3D应用嵌入到WPF中,问题是SignalR服务端和WPF客户端实时通信很简单,SignalR服务端怎么与Unity3D应用实时传输数据呢?下面就开始讨论了。
1.问题背景
1.1为什么要在Unity3D中实时传输数据?
在多人游戏中要实时显示对手的数据,如位置、血量等。在3D业务系统中可能要显示外部提供的一些数据,而这些数据是随时都有可能发生变化的,需要实时在Unity3D应用中显示出来,比如外部采集的温度、湿度等。
1.2为什么要使用SignalR进行实时通信?
选用SignalR进行实时通信并不意味着在Unity3D中进行实时通信只有SignalR这一种方案,只是在之前的文章中使用融合SignalR的WebAPI做服务端已经很顺手了,业务系统也相对成熟,所以打算继续沿用下去。目前常见的方案一般是用Socket做通信,但是需要自己做底层的封装,对于我这种技术菜鸟而言搞不定,所以最终还是希望能够使用SignalR做实时数据传输。
WebAPI集成SignalR
1.3如何将SignalR应用到Unity3D中?
最开始我的想法是,反正都是C#的类库,直接添加进去引用就好了。但是Unity3D编辑器是基于Mono解决方案的,而Mono是基于.Net Framework3.5的,SignalR客户端对运行环境的要求是至少是.Net Framework4.5的,这也就意味着是无法直接在Unity3D中使用SignalR的。问题似乎陷入了僵局,但是天无绝人之类,我们发现在Unity应用商店有一个神奇的插件:BestHttp,对比较常用的网络通信方式进行了封装,包括Http、WebSocket、Socket和我们很熟悉的SignalR。BestHttp比Unity3D自带的www功能丰富很多,但是请注意,BestHttp是收费的,BestHttp是收费的,BestHttp是收费的,重要事情说三遍。
2.解决方案
通过上面的分析,我们决定采用融合SignalR的WebAPI作为客户端,Unity3D融合BestHttp作为客户端,采用之前开发的OPCClient作为采集端,搭建一个实时监测温湿度等环境参数的例子。
融合SignalR的OPCClient实现环境参数实时监测
2.1安装BestHttp
打开Unity3D编辑器,新建项目MonitorDemo,选择菜单Window>Asset Store,打开应用商店查找BestHttp,导入到项目中。
导入完成后可以看到Assets目录下面多了一个BestHttp的文件夹,可以在文件夹下找到BestHttp的说明文档,本文中的例子都是基于这份说明文档进行开发的。
2.2搭建Unity3D界面
使用Unity3D的UI添加一个Canvas,添加两个Text,TextTempKey显示“温度”,TextTempValue显示温度数据。
为了看的清楚些,加了一个图片做背景。
将Canvas的Render Mode改为Screen Space-Camera,并将Render Camera改为MainCamera,使得Canvas始终以UI界面形式呈现。
2.3创建C#脚本
新建脚本SignalrHelper
public class SignalrHelper : MonoBehaviour
{
public Text txt;
private Connection _connection;
private Hub _proxy;
readonly Uri _uri=new Uri("http://localhost:49749/MessageBus");
// Use this for initialization
void Start () {
//实例化代理Hub
_proxy=new Hub("MessageHub");
//实例化连接
_connection=new Connection(_uri,_proxy);
//接收数据事件注册
_proxy.On("receiveData",ReceiveData);
//启动连接
_connection.Open();
}
private void ReceiveData(Hub hub, MethodCallMessage methodCall)
{
string arg0 = methodCall.Arguments[0].ToString();
ItemValueModel item = Newtonsoft.Json.JsonConvert.DeserializeObject<ItemValueModel>(arg0);
Debug.Log("I received data:"+arg0);
txt.text=item.value;
}
// Update is called once per frame
void Update () {
}
}
void OnDestory()
{
if (_connection!=null)
{
_connection.Close();
}
}
}
其中ItemValueModel是我们定义的监测项数据类
/// <summary>
/// 监测标签数据类
/// </summary>
public class ItemValueModel
{
/// <summary>
/// 标签唯一标识
/// </summary>
public string id { get; set; }
/// <summary>
/// 标签名称
/// </summary>
public string name { get; set; }
/// <summary>
/// 标签数据值
/// </summary>
public string value { get; set; }
}
将脚本SignalrHelper作为组件添加到Canvas上,并将所定义的Canvas的txt属性设为TextTemp_Value,使得Canvas初始化的时候就能与服务端建立连接,并监听服务端方法receiveData,当接收到数据时将TextTemp_Value的文本改为测点值。
2.4添加服务端方法
public class MessageHub:Hub
{
public async Task ReceiveData(string data)
{
ItemValueModel item = JsonConvert.DeserializeObject<ItemValueModel>(data);
await Clients.All.receiveData(item);
}
}
完成以上工作后,启动服务端,然后依次启动采集端(OPCClient)和客户端(Unity3D),看到跳动的数字就说明成功了。
3.方案改进
在上面的步骤已经实现了一个测点的实时数据监测,但显然有点简单了,因为实际上我们在业务系统中不可能只有一个测点,而且并不仅仅只是读取数据,还会有写入数据的情况。另外在上面的示例运行的时候发现测点数据变化频率很快的时候,界面会变得卡顿,于是针对这些问题进行改进。
3.1添加数据处理队列
private static Queue<ItemValueModel> _queue;
private void ReceiveData(Hub hub, MethodCallMessage methodCall)
{
string arg0 = Newtonsoft.Json.JsonConvert.SerializeObject(methodCall.Arguments[0]);
ItemValueModel item = Newtonsoft.Json.JsonConvert.DeserializeObject<ItemValueModel>(arg0);
_queue.Enqueue(item);
}
接收到测点数据变化时,并不立即处理,而是将数据添加到队列_quene中,在Update方法中去处理数据,这样处理数据就和帧刷新保持一致,避免出现卡顿的现象。
void Update () {
if (_queue.Any())
{
ItemValueModel item = _queue.Dequeue();
string tmp = item.id.ToUpper();
GameObject go = GameObject.FindWithTag(tmp);
if (go != null)
{
Debug.Log(go.name);
var target = go.GetComponent<Text>();
if (target != null)
{
target.text = item.value;
Debug.Log(target.name + ":" + item.value);
}
}
}
}
3.2添加数据写入方法
在脚本SignalrHelper中添加写入数据的方法,并通过BestHttp提供的Call方法发送给服务端。
public Button button;
public Text text_ID;
public Text text_Value;
void Start () {
//数据处理队列
_queue=new Queue<ItemValueModel>();
//按钮事件
button.onClick.AddListener(delegate()
{ WriteItem(text_ID.text,text_Value.text);
});
//实例化代理Hub
_proxy=new Hub("MessageHub");
//实例化连接
_connection=new Connection(_uri,_proxy);
//接收数据事件注册
_proxy.On("receiveData",ReceiveData);
//启动连接
_connection.Open();
}
private void WriteItem(string id,string value)
{
if (_connection!=null&&_proxy!=null)
{
_proxy.Call("WriteItem", id, value);
}
}
3.3添加界面控件
添加两个Input分别用来输入要写入的标签名称和值,添加一个Button用来发送写入命令,再多加几个Text来表示不同的标签。
通过以上的改造我们就能在Unity3D中实现多个测点实时数据的监测和测点数据写入了,也就意味着如果是一个设备监控系统的话,我们就能在Uinity3D里进行设备状态的监测和控制了。
在上面的步骤中,我们使用BestHttp在Unity3D里与SignalR服务端建立连接,实现了实时通信,但仍有部分问题需要解决:
如何将将SignalrHelper作为实时通信的总入口,实现数据传输的模块化?
如何实现测点数据与UI控件的动态绑定?
这些问题也许会在后面的实践中得到解决。