Unity | 发布WebGL遇到的那些事儿

06-01 1904阅读

目录

一、跨域问题

二、InputFeild输入框不支持复制粘贴问题

三、读取本地文件失败问题

1. 先附个WebGL的页面效果

2. 在Unity Assets/Plugins目录下创建jslib文件FileDialog.jslib

3.Js模版

4.Unity中调用 

5.打包测试

四、补充说明


        最近想开发一个提效工具,用于删除现有的云端(比如阿里云、腾讯云等。我们公司的是未来云)资源,并上传新的文件(我们处理的是unity热更资源,包括bundle文件和zip文件)到云端。为了方便mac和windows都可用,准备用unity发布WebGL的方式实现。想着应该很简单,因为这个功能已经在Unity 编辑器内实现了,如下:

Unity | 发布WebGL遇到的那些事儿

        还是年轻,想的太简单了。Unity发布WebGL后发现一堆一堆一堆坑,解决了一周时间,终于柳暗花明又一村。现在总结一下这些坑及解决方式。

  1. 发布WebGL后,需要部署到服务端,或者本地搭建环境模拟线上环境。
  2. 发布WebGL后,遇到了跨域问题,原先可成功调用的上传、删除等接口报405错误
  3. 发布WebGL后,InputFeild输入框不支持复制粘贴
  4. 最重要的问题,本地文件读取不了了

        接下来依次来解决234问题。

一、跨域问题

