在Unity中创建一个带有Chromium功能的WebView编辑器窗口是一项挑战性的任务,涉及到反射和对 Unity 回调机制的深入理解。本文将指导如何在不到 5 分钟的时间内完成这项任务。
Unity提供了Chromium的封装,因此不需要自己实现或使用操作系统的 API,尽管在其他项目中这可能很有用。此外,自己实现 Chromium 在代码分发时会非常占用空间。如果打算分发代码,最好考虑一下这一点。
Unity 5.4 及更高版本在 WebView 实现方面与早期版本有很大不同。强烈建议反编译 Unity 编辑器并检查文件。
在 Unity 5.4 及更高版本中,WebView 的实现方式有很大不同。在 5.4 之前,能够通过反射使用 WebView DLL 调用其初始化,并接收一个可以停靠或浮动的 WebView 窗口,并且可以调用 WebView 函数如 "onLoadError" 来接收回调,没有任何问题。但现在情况并非如此。
目前,仅使用 "web-view" DLL 将给一个部分功能的窗口,它不能在停靠后重新浮动,因为 Unity 保存了窗口上渲染的对象的方式。
如果不知道什么是 Chromium WebView,让一步一步来(花了好几天时间才弄明白,所以不用担心)。
一般来说,想想Unity的 Asset Store。Asset Store 窗口神奇地使用了 Unity 的网络。嗯...一旦反编译 Unity,会发现根本没有什么魔法,只是一堆混乱的代码,里面有很多内部黑客行为,这就是为什么它没有作为 API 提供的一个很好的原因。
以下是 Asset Store 窗口初始化 WebView 的代码示例:
private void InitWebView(Rect webViewRect)
{
this.m_CurrentSkin = EditorGUIUtility.skinIndex;
this.m_IsDocked = this.docked;
this.m_IsOffline = false;
if (!(bool)this.webView)
{
int x = (int)webViewRect.x;
int y = (int)webViewRect.y;
int width = (int)webViewRect.width;
int height = (int)webViewRect.height;
this.webView = ScriptableObject.CreateInstance();
this.webView.InitWebView((GUIView)this.m_Parent, x, y, width, height, false);
this.webView.hideFlags = HideFlags.HideAndDontSave;
this.webView.AllowRightClickMenu(true);
if (this.hasFocus)
this.SetFocus(true);
}
this.webView.SetDelegateObject((ScriptableObject)this);
this.webView.LoadFile(AssetStoreUtils.GetLoaderPath());
}
要调用 Asset Store 窗口,需要一个矩形,"this" 参数 - 这是实际的窗口,以及 (GUIVIEW).m_Parent - 这是编辑器窗口中的一个私有成员,它包含停靠区域。这是最麻烦的部分。通过反射不能总是得到 m_parent。但这个参数在停靠或取消停靠窗口时是必需的。
Asset Store 通过调用 JavaScript 来更新这个问题:
AssetStoreContext.GetInstance().docked = this.docked;
this.InvokeJSMethod("document.AssetStore", "updateDockStatus");
发现了一个几乎完美的 DLL "UnityEditor.Web.WebViewEditorWindowTabs"。它在窗口功能方面(如停靠和取消停靠)按预期工作,但为了接收回调或调用额外的 Chromium 功能,需要窗口实例,WebView 对象和实际窗口。不幸的是,没有简单的方法来获取它们。
最终实现了自己的 "Create" 函数,创建窗口并同时获取 WebView。这个 DLL 的好处是不需要 m_parent,也不需要使用 "set host" 反射函数。
所做的是:
public static T CreateWebViewEditorWindow(string title, string sourcesPath, int minWidth, int minHeight, int maxWidth, int maxHeight) where T : CustomWebViewEditorWindow, new()
{
var createMethod = webViewEditorWindowType.GetMethod("Create", BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy).MakeGenericMethod(webViewEditorWindowType);
var window = createMethod.Invoke(null, new object[] { title, sourcesPath, minWidth, minHeight, maxWidth, maxHeight });
var customWebEditorWindow = new T { webViewEditorWindow = window };
EditorApplication.delayCall += () =>
{
EditorApplication.delayCall += () =>
{
webView = webViewEditorWindowType.GetField("m_WebView", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(customWebEditorWindow.webViewEditorWindow);
AddGlobalObject(Type.GetType("webView"));
};
};
}
从这一点开始,事情变得容易多了,因为有了所有需要与 Chromium 一起工作的东西。现在有窗口,窗口实例,以及从 DLL 反射得到的 WebView 对象。仍然想要所有的 Chromium 功能,为此使用了反射。例如:
public void LoadURL(string path)
{
try
{
if (webViewEditorWindow != null && webView != null)
{
MethodInfo invokeLoadURLMethod = webView.GetType().GetMethod("LoadURL", BindingFlags.Static | BindingFlags.Public | BindingFlags.FlattenHierarchy | BindingFlags.NonPublic | BindingFlags.Instance);
if (invokeLoadURLMethod != null)
{
object[] param = new object[] { path };
invokeLoadURLMethod.Invoke(webView, param);
}
}
}
catch (TargetInvocationException ex)
{
webView = null;
EditorApplication.update = null;
Debug.LogFormat("{0}", ex.Message);
return;
}
}
为此使用了 unityAsync 机制。这是一种通过 JSON 格式从 JavaScript 向 Unity 发送和接收消息的文档化方式。开始通过调用 init 并提供一个指向 HTML 文件的链接:
[MenuItem("Window/webViewImp")]
private static void Open()
{
string path = "file:///{0}/Assets/StreamingAssets/test/index.html";
var w = CreateWebViewEditorWindow("webViewImp", path, 200, 530, 800, 600);
}