在之前的一篇博文中描述了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在编译时建议进行以下配置:

WPF界面 如何通过Unity 容器配置文件注册 unity导入wpf_Data

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



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