您的位置:首页 > 移动开发 > Unity3D

unity启动高级.net功能模块

2017-04-16 17:37 369 查看
重要说明:

首先,目前的unity3d只支持.net3.5的功能,最新的测试版貌似可以使用.net的高级功能,但也还不能发布使用。为此这里有一个可以实现类似功能的方案--利用unity3d,进程启动另一个微软的中间软件,在unity3d中向其传入参数,并利用反射将开发的dll加载到其中,最后将得到的结果返回到unity3d。这样的过程有点复杂,但我想如果封装的好,应该还是比较容易使用的。在说一个开发这个例子的背景,前些日子,利用windows自带的api可以实现打开窗口选择文件,但由于使用参数复杂,封装了两个有简单参数的类,但目前发现不稳定,会出现崩溃的意外,而且还不能正常显示一些基本信息。为此伤透了脑筋。如果是微软自己的软件来调用这些接口应该不会出现这些问题,更何况winform和wpf已经把这样常用的功能封装好了,只需要调用就好了。无奈我的大unity3d还需要购插件才能使用类似的选择文件的功能。其次,由于实际开发过程中需要一些联合unity和wpf或winform的功能,如果将winform做成3.5到是可以放置到unity中使用,但这样做的情况是丑到爆的界面。如果将使用wpf封装一些界面模块的话,应该就是不能使用了,这时如果用wpf开发一个启动器专门用来加载其相关的dll,然后将最后的结果传回到unity就好了。当然本文只使用了winform开发了一个启动器,还有大量的工作需要去完善和实现,最跟本的还是要得项目能用上才有时间继续开发了。

下面将介绍这个模块的使用方法,以打开文件和通信为例子,但并不是说这个模块只有这点功能,如果需要可以自行扩展。最后会后讲一下程序中的重点和难点,然后会附上github源码地址,如果有兴趣自己去研究一下,如果能扩展和提出宝贵的意见更好。

一、最终功能简单实现:

1.可以使用所有的window自带的dialog(文件选择,颜色选择)



(图一.颜色和路径选择后)



(图二、打开文件选择框)

2.可实时通信交互数据



(图三、利用外部面板来操作unity3d对象)

二、以打开文件夹选择为例的使用方法说明:

1.在unity3d的中定义中间程序的相对路径位置

程序中有一些需要使用协程进行等待的功能,同时在运行和结束时自动注册和注销一些信息,所以将这部分功能写到了一个继承于Monobehaiver的脚本中,但使用时,用了一个静态的脚本,便于调用,初始化路径相关的功能都放置到这个静态脚本中,而有关于通信模块的调用的事件的分发调用都放置到了这个组件脚本中。

下面是程序路径配制方法,在FormLoaderUser脚本中

public static void InitEnviroment()
{
GameObject go = new GameObject(typeof(FormLoaderBehavier).ToString());
behaiver = go.AddComponent<FormLoaderBehavier>();
string path = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Demo")) + "FormLoader/FormLoader/bin/Debug/FormLoader.exe";
Behaiver.TryOpenHelpExe(path);
}

2.编写第三方dll

