基於Unity的編輯器開發(二): 程序間通訊
共享程式碼
首先要做的, 是需要編輯器和Unity共享一部部分程式碼, 至少協議定義和解析我不想寫兩遍. 雖然有protobuf這樣的工具庫, 但是如果不是跨語言的話, 我覺得沒必要引入另一套流程. 所以我就想能不能讓一個C# dll庫可以同時被Winforms的編輯器和Unity指令碼引用呢? 考查了一下還是可以的:
- Unity的.Net預設是Subset, 需要改成全的
- Unity的.Net預設是C#3.5版本相容的, 一些新的語法(如async)不支援
- Unity中如果要引用Visual Studio編譯出來的C# dll, 需要把Target framework改成”Unity 3.5 .net full Base Class Libraries”
- 把VS編譯出來的dll, 拷到Unity的Asset目錄, mono腳本里就可以直接引用了
同時, 編輯器這邊也需要知道一些遊戲的資料型別和介面的定義, 實驗了一下, UnityEngine.dll, Assembly-CSharp.dll, Assembly-CSharp-firstpass.dll可以直接被VS這邊的C#工程引用, 只要不執行Unity特有的方法(會報”ECall 方法必須打包到系統模組中”的異常), 就可以在Winforms工程中安全地複用遊戲指令碼中的程式碼了.
經過這樣的設定, 編輯器和Unity遊戲可以共享一個dll庫, 從程式碼上做到了共享, 這就為我們的程式碼複用和通訊協議定義提供了基礎保障.
程序間通訊
為了達成這一通訊需求, 首先做了一些搜尋:
- Unity這邊會報異常, Unity的mono對NamePipe支援不是很好
- 有時候會連不上, 比如管道被佔用
所以又換了一個不依賴mono那個不靠譜.net framework的方案, 搜了搜看起來NNanomsg不錯, 使用起來夠簡單, 不過也遇到一些問題:
- 使用ipc協議也會出現莫名其妙連不上的問題, 本質上底層還是走的NamePipe, 換成tcp協議就好了
- 錯誤資訊不夠直觀, 所以我又在NNanomsg里加了nanomsg的一些除錯用的函式的介面
- 一次性發送大量資料(比如幾MB), 會導致連結斷開或卡死, 問了作者說實現機制的問題, 讓我嘗試
用nanomsg的好處就是連線的建立/傳送/接收等不用自己操心, 可以直接連線UnityEditor進行雙端開發, 對於除錯修改非常方便:
通訊協議
class IntMessage : Message
{
public int Value = 100;
}
class StringMessage : Message
{
public string Value = "a string";
}
static void Main(string[] args)
{
MessageHandler.Subscribe((StringMessage m) => Console.WriteLine("String : " + m.Value));
MessageHandler.Subscribe((StringMessage m) => Console.WriteLine("2nd String : " + m.Value));
MessageHandler.Subscribe((IntMessage m) => Console.WriteLine("Int : " + m.Value));
MessageHandler.Subscribe((IntMessage m) => Console.WriteLine("2nd Int : " + m.Value));
MessageHandler.Publish(new IntMessage());
MessageHandler.Publish(new StringMessage());
}
Message直接序列化後就可以傳送到另一端程序了. 不過這樣還是有點繁瑣, 所以我照著Unity的RPC山寨了個LPC:
本質上的實現也是把MethodCall給序列化了, 走的還是SendMessage的流程:
public static void SendMessage(Message message)
{
var stream = new MemoryStream();
binaryFormatter.Serialize(stream, message);
if (!IPC.SendImmediate(stream.GetBuffer()))
{
string log = String.Format("{0} : {1}", message.ToString(), NN.StrError(NN.Errno()));
LogCore(LogType.Error, log);
}
}
public static void ProcedureCall(string className, string methodName, params object[] args)
{
SendMessage(new ProcedureCallMessage { ClassName = className, MethodName = methodName, Arguments = args });
}
收到訊息後通過預先註冊好的MethodInfo直接Invoke呼叫執行就可以了.
其它
還有更高階的需求, 那就是跨程序的物件屬性編輯. 目前的思路是這樣的:
- Unity這邊的資料物件序列化, 傳送到編輯器
- 編輯器收到資料, 反序列化出資料物件(不能依賴Unity的方法, 否則會拋異常)
- 編輯器修改後的物件序列化後傳送到Unity
- Unity這邊反序列化出修改後的物件, 把屬性值拷貝到當前編輯物件上去
雖然簡單暴力, 但也是行之有效的做法, IPC也不用過多考慮資料流量的問題, 當然比較極致一點是實現一套像WPF那樣的DataBinding, 針對每個變化的屬性做程序間同步, 有時間可以嘗試下.