Unity 5.6 带标签和回调的 WebView
在Unity编辑器中实现带有选项卡窗口和从HTML回调的webview的简单方法
引言
最近有人要求我在 Unity 编辑器中实现一个 WebView。我的客户要求以下功能:
- 一个编辑器窗口 - 及其所有实用功能
- 基本的 Chromium 功能(加载 URL、加载文件、执行 JS 等)
- 接收来自 JS 的回调
- 并使其简单:0)
这项任务非常艰巨,因为它需要大量的反射以及对 Unity 回调机制的深入理解。我花了一个多月的时间才弄清楚如何正确地完成它。所以,让我帮你花不到 5 分钟搞定。
背景
Unity 有自己的 Chromium 包装器,所以你不需要使用操作系统 API 或自己实现它,尽管它可能在其他项目中很有用。此外,自己实现 Chromium 在大小方面成本非常高。如果你要分发代码,最好考虑一下。
另一个问题是,Unity 5.4 及更新版本在所有 Web 实现方面与早期版本有很大不同。我强烈建议反编译 Unity 编辑器并检查文件。
使用代码
正如我所说的,Unity 5.4 及更新版本在他们实现整个 Web-view 设计的方式方面有很大不同。
直到 5.4 版本,我才能够使用 web-view DLL(通过反射)调用其初始化并接收一个功能齐全的 Web-view 窗口,该窗口可以停靠或浮动,并且可以调用 web-view 函数,如“onLoadError”来毫无问题地接收回调……但这已不再是当前情况。
目前,仅使用“web-view”DLL 会得到一个部分功能的窗口,该窗口无法停靠或在停靠后重新浮动,这仅仅是因为 Unity 保存渲染在窗口上的对象的方式。
这一段是为那些不太了解 Chromium WebView 是什么的人准备的。让我们一步一步来(我花了几天才弄明白,所以别担心)。
总的来说,想想 Asset Store。Asset Store 窗口神奇地使用了 Unity 的 Web 功能。嗯……一旦你反编译了 Unity,你会发现根本没有魔法,只有一堆混乱的代码和大量的内部 hack,而且它没有被提供为 API 是有充分理由的。
从 Asset Store 反编译
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<WebView>();
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 窗口,你需要“rectangle”(矩形)、“this”参数——即实际的窗口,以及 (GUIVIEW).m_Parent
——这是一个存在于 EditorWindow 中的私有成员,它保存着停靠区域。这实际上是最棘手的部分。通过反射,你并不总是能获得 m_parent
。但这个参数在停靠或取消停靠窗口时是必需的。
Asset Store 通过调用 JS 进行更新来处理这个问题。
AssetStoreContext.GetInstance().docked = this.docked; this.InvokeJSMethod("document.AssetStore", "updateDockStatus");
所以,长话短说,这不是处理它的正确方法。经过一些调查,我意识到我正在寻找的是 WebView 和标签式窗口之间的连接。
我发现了这个 DLL "UnityEditor.Web.WebViewEditorWindowTabs",它几乎完美。在窗口实用功能(如停靠和取消停靠)方面,它的表现符合预期,但为了接收回调或调用额外的 Chromium 功能,我需要窗口的实例、WebView 对象以及实际的窗口。遗憾的是,没有简单的方法可以获取它们。
我最终实现了自己的“Create
”函数,该函数同时创建窗口并获取 WebView。这个 DLL 的好处是m_parent
不是必需的,我也不需要使用“set host”反射函数。
我所做的就是
public static T CreateWebViewEditorWindow<T>(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);
//For a static method (such as create in UnityEditor.Web.WebViewEditorWindow),
//pass null as the first argument to Invoke. That's nothing to do with generic methods - it's just normal reflection.
var window = createMethod.Invoke(null, new object[] {
title,
sourcesPath,
minWidth,
minHeight,
maxWidth,
maxHeight
});
//saving the window object in webViewEditorWindow
var customWebEditorWindow = new T { webViewEditorWindow = window };
// attach a delegate which will execute after the editor is done updating inspectors
EditorApplication.delayCall += () =>
{
EditorApplication.delayCall += () =>
{
//after all was updated I also take the inst of the window.
webView = webViewEditorWindowType.GetField("m_WebView", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(customWebEditorWindow.webViewEditorWindow);
//addgloabals is necessary for the js communication with the window
AddGlobalObject(Type.GetType("webView"));
};
};
我从这个 GitHub 获得了这个绝妙的主意
https://github.com/kimsama/Unity-WebViewEditorWindow/blob/master/Assets/Editor/CustomWebViewEditorWindow.cs
是的,我复制了 WebViewEditor 反编译的“create window”模板,并获取了 WebView 对象、窗口及其实例。但我还希望这个脚本能够与 JS 对话,所以我不得不创建一个 jsproxymgr 的实例并赋予它我所有的全局变量。请参阅附件代码以获取说明。
从这一点开始,事情就容易多了,因为我拥有了处理 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)
{
// should be set as null to open the editor window again.
webView = null;
// force stop calling the delegate even after closing the window.
EditorApplication.update = null;
Debug.LogFormat("{0}", ex.Message);
return;
}
}
当我实例化窗口时,使用我获取的 WebView。
所以现在我有一个可以停靠和取消停靠的 Web View 窗口的设置方法,有一个可以调用 Unity Chromium 包装器的 WebView 对象,剩下要做的就是看看如何从 HTML 获取回调到我的窗口。
为此,我使用了 unityAsync
这个机制,它类似于协程。这是一种通过 JSON 格式发送和接收来自 JS 到 Unity 的消息的良好文档化的方式。我首先调用 init 函数,并链接到 html 文件。
[MenuItem("Window/webViewImp")]
private static void Open()
{
string path = "file:///{0}/Assets/StreamingAssets/test/index.html";
var w = CreateWebViewEditorWindow<webViewImp>("webViewImp", path, 200, 530, 800, 600);
}
第一个参数是窗口的名称,第二个参数是完整的文件名,另外四个参数是矩形(x, y, 宽度, 高度)。请注意,我这里获取的是实际的窗口,而不是仅仅 scriptableObject
实例,使用了“var w”。当执行 JS 或加载 URL 时,这会很有用。你必须确保你的脚本中有与 HTML 中相同的函数名称,并且不要忘记将它们设置为 public。
请参阅附带的示例。