9

我希望用户在 Unity WebGL 游戏中从计算机中选择图像,但我无法为这个东西获取任何库或代码。我想要 UnityWebGL(运行时)的 EditorUtility.OpenFilePanel 的确切功能。

string path = EditorUtility.OpenFilePanel("用 png 覆盖","","png");

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

4

2 回答 2

13

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

这个想法是使用 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 文档中,然后统一监听按钮上的鼠标按下。当它关闭时,我们知道下一个 UP 将是单击,因此我们在 HTML 文档中标记一些标志以记住它。然后,当我们点击文档时,我们可以查看这个标志并得出我们的按钮被点击的结论。然后我们调用打开文件对话框的 javascript 函数,并使用 SendMessage http://docs.unity3d.com/Manual/webgl-interactingwithbrowserscripting.html将结果发送回 Unity 。最后。

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

我们可以使用 WWW 类从中检索数据,只要记住这个 URL 只能在您的应用程序范围内访问。

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

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;
    }
}

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

于 2015-12-06T16:14:40.247 回答
5

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

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

因此,为了帮助任何想要实现这一点的人:

两个 Javascript 函数都必须放在“插件”文件夹内的 .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)我删除了“专注”。这可以防止脚本在程序开始时触发:

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

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

   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 上调用。

于 2019-10-21T15:13:46.947 回答