EditorUtility.OpenFilePanel 用于 Unity WebGL(运行时)

EditorUtility.OpenFilePanel for Unity WebGL (Runtime)

我希望用户在 Unity WebGL 游戏中 select 来自计算机的图像,但我无法获得任何库或代码。我想要 UnityWebGL(运行时)的 EditorUtility.OpenFilePanel 的确切功能。

字符串路径 = EditorUtility.OpenFilePanel("Overwrite with png","","png");

有什么方法可以在 Unity WebGL 构建中打开这个对话框吗?或者有什么办法可以在 java 脚本中做到这一点?就像我从用户的 java-script 中获取图像并将其传递给我的 C# 代码一样。

这听起来似乎很简单,但实际上做起来却相当复杂,原因是 WebGL 构建在浏览器中运行,并且受到许多安全限制,其中包括限制其对本地文件系统的访问。不过,以一种 hacky 的方式是可以做到的。

想法是使用HTML文件输入打开文件浏览对话框。我们可以使用 ExternalEval 从 Unity 代码中调用它,请在此处查看更多信息: http://docs.unity3d.com/Manual/UnityWebPlayerandbrowsercommunication.html http://docs.unity3d.com/ScriptReference/Application.ExternalEval.html

然而,这并不容易。问题是所有现代浏览器都只允许在用户单击事件时显示文件对话框,这是一种安全限制,您对此无能为力。

好的,所以我们可以创建一个按钮,并在单击时打开文件对话框,这样就可以了,对吧?错误的。如果我们简单地创建统一按钮并处理点击 - 这将不起作用,因为 Unity 有自己的事件管理,它与帧速率同步,所以事件只会在实际 javascript 事件结束时发生。这几乎与此处描述的问题相同,http://docs.unity3d.com/Manual/webgl-cursorfullscreen.html 除了 Unity 没有好的内置解决方案。

所以这里是 hack:单击是鼠标按下 + 鼠标向上,对吗?我们将点击监听器添加到 HTML 文档,然后统一监听鼠标在我们的按钮上的按下。当它 down 时,我们知道下一个 UP 将是点击,所以我们在 HTML 文档中标记一些标志以记住它。然后,当我们在文档中点击时,我们可以查看这个标志并得出我们的按钮被点击的结论。然后我们调用 javascript 打开文件对话框的函数,我们使用 SendMessage http://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html 将结果发送回 Unity。终于。

等等,还有更多。问题是我们不能在浏览器中 运行 时简单地获取文件路径。我们的应用程序不允许获取有关用户计算机的任何信息,同样是安全限制。我们能做的最好的事情是使用 URL.CreateObjectURL 获得一个 blob url,这将适用于大多数浏览器,http://caniuse.com/#search=createobjecturl

我们可以使用 WWW class 从中检索数据,请记住此 URL 只能从您的应用程序范围内访问。

所以对于所有这些,解决方案是非常 hacky,但可能。这是一个示例代码,允许用户 select 图像,并将其设置为 material 纹理。

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;

public class OpenFileDialog : MonoBehaviour, IPointerDownHandler {

    public Renderer preview;
    public Text text;