Unity | 发布WebGL遇到的那些事儿

        解决方法:确保API服务器在响应头中设置了适当的CORS头,例如Access-Control-Allow-Origin。这可以是通配符 (*),但更安全和推荐的方法是指定确切的域名(如 http://example.com)

        幸好公司提供的未来云平台支持设置跨域(跨域问题直接让提供API的服务器伙伴解决):

Unity | 发布WebGL遇到的那些事儿

二、InputFeild输入框不支持复制粘贴问题

        Unity插件unity-webgl-copy-and-paste-v0.2.0.unitypackage即可解决这个问题,看了下原理,也是采用和JS交互来解决的。

三、读取本地文件失败问题

        由于浏览器的安全设置,System.IO读取本地文件的大部分功能都会受限。比如之前获取本地文件夹中文件列表、读取本地文件的代码都不支持:

    private static Queue GetLocalFileLists(string dirPath, bool onlyZip = false)
    {
        Queue fileList = new Queue();
        if (Directory.Exists(dirPath))
        {
            string[] files = Directory.GetFiles(dirPath);
            for (int i = 0; i  
    private static byte[] LoadData(string path)
    {
        Debug.Log("LoadData path:" + path);
        return System.IO.File.ReadAllBytes(path);
    }
    
     private static byte[] LoadData(string path)
    {
         FileStream fs = new FileStream(path, FileMode.Open);
         byte[] data = new byte[fs.Length];
         fs.Read(data, 0, data.Length);
         fs.Close();
         return data;
    }

        网上大佬前辈们和ChatGpt都建议使用Unity WebGL和JS交互来解决这个问题。先说一下原理:浏览器沙盒目录中的文件才支持读取。那我们需要利用JS创建一个文件夹选择对话框来选择要操作的文件,将文件列表发送给Unity WebGL,在Unity中利用UnityWebRequest将文件加载到浏览器沙盒目录下。就这么简单。来吧展示!

1. 先附个WebGL的页面效果

Unity | 发布WebGL遇到的那些事儿

2. 在Unity Assets/Plugins目录下创建jslib文件FileDialog.jslib

mergeInto(LibraryManager.library, {
    LoadFolder: function(_gameObjectName, _isZip) {
        console.log('Pointers:', {
            gameObjectName: _gameObjectName,
            isZip: _isZip
        });
        var gameObjectName = UTF8ToString(_gameObjectName);
        var isZip = UTF8ToString(_isZip);
        
        console.log('LoadFolder called for GameObject:', gameObjectName);
        console.log('LoadFolder called for ISZip:', isZip);
        // 创建新的文件输入元素
        console.log('Creating new file input element.');
        var fileInput = document.createElement('input');
        fileInput.type = 'file';
        fileInput.id = 'folderInput';
        fileInput.webkitdirectory = true;  // 允许选择文件夹
        fileInput.multiple = true; // 允许多选文件
        fileInput.style.display = 'none';  // 隐藏元素
        document.body.appendChild(fileInput);
        // 定义事件处理函数
        var fileInputChangeHandler = function(event) {
            console.log('File selection changed.'); // 输出文件选择发生变化的信息
            var files = Array.from(event.target.files);
            console.log('Selected files:', files); // 输出所选文件的信息
            var fileNames = files.map(file => ({
                name: file.name,
                blobPath: URL.createObjectURL(file),
                localPath: file.webkitRelativePath || file.name
            }));
            var resultString = JSON.stringify({ files: fileNames });
            console.log('Sending file dialog result:', resultString);  // 输出要发送到 Unity 的结果信息
            // 确保 gameInstance 已正确初始化
            if (window.gameInstance) {
                var message = isZip + "|" + resultString;
                window.gameInstance.SendMessage(gameObjectName, 'FileDialogResult', message);
            } else {
                console.error('gameInstance is not defined');
            }
            // 移除事件监听器并删除输入元素
            fileInput.removeEventListener('change', fileInputChangeHandler);
            document.body.removeChild(fileInput);
        };
        // 添加事件监听器
        fileInput.addEventListener('change', fileInputChangeHandler);
        console.log('Triggering file input click.');
        fileInput.click();
    }
});

(1)LoadFolder函数支持Unity调用,该函数有两个参数,第一个是Unity中挂载脚本的物体名,第二个参数是我根据需求来设置传zip文件还是普通文件。所有C#传给js的字符串都需要用Pointer_stringify(或UTF8ToString 2021.2版本及以上)转化一遍,才能转化成js识别的字符串。官方文档:Interaction with browser scripting - Unity 手册

(2)调用LoadFolder函数,会创建文件夹选择对话框。当选择的文件有变化时,会触发fileInputChangeHandler函数,函数中会通过(gameInstance)Unity的SendMessage函数来进行通知,调用挂载脚本的FileDialogResult函数,传递文件列表。

(3)文件列表数据如下:

Sending file dialog result: {"files":
[
{
"name":".DS_Store",
"blobPath":"blob:https://static0.xesimg.com/aa004c1f-947a-4237-8e15-cfd86b50281e",
"localPath":"zip/.DS_Store"
},
{
"name":"Android_Resource_base.zip",
"blobPath":"blob:https://static0.xesimg.com/d3df1350-032a-4e2e-89d4-d2185f9015cf",
"localPath":"zip/Android_Resource_base.zip"
}
]}

        注意文件列表中的blobPath值(blob:xxx的形式),这种形式才能被WebRequest读取到,再加载到浏览器沙盒目录下。沙盒目录下路径为::/idbfs/bad4f2aac7af9d794a38b7e22b79d351/Res/Android_Resource_base.zip

(4)由于SendMessage只支持一个参数,在这里把isZip和文件列表信息合并在了一个字符串中。当然也可封装成一个json字符串。

(5)gameInstance是什么呢?gameInstance是unity运行实例,有的叫unityInstance或者别的东西,具体看自己js模版中定义的变量。

(6)JS代码中每次调用LoadFolder都创建fileInput对话框,及时销毁即可,防止内存泄漏。因为本人出现过【第一次调用LoadFolder函数isZip是true,第二次传的是false,但第二次isZip返回到unity的还是true】的问题及【change监听触发多次】的问题。可能在于 fileInputChangeHandler 函数中 isZip 变量的值没有及时更新,导致多次调用 LoadFolder 时使用的是上一次调用时的参数值。

3.Js模版

        补充index.html文件,来实例化gameInstance:

Unity | 发布WebGL遇到的那些事儿

4.Unity中调用 

[System.Serializable]
public class FileList
{
    public FileDetail[] files;
}
[System.Serializable]
public class FileDetail
{
    public string name;
    public string blobPath;
    public string localPath;
}
public class ToolView : MonoBehaviour
{
    [DllImport("__Internal")]
    private static extern void LoadFolder(string gameObjectName, string isZip);
  
    private Button zipPathButton;
    private Button resPathButton;
    private void Awake()
    {
        zipPathButton = transform.Find("ZipPath/ZipPathButton").GetComponent();
        zipPathButton.onClick.AddListener(() =>
        {
            GetLocalFileLists(true);
        });
        resPathButton = transform.Find("ResPath/ResPathButton").GetComponent();
        resPathButton.onClick.AddListener(() =>
        {
            GetLocalFileLists(false);
        });
    }
    public void GetLocalFileLists(bool isZip = false)
    {
        string _iszip = isZip ? "true" : "false";
        string name = gameObject.name;
        Debug.Log("Unity GetLocalFileLists " + "isZip: " + _iszip + " name: " + name);
        LoadFolder(name, _iszip);
    }
    public void FileDialogResult(string message)
    {
        Debug.Log("Unity FileDialogResult: " + message);
        string[] messages = message.Split('|');
        string filesJson = messages[1];
        bool isZip = bool.Parse(messages[0]);
        if (isZip)
        {
            zipLocalFileList.Clear();
        }
        else
        {
            resLocalFileList.Clear();
        }
        var files = JsonUtility.FromJson(filesJson);
        needCopyCount = files.files.Length;
        Debug.Log("Received files:" + needCopyCount);
        copyCount = 0;
        foreach (var file in files.files)
        {
            StartCoroutine(CopyFile(file, isZip));
        }
    }
    int copyCount = 0;
    int needCopyCount = 0;
    IEnumerator CopyFile(FileDetail jsFileInfo, bool isZip = false)
    {
        // Debug.Log("Unity CopyFile: " + jsFileInfo.name + " - " + jsFileInfo.path);
        UnityWebRequest request = UnityWebRequest.Get(jsFileInfo.blobPath);
        //创建文件夹
        string dirPath = Path.Combine(Application.persistentDataPath, "Res");
        // Debug.Log("将被存至目录:" + dirPath);
        if (!Directory.Exists(dirPath))
        {
            Directory.CreateDirectory(dirPath);
        }
        string fullPath = Path.Combine(dirPath, jsFileInfo.name);
        request.downloadHandler = new DownloadHandlerFile(fullPath);//路径+文件名
        // Debug.Log("复制到沙盒ing:" + fullPath);
        yield return request.SendWebRequest();
        if (request.result == UnityWebRequest.Result.Success)
        {
            copyCount++;
            Debug.Log("复制到沙盒完成:" + fullPath + "," + copyCount);
            if (isZip)
            {
                if (fullPath.EndsWith(".zip"))
                {
                    zipLocalFileList.Enqueue(fullPath);
                }
            }
            else
            {
                resLocalFileList.Enqueue(fullPath);
            }
            if (needCopyCount == copyCount)
            {
                if (isZip)
                {
                    zipPathInputField.text = ".../" + jsFileInfo.localPath;
                }
                else
                {
                    resPathInputField.text = ".../" + jsFileInfo.localPath;
                }
            }
        }
        else
        {
            Debug.Log(request.error);
        }
    }
}

        文件拷贝到浏览器沙盒目录后, 即可使用​​​​​​​System.IO.File.ReadAllBytes(path)加载文件喽:

             while (localFileList.Count > 0)
            {
                string item = zipLocalFileList.Dequeue();
                byte[] data = LoadData(item);
                if (data != null)
                {
                    Debug.Log("LoadData succeed");
                    //...
                }
            }

5.打包测试

        注意Editor模式运行会报错EntryPointNotFoundException。需要打包运行测试。

四、补充说明

1.使用Chrome浏览器来运行WebGL,Safari浏览器无法弹出文件夹选择对话框。

2.运行WebGL偶现报错,清理浏览器缓存后解决,后续解决后补充。

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

相关阅读

目录[+]

取消
微信二维码
微信二维码
支付宝二维码