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

目录

一、跨域问题

二、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后发现一堆一堆一堆坑,解决了一周时间,终于柳暗花明又一村。现在总结一下这些坑及解决方式。

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

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

一、跨域问题

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

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

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

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

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

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

    private static Queue<string> GetLocalFileLists(string dirPath, bool onlyZip = false)
    {
        Queue<string> fileList = new Queue<string>();
        if (Directory.Exists(dirPath))
        {
            string[] files = Directory.GetFiles(dirPath);
            for (int i = 0; i < files.Length; i++)
            {
                Debug.Log("local files:" + files[i]);
                if (onlyZip && !files[i].Contains(".zip"))
                {
                    Debug.Log("只上传zip文件,跳过:" + files[i]);
                    continue;
                }
                fileList.Enqueue(files[i]);
            }
        }
        return fileList;
    }
    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的页面效果

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:

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<Button>();
        zipPathButton.onClick.AddListener(() =>
        {
            GetLocalFileLists(true);
        });

        resPathButton = transform.Find("ResPath/ResPathButton").GetComponent<Button>();
        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<FileList>(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偶现报错,清理浏览器缓存后解决,后续解决后补充。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

烫青菜

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值