由于中间程序只负责启动的功能,主要的逻辑将在具体的dll中实现,下面是我写的对话框dll中的一个选择文件对话框的方法。要注意的一点是,直接传字符串过来始终会出现乱码,暂时还没有解决这个问题,直接传byte[]会出现新的问题,所以这部分暂时用这个方法来进行中文字符串的处理。
private string ConverByte(object titlebytes)
{
JSONArray array = JSONArray.Parse(titlebytes.ToString()).AsArray;
byte[] byts = new byte[array.Count];
for (int i = 0; i < byts.Length; i++)
{
byts[i] = (byte)array[i].AsInt;
}
string title = System.Text.Encoding.UTF8.GetString(byts);
return title;
}
public string OpenFileDialog(object titlebytes, string filter, string initialDirectory)
{
string title = ConverByte(titlebytes);

string resultFile = "";
try
{
OpenFileDialog openFileDialog1 = new OpenFileDialog();
openFileDialog1.InitialDirectory = initialDirectory;
openFileDialog1.Filter = filter;// "txt files (*.txt)|*.txt|All files (*.*)|*.*" ;
openFileDialog1.FilterIndex = 2;
openFileDialog1.Title = title;
openFileDialog1.RestoreDirectory = true;
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{

// Insert code to read the stream here.
resultFile = openFileDialog1.FileName;
}
}
catch (Exception ex)
{
MessageBox.Show("Error: Could not read file from disk. Original error: " + ex.Message);
}

return resultFile;

}
3.在unity通过中间程序调用以上的dll
由于中间程序可能会出现问题,首先可以进行不通过中间程序的方法,确保dll和unity中的方法都能正常运行,Test方法调用后在编辑器模式下可以直接打开.net 2.0的winform
界面 ,如图四所示:
public static void OpenFileDialog(string title, FileType fileType, string initialDirectory, Action<string> onReceive)
{
List<byte> byts = new List<byte>(Encoding.UTF8.GetBytes(title));
string path = Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Demo")) + "Demo/FileDialogHelp/FileDialogHelp/bin/Debug/FileDialogHelp.dll";
string clsname = "FileDialogHelp.FileDialog";
string methodname = "OpenFileDialog";
string filter = "";
switch (fileType)
{
case FileType.Txt:
filter = "txt files (*.txt)|*.txt|All files (*.*)|*.*";
break;
default:
break;
}
object[] aregument = new object[] { byts, filter, initialDirectory };
DllFuction pro = new DllFuction(path, clsname, methodname, aregument);
string text = JsonConvert.SerializeObject(pro);
Behaiver.AddSendQueue(ProtocalType.dllfunction, text);
Behaiver.RegisteReceive(ProtocalType.dllfunction, onReceive);
//Test(text);
}
static void Test(string x)
{
DllFuction protocal = JsonConvert.DeserializeObject<DllFuction>(x);
System.Reflection.Assembly asb = System.Reflection.Assembly.LoadFrom(protocal.dllpath);
var cls = asb.GetType(protocal.classname);
var method = cls.GetMethod(protocal.methodname);
var instence = Activator.CreateInstance(cls);
string back = (string)method.Invoke(instence, protocal.argument);
Debug.Log(back);
}




(图四,在编辑器模式下不通过中间程序反射得到的打开文件窗口界面)

4.异步调用

在view 层,可以直接关联一个按扭来触发打开界面事件,如下所示,当选择完成后会自动将title的信息设置好,但直得到注意的是,这了防止unity因为通信卡死,主程序已经将

这部分功能写在了线程中,如果在选择文件之前,title已经被销毁了,就会出现问题,所以在程序中为了防止这问题,最好是要判断回调时候是否还有需要。当然一个成熟的功能模块应该也要在内部判断异常的机制。

private void openFileBtnClicked()
{
FormLoaderUser.OpenFileDialog("打开文件选择窗口", FormLoaderUser.FileType.Txt, "C://", (x) =>
{
title.text = x;
});
}三、双向通信模块为例说明可扩展性

以上的对话框打开的实现有多种方法,本文介绍的是相对复杂的实现方式,但也是目前我所知最稳定的解决方式,但用其作为本模块功能介绍的目的并不是要实现一个窗口打开的功能,而是要介绍一种unity中调用window窗体的可行的方案。下面将介绍在unity中发信息到winfom,和在winform中控制unity对象的方式,而注意的是这里的winform不是一个独立的程序,而是一个基于中间程序的dll模块!
1.封装一个简单的form(图五)



(图五,一个简单的form界面)

这个form启动器中,向form注册了状态改变的事件,当进行点击按扭的操作时,需要在这个dll中去定义一个和unity3d通信的协议,注意到如果用其他dll时,这并不会对中间程序产生任何需要修改的功能需求,下面是这个form的所有代码,就是告诉unity3d中的一个简单对象应该怎么变化

public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
}
public Action<string> changedaction;
private void turnBig(object sender, EventArgs e)
{
var obj = new { methodName = "turnBig" };
string txt = JsonConvert.SerializeObject(obj);
if(changedaction != null)
{
changedaction.Invoke(txt);
}
else
{
MessageBox.Show("changedaction == null");
}
}

private void turnSmall(object sender, EventArgs e)
{
var obj = new { methodName = "turnSmall" };
string txt = JsonConvert.SerializeObject(obj);
if (changedaction != null)
{
changedaction.Invoke(txt);
}
else
{
MessageBox.Show("changedaction == null");
}

}

