目录
2. 在Unity Assets/Plugins目录下创建jslib文件FileDialog.jslib
最近想开发一个提效工具,用于删除现有的云端(比如阿里云、腾讯云等。我们公司的是未来云)资源,并上传新的文件(我们处理的是unity热更资源,包括bundle文件和zip文件)到云端。为了方便mac和windows都可用,准备用unity发布WebGL的方式实现。想着应该很简单,因为这个功能已经在Unity 编辑器内实现了,如下:
还是年轻,想的太简单了。Unity发布WebGL后发现一堆一堆一堆坑,解决了一周时间,终于柳暗花明又一村。现在总结一下这些坑及解决方式。
- 发布WebGL后,需要部署到服务端,或者本地搭建环境模拟线上环境。
- 发布WebGL后,遇到了跨域问题,原先可成功调用的上传、删除等接口报405错误
- 发布WebGL后,InputFeild输入框不支持复制粘贴
- 最重要的问题,本地文件读取不了了
接下来依次来解决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
- 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偶现报错,清理浏览器缓存后解决,后续解决后补充。
评论记录:
回复评论: