Unity3D应用嵌入WPF应用并实现通信之进阶篇

时间:2022-09-10 20:33:27

在之前的一篇博文中描述了Unity3D应用嵌入WPF应用的具体实现方式,但仅仅是解决了最基本的技术问题,想要将其具体的应用到项目中还需要具体的细化。经过近期在项目中的实践进行了细化,现将本人最近的一些过程整理成文,供大家讨论。上篇博文地址如下:

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在编译时建议进行以下配置:
Unity3D应用嵌入WPF应用并实现通信之进阶篇

勾选Run In Background和Visible in Background,否则会出现在WPF界面操作时Unity3D会最小化的情况。


以上就是本人关于Unity3D嵌入WPF应用的一些思考和实践,欢迎大家批评指正,不吝赐教!