private void toLeft(object sender, EventArgs e)
{
var obj = new { methodName = "toLeft" };
string txt = JsonConvert.SerializeObject(obj);

if (changedaction != null)
{
changedaction.Invoke(txt);
}
else
{
MessageBox.Show("changedaction == null");
}

}

private void toRight(object sender, EventArgs e)
{
var obj = new { methodName = "toRight" };
string txt = JsonConvert.SerializeObject(obj);

if (changedaction != null)
{
changedaction.Invoke(txt);
}
else
{
MessageBox.Show("changedaction == null");
}
}

private void toDown(object sender, EventArgs e)
{
var obj = new { methodName = "toDown" };
string txt = JsonConvert.SerializeObject(obj);

if (changedaction != null)
{
changedaction.Invoke(txt);
}
else
{
MessageBox.Show("changedaction == null");
}
}

private void toUp(object sender, EventArgs e)
{
var obj = new { methodName = "toUp" };
string txt = JsonConvert.SerializeObject(obj);

if (changedaction != null)
{
changedaction.Invoke(txt);
}
else
{
MessageBox.Show("changedaction == null");
}
}

public void SetLable(string txt)
{
label1.Text = txt;
}

internal void RegisterHolder(Action<string> onStateChange)
{
this.changedaction = onStateChange;
}
}

2.将unity3d中的一个Cube脚本关联到模块中

在程序的运行初其就将需要操作的对向注册到了这个通信模块中,只要form中触发了对应的事件,那么unity中可以触发相应的效果。

public static void ReigsterCube(Cube cube)
{
try
{
behaiver.RegisteReceive(ProtocalType.communicate, (txt) =>
{
Debug.Log(txt);
JSONNode node = JSONNode.Parse(txt);
string methodName = node.AsObject["methodName"].Value;
var method = cube.GetType().GetMethod(methodName);
method.Invoke(cube, null);
});
}
catch (Exception e)
{
Debug.Log(e);
}

}

cube脚本挂到场景的一个对象上面就好

public class Cube : MonoBehaviour {
void Start()
{
FormLoaderUser.ReigsterCube(this);
}
public void turnBig()
{
transform.localScale *= 2;
}
public void turnSmall()
{
transform.localScale *= 0.5f;
}
public void toLeft()
{
transform.localPosition += Vector3.left;
}
public void toRight()
{
transform.localPosition += Vector3.right;
}
public void toDown()
{
transform.localPosition += Vector3.down;
}
public void toUp()
{
transform.localPosition += Vector3.up;
}
}


3.在unity3d中触发winform的事件

同理,只要在这个窗体打开的时候注册好了相应的事件,在unity可以简单的这样发送信息,效果如图六所示,当前,有一点需要注意,目前还没有解决通过dll加载时会出现中文乱码的问题,如果有方案解决,一定尽快更新代码。
/// <summary>
/// dll中的事件回调
/// </summary>
/// <param name="state"></param>
private void OnHolderStateChanged(string state)
{
dataSender.SendMessage(ProtocalType.communicate.ToString(), state);
//由于在unityedior模式下不支持信息传回所以用读写的方式
if (writeData)
{
string path = System.IO.Directory.GetCurrentDirectory() + "/" + ProtocalType.communicate.ToString() + ".txt";
System.IO.File.WriteAllText(path, state);
}
}

(图六,在unity3d中向已经打开的模块发信息)

四、要点总结

1.中间程序加载说明

中间程序启动后,只是等待信息接收和分发,并不会消耗多少内存和cpu,下面是这个中间程序的启动是属于窗口程序切换模块的一中,我已经将这部分功能放置到了一个dll中(windowswitch),于是调用一个新的程序只需要关心传入什么参数等不需要关心进程启动,然后怎么获取参数,关闭进程等问题

