在之前的一篇博文中描述了Unity3D应用嵌入WPF应用的具体实现方式,但仅仅是解决了最基本的技术问题,想要将其具体的应用到项目中还需要具体的细化。经过近期在项目中的实践进行了细化,现将本人最近的一些过程整理成文,供大家讨论。上篇博文地址如下:
问题&需求
为什么要将Unity3D应用嵌入WPF应用?
Unity3D是近些年比较流行的游戏引擎之一,在三维空间展现方面有着不错的效果,尤其是开源后有很多可用的资源,最重要的是其支持C#语言脚本开发,能够与传统.NET应用程序相融合。WPF和WinForm多用于桌面应用程序开发,处理业务逻辑有很大的优势,但用来做三维场景展示却是捉襟见肘(虽然WPF支持3D绘图,但从头开发工作量太大);另一方面Unity3D擅长三维场景展示、动画特效、场景交互,但处理业务逻辑有一定的局限性。
从实际项目上考虑,我们系统的业务逻辑处理已经比较完善,如果所用功能使用Unity3D从头开发一遍,学习、开发、测试都需要大量的时间,所以基于这种考虑,保留原有WPF系统的业务处理,将Unity3D的场景展示融合进来,发挥两者各自的优势。
Unity3D应用需要做什么?
作为三维图形处理引擎,我们希望Unity3D能做它最擅长的:动画、模型交互、场景渲染烘培。作为整个业务平台的一部分,我们希望Unity3D将业务系统中抽象的数据以直观的形式展示出来:空间定位、模型交互、空间移动。
方案&实现
解决方案
在上一篇博文中我们已经解决了Unity3D与WPF通信的功能,由于通信是双向的,这就意味者既可以让WPF告诉Unity3D去做什么,也可以让Unity3D告诉WPF去做什么。关键在于要提前定义好两者交互的标准,也就是WPF和Unity3D互相发送消息的“口令”。下面是在项目中整理的WPF和Unity3D的交互规则,便于Unity3D开发人员和WPF开发人员的协同开发:
Unity3D应具备的功能(与WPF无关)
- 视角平移、缩放、旋转
- 场景漫游(前进、后退、左转、右转)
- 指南针
- 空间测距
WPF应用向Unity3D应用发送消息
- 查找指定对象
- 指定对象高亮显示或执行动画
- 指定对象显示、隐藏或半透明
- 移动相机至指定位置
- 获取相机当前位置信息
Unity3D应用向WPF应用发送消息
- 当前被选中对象或对象组标识
- 当前相机位置信息
交互接口
针对WPF和Unity3D的交互需要,我们创建了一个公共类库WPF.UnityConnector来定义两者的通信规范。
Unity3D交互数据类
public class MessageData
{
private OperateCommand _operateType;
private string _operateTarget;
private object _operateData;
public MessageData()
{
}
public MessageData(OperateCommand type)
{
_operateType = type;
}
public MessageData(OperateCommand type,string target)
{
_operateType = type;
_operateTarget = target;
}
public MessageData(OperateCommand type,string target,object data)
{
_operateType = type;
_operateTarget = target;
_operateData = data;
}
/// <summary>
/// 操作类型
/// </summary>
public OperateCommand OperateType
{
get { return _operateType; }
set { _operateType = value; }
}
/// <summary>
/// 操作对象
/// </summary>
public string OperateTarget
{
get { return _operateTarget; }
set { _operateTarget = value; }
}
/// <summary>
/// 数据主体
/// </summary>
public object OperateData
{
get { return _operateData; }
set { _operateData = value; }
}
public override string ToString()
{
return base.ToString();
}
}
操作类型枚举类
[JsonConverter(typeof(StringEnumConverter))]
public enum OperateCommand
{
/// <summary>
/// 空
/// </summary>
None,
/// <summary>
/// 定位
/// </summary>
SetPosition,
/// <summary>
/// 获取位置
/// </summary>
GetPosition,
/// <summary>
/// 选择单个对象
/// </summary>
SelectSignle,
/// <summary>
/// 选择对象组
/// </summary>
SelectGroup,
/// <summary>
/// 设置显示
/// </summary>
SetVisible,
/// <summary>
/// 设置隐藏
/// </summary>
SetHidden,
/// <summary>
/// 设置透明
/// </summary>
SetTransparent,
/// <summary>
/// 设置高亮
/// </summary>
SetHighLight,
/// <summary>
/// 设置动画
/// </summary>
SetAnimation,
/// <summary>
/// 无坐标数据通过算法定位
/// </summary>
SetPositionNoData,
/// <summary>
/// 设置帮助说明是否可见
/// </summary>
SetHelpTextVisible,
/// <summary>
/// 返回默认全局视点
/// </summary>
ReturnBack,
/// <summary>
/// 漫游
/// </summary>
SetFreeWalk,
/// <summary>
/// 自动旋转
/// </summary>
SetAutoRotate
}
代码示例
举个例子,假设我们有一个设施管理系统,在WPF业务界面是一个房间内所有家具的列表,在Unity3D展示界面是这个房间的三维模型,想要实现的效果是点击WPF列表中的某一个家具,Unity3D模型中的对应家具模型高亮显示并将相机推近,或者是点击Unity3D模型中的某个家具,WPF界面弹出其详细信息。
WPF列表点击事件:
private void Btn_Click(object sender, RoutedEventArgs e)
{
Button Btn = sender as Button;
DataRow dr = Btn.Tag as DataRow;
Helpers.BIMOperator.SetPosition(dr["RowGuid"].ToString());
//自己的业务代码//
}
由于最终效果实际上包含两部分:相机推近和物体高亮,所以需要发送两个命令。
命令拼接:
public static void SetPosition(string guid)
{
List<MessageData> datas = new List<MessageData>();
string ViewPoint = "10_10_10_10_120_12";//由数据库查询所得
//向Unity发送消息,定位设备
MessageData serverMsg = new MessageData();
serverMsg.OperateType = OperateCommand.SetPosition;//定位
serverMsg.OperateTarget = guid;
serverMsg.OperateData = ViewPoint;//数据格式"0_1_-5_20_0_0"
datas.Add(serverMsg);
//高亮显示
MessageData msg = new MessageData(OperateCommand.SetVisible, "", guid);
datas.Add(msg);
SendMsgToUnity(datas);
}
WPF发送消息:
public SocketHelper _connector;
private static void SendMsgToUnity(List<MessageData> msgdata)
{
string sendmsg = JsonConvert.SerializeObject(msgdata);
_connector.SendMessage(sendmsg);
}
Unity3D接收消息
private void ReceiveMessage(IAsyncResult ar)
{
try
{
_error = "";
int bytesRead;
bytesRead = _client.GetStream().EndRead(ar);
if (bytesRead < 1)
{
return;
}
else
{
string message = System.Text.Encoding.ASCII.GetString(_data, 0, bytesRead);
ReceiveMsgDo(message);
_error = string.Format("{0}:{1}", DateTime.Now.ToString(), message);
//this._client.GetStream().BeginRead(_data, 0, System.Convert.ToInt32(this._client.ReceiveBufferSize), ReceiveMessage, null);
}
}
catch (Exception ex)
{
_error = ex.Message;
LogicMgr.Instance.GetLogic<LogicTips>().ShowTips(ex.Message);
}
finally
{
this._client.GetStream().BeginRead(_data, 0, System.Convert.ToInt32(this._client.ReceiveBufferSize), ReceiveMessage, null);
}
}
/// <summary>
/// 接收Unity消息进行操作
/// </summary>
/// <param name="msg"></param>
public void ReceiveMsgDo(string msg)
{
List<MessageData> datas = JsonConvert.DeserializeObject<List<MessageData>>(msg);
foreach (MessageData data in datas)
{
GameMassageReciver.Instance.SetCurrentOperateCommand(data.OperateType, data.OperateData.ToString(), data.OperateTarget);
}
}
Unity3D命令转换
public class GameMassageReciver : SingletonMonoBase<GameMassageReciver>
{
private OperateCommand currentOperateCommand;
private string currentMessage;
private string currentTarget;
// Use this for initialization
void Start () {
}
// Update is called once per frame
void Update () {
ReciveMessageDo();
}
private void ReciveMessageDo()
{
switch (currentOperateCommand)
{
case OperateCommand.None://空
break;
case OperateCommand.SetPosition://定位摄像机
MessageSetPosition(currentMessage);
break;
case OperateCommand.SetVisible://设置显示
MessageSetVisible(currentMessage);
break;
case OperateCommand.SetHidden://设置隐藏
MessageSetHidden(currentMessage);
break;
case OperateCommand.SetTransparent://设置透明
MessageSetTransparent(currentMessage);
break;
case OperateCommand.SetHighLight:
MessageSetHighlight(currentTarget);
break;
case OperateCommand.SetFreeWalk://漫游
MessageSetFreeWalk();
break;
default:
break;
}
SetCurrentOperateCommand(OperateCommand.None,null,null);
}
public void SetCurrentOperateCommand(OperateCommand current,string msg,string target)
{
this.currentMessage = msg;
this.currentOperateCommand = current;
this.currentTarget = target;
}
/// <summary>
/// WPF请求Unity设置摄像机位置
/// </summary>
private void MessageSetPosition(string data)
{
if (data != null)
{
string[] _arr = data.Split('_');
if (_arr.Length == 6)
{
CameraPointData _data = new CameraPointData();
_data.mPosition = new Vector3(GetFloat(_arr[0]), GetFloat(_arr[1]), GetFloat(_arr[2]));
_data.mRotation = new Vector3(GetFloat(_arr[3]), GetFloat(_arr[4]), GetFloat(_arr[5]));
CameraController.Instance.SetPosition(_data);
}
else
{
Debug.LogWarning("数据转化失败!");
}
}
else
{
Debug.LogWarning("数据为空!");
}
}
private float GetFloat(string str)
{
return float.Parse(str);
}
/// <summary>
/// WPF请求Unity显示某物体
/// </summary>
/// <param name="id"></param>
public void MessageSetVisible(string id)
{
if (id != null)
{
GameObject _go = ModelMgr.Instance.GetModelById(id);
if (_go != null)
{
ModelMgr.Instance.SetObjVisible(_go);
HighLightManager.Instance.HideCurrentEffect();
HighLightManager.Instance.SwitchFlashHighLight(_go);
}
else
{
Log.Warning("场景中不存在该物体或ID错误!" + id);
}
}
else
{
Debug.LogWarning("数据为空!");
}
}
/// <summary>
/// WPF请求Unity高亮显示物体
/// </summary>
/// <param name="id"></param>
public void MessageSetHighlight(string id)
{
if (id != null)
{
LogicMgr.Instance.GetLogic<LogicTips>().ShowTips("id:" + id);
GameObject _go = ModelMgr.Instance.GetModelById(id);
if (_go != null)
{
ModelMgr.Instance.SetObjVisible(_go);
HighLightManager.Instance.SwitchFlashHighLight(_go);
}
else
{
Log.Warning("场景中不存在该物体或ID错误!" + id);
}
}
else
{
Debug.LogWarning("数据为空!");
}
}
/// <summary>
/// WPF请求Unity隐藏某物体
/// </summary>
/// <param name="id"></param>
public void MessageSetHidden(string id)
{
if (id != null)
{
LogicMgr.Instance.GetLogic<LogicTips>().ShowTips("id:" + id);
GameObject _go = ModelMgr.Instance.GetModelById(id);
if (_go != null)
{
ModelMgr.Instance.SetObjHidden(_go);
}
else
{
Log.Warning("场景中不存在该物体或ID错误!" + id);
Log.Warning("场景中不存在该物体或ID错误!18cc8045-3d61-4082-ba0b-d7ddd7fb6f70");
}
}
else
{
Debug.LogWarning("数据为空!");
}
}
}
Unity3D相机位移:
[HideInInspector]
public Vector3 m_target;//目标
private float m_targetDistance = 2f;
private float m_currentDistance;
private Quaternion m_currentRotation;
public void SetPosition(CameraPointData data)
{
isSettingPosition = false;
StopCoroutine("OnSetCameraPosition01");
m_target = data.mPosition;
m_targetDistance = 0;
CaculatePosition(data.mRotation.y, data.mRotation.x, m_targetDistance);
StartCoroutine("OnSetCameraPosition01");
}
void CaculatePosition(float eulerY, float eulerX, float dis = 0)
{
Log.Debug("eulerX:" + eulerX + "eulerY:" + eulerY);
m_Anglex = eulerY;
m_Angley = eulerX;
m_distance = dis;
m_desiredRotation = Quaternion.Euler(m_Angley, m_Anglex, 0);
m_desiredPosition = m_target + m_desiredRotation * new Vector3(0, 0, -m_distance);
}
Unity3D对象高亮:
//当前所有的高亮物体
private List<HighlightableObject> m_highLightObjs = null;
private HighlightableObject m_currentObj = null;
public void SwitchFlashHighLight(GameObject obj)
{
FlashHighLightGameObject(obj, true);
}
private void FlashHighLightGameObject(GameObject obj, bool isSwitch=false,bool isShow=true)
{
if (obj==null)
{
return;
}
//获取高亮物体控制脚本
HighlightableObject _highLightObj;
_highLightObj = obj.GetComponent<HighlightableObject>();
if (_highLightObj==null)
{
_highLightObj = obj.AddComponent<HighlightableObject>();
}
m_currentObj = _highLightObj;
//加入列表,便于以后去除所有高亮效果
if (!m_highLightObjs.Contains(_highLightObj))
{
m_highLightObjs.Add(_highLightObj);
}
//设置高亮颜色
_highLightObj.FlashingParams(Color.white, Color.red, 1f);
if (isSwitch)
{
_highLightObj.FlashingSwitch();
return;
}
if (isShow)
{
_highLightObj.FlashingOn();
}
else
{
_highLightObj.FlashingOff();
}
}
Unity3D发送消息由WPF接收并弹出界面的过程参考上述过程类比实现。
其他
为了确保Unity3D和WPF能够顺利交互,Unity3D在编译时建议进行以下配置:
勾选Run In Background和Visible in Background,否则会出现在WPF界面操作时Unity3D会最小化的情况。
以上就是本人关于Unity3D嵌入WPF应用的一些思考和实践,欢迎大家批评指正,不吝赐教!