Unity WebView 编辑器窗口实现指南

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 简介

如果不知道什么是 Chromium WebView,让一步一步来(花了好几天时间才弄明白,所以不用担心)。

一般来说,想想Unity的 Asset Store。Asset Store 窗口神奇地使用了 Unity 的网络。嗯...一旦反编译 Unity,会发现根本没有什么魔法,只是一堆混乱的代码,里面有很多内部黑客行为,这就是为什么它没有作为 API 提供的一个很好的原因。

Asset Store 的反编译

以下是 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 窗口的调用

要调用 Asset Store 窗口,需要一个矩形,"this" 参数 - 这是实际的窗口,以及 (GUIVIEW).m_Parent - 这是编辑器窗口中的一个私有成员,它包含停靠区域。这是最麻烦的部分。通过反射不能总是得到 m_parent。但这个参数在停靠或取消停靠窗口时是必需的。

Asset Store 通过调用 JavaScript 来更新这个问题:

AssetStoreContext.GetInstance().docked = this.docked; this.InvokeJSMethod("document.AssetStore", "updateDockStatus");

WebViewEditorWindowTabs DLL

发现了一个几乎完美的 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")); }; }; }

与 JavaScript 通信

从这一点开始,事情变得容易多了,因为有了所有需要与 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); }
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485