public void TryOpenHelpExe(string exePath)
{
windowswitch = new WindowSwitchUnity();
string path = exePath;// Application.dataPath.Substring(0, Application.dataPath.LastIndexOf("Demo")) + "FormLoader/FormLoader/bin/Debug/FormLoader.exe";
if (System.IO.File.Exists(path))
{
#if UNITY_EDITOR
if (windowswitch.OpenChildWindow(path, false, "1"))
#else
if (windowswitch.OpenChildWindow(path, false))
#endif
{
//打开子应用程序
StartCoroutine(DelyRegisterSender());
}
}
else
{
Debug.LogError("exe not fond");
}
sendThread.Start();
}
需要说明的事,sendmessage还有个问题目前没有解决,暂时无法向unity的编辑器发送信息,只有打包出来之后才能相互沟通,所以暂时,在编辑器模式下只写了一个模拟的实时通信的功能(信息保存到txt中再读取),"1"这个参数就是告诉中间程序应该是以模拟方式运行还是直接发回信息
2.中间程序批量处理dll

unity中序列化的字符串,在中间程序中的解析,下面是一个简单的调用dll中的一个方法,并获取返回值的功能。这个方法写在一个专门处理具体信息解析的脚本中,然后在一个管理程序中进行注册。Win32API.SetWindowPos(currWin,-1,0,0,0,0, Win32API.SetWindowPosFlags.DoNotRedraw);这句代码的作用就是将窗口置于最前端显示,不然打开了窗口,怕是找不到、
public void InvokeSimpleMethodByProtocal(string x)
{
Win32API.SetWindowPos(currWin,-1,0,0,0,0, Win32API.SetWindowPosFlags.DoNotRedraw);

try
{
DllFuction protocal = JsonConvert.DeserializeObject<DllFuction>(x);
Assembly asb = Assembly.LoadFrom(protocal.dllpath);
var cls = asb.GetType(protocal.classname);
var method = cls.GetMethod(protocal.methodname);
var instence = Activator.CreateInstance(cls);
string back = (string)method.Invoke(instence, protocal.argument);
dataSender.SendMessage(ProtocalType.dllfunction.ToString(), back);
//由于在unityedior模式下不支持信息传回所以用读写的方式
if (writeData)
{
string path = System.IO.Directory.GetCurrentDirectory() + "/" + ProtocalType.dllfunction.ToString() + ".txt";
System.IO.File.WriteAllText(path, back);
}
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}

}
以上这种方法的注册是这样写的,分别用于处理不同的dll类型
/// <summary>
/// 注册执行事件
/// </summary>
private void RegistEntry()
{
exeHelp = new ExecuteHelp(swi.Current,dataSender,writeData);
Win32API.SendMessage(swi.Current, Win32API.WM_ACTIVATE, Win32API.WA_ACTIVE, IntPtr.Zero);
dataReceiver.RegisterEvent(ProtocalType.dllfunction.ToString(), exeHelp.InvokeSimpleMethodByProtocal);
dataReceiver.RegisterEvent(ProtocalType.lunchholder.ToString(), exeHelp.LunchCommnuicateHolder);
dataReceiver.RegisterEvent(ProtocalType.communicate.ToString(), exeHelp.RegisterCommuateModle);
}

3.  不显示主窗口的中间程序

 由于sendmessage的实现是基于窗体程序的,如果没有主窗体这功能貌似有点问题,handle找不到,所以研究了下不显示窗体的启动方式,自己定义个HideOnStartupApplicationContext继承于ApplicationContext,在unity中找到了handle后将窗体隐藏了就好,这样:

public HideOnStartupApplicationContext(string[] datas)
{
mainForm = new Form();
mainForm.WindowState = FormWindowState.Minimized;
mainForm.Show();

swi = new WindowSwitchWinform();
if (swi.OnOpenedByParent(ref datas))
{
try
{
if (datas.Length > 0) writeData = int.Parse(datas[0]) == 1;
}
catch (Exception e)
{
MessageBox.Show(e.Message);
}

RegistMessageTrans(parent);
}
mainForm.Hide();
}
五、参考资源:
1.通信核心模块来源

这个大神写的sendmessage功能,实现了unity3d中和其他进程进行通信的方案

unity3d进程通信利用WM_COPYDATE和HOOK

2.常用winapi功能说明

这个网址有所以window开发的api,及c#调用的方法,比较于详细

PInvoke.net

3.本程序相关的dll源码地址

这是个人开发的模块功能,github上有不少开源功能,欢迎收藏

通信功能的封装及demo --sendmessage_unity

窗口切换功能的封装及demo -- windowswitch

4.c#使用json必需品

非常方便的进行json的转换

Newtonsoft.Json

5.隐藏窗口程序的实现方法


WinForm 之 程序启动不显示主窗体
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