    void Start() {
        Application.ExternalEval(
            @"
document.addEventListener('click', function() {

    var fileuploader = document.getElementById('fileuploader');
    if (!fileuploader) {
        fileuploader = document.createElement('input');
        fileuploader.setAttribute('style','display:none;');
        fileuploader.setAttribute('type', 'file');
        fileuploader.setAttribute('id', 'fileuploader');
        fileuploader.setAttribute('class', 'focused');
        document.getElementsByTagName('body')[0].appendChild(fileuploader);

        fileuploader.onchange = function(e) {
        var files = e.target.files;
            for (var i = 0, f; f = files[i]; i++) {
                window.alert(URL.createObjectURL(f));
                SendMessage('" + gameObject.name +@"', 'FileDialogResult', URL.createObjectURL(f));
            }
        };
    }
    if (fileuploader.getAttribute('class') == 'focused') {
        fileuploader.setAttribute('class', '');
        fileuploader.click();
    }
});
            ");
    }

    public void OnPointerDown (PointerEventData eventData)  {
        Application.ExternalEval(
            @"
var fileuploader = document.getElementById('fileuploader');
if (fileuploader) {
    fileuploader.setAttribute('class', 'focused');
}
            ");
    }

    public void FileDialogResult(string fileUrl) {
        Debug.Log(fileUrl);
        text.text = fileUrl;
        StartCoroutine(PreviewCoroutine(fileUrl));
    }

    IEnumerator PreviewCoroutine(string url) {
        var www = new WWW(url);
        yield return www;
        preview.material.mainTexture = www.texture;
    }
}

如果有人设法找到更简单的方法,请分享,但我真的怀疑是否可行。希望这有帮助。

哇。 Yuri Nudelman 的解决方案是令人印象深刻的黑魔法,它仍然是最好的解决方案。但是:WWW-class 和 ExternalEval 现在都已弃用。

它确实 运行(带有警告),但在不久的将来会停止工作。

所以,为了帮助任何想要实现这个的人:

两个 Javascript 函数都必须放在 "Plugins" 文件夹内的 .jslib 中。 第一个像这样:

mergeInto(
  LibraryManager.library,
  {
    AddClickListenerForFileDialog: function () {
      document.addEventListener('click', function () {

        var fileuploader = document.getElementById('fileuploader');
        if (!fileuploader) {
          fileuploader = document.createElement('input');
          fileuploader.setAttribute('style', 'display:none;');
          fileuploader.setAttribute('type', 'file');
          fileuploader.setAttribute('id', 'fileuploader');
          fileuploader.setAttribute('class', '');
          document.getElementsByTagName('body')[0].appendChild(fileuploader);

          fileuploader.onchange = function (e) {
            var files = e.target.files;
            for (var i = 0, f; f = files[i]; i++) {
              window.alert(URL.createObjectURL(f));
              SendMessage('BrowserFileLoading', 'FileDialogResult', URL.createObjectURL(f));
            }
          };
        }
        if (fileuploader.getAttribute('class') == 'focused') {
          fileuploader.setAttribute('class', '');
          fileuploader.click();
        }
      });
    }
  }
);

请注意,我添加了两个更改: a) 我删除了 'focused'。这可以防止脚本在程序开始时触发:

   fileuploader.setAttribute('class', '');

b) 我手动添加了我的 Unity GameObject 的名称。 必须 与您放置 (unity-) 脚本的游戏对象相同:

   SendMessage('BrowserFileLoading', 'FileDialogResult', URL.createObjectURL(f));

您可以使用以下方式调用此外部函数:

using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using System.Collections;
using UnityEngine.Networking;
using System.Runtime.InteropServices;
public class BrowserFileLoadingDialog : MonoBehaviour
{
  [DllImport("__Internal")] private static extern void AddClickListenerForFileDialog();

  void Start()
  {
    AddClickListenerForFileDialog();
  }

  public void FileDialogResult(string fileUrl)
  {
    Debug.Log(fileUrl);
    UrlTextField.text = fileUrl;
    StartCoroutine(LoadBlob(fileUrl));
  }

  IEnumerator LoadBlob(string url)
  {
    UnityWebRequest webRequest = UnityWebRequest.Get(url);
    yield return webRequest.SendWebRequest();

    if (!webRequest.isNetworkError && !webRequest.isHttpError)
    {
      // Get text content like this:
      Debug.Log(webRequest.downloadHandler.text);

    }
}

第二个脚本(可以放在同一个 .jslib 文件中)如下所示:

mergeInto(
  LibraryManager.library,
  {
    FocusFileUploader: function () {
      var fileuploader = document.getElementById('fileuploader');
      if (fileuploader) {
          fileuploader.setAttribute('class', 'focused');
      }
    }
  }
);

这里没有大的变化,像上面那样使用并且应该(就像 Yuri Nudelman 建议的那样)在 CursorDown 上调用。