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后发现一堆一堆一堆坑,解决了一周时间,终于柳暗花明又一村。现在总结一下这些坑及解决方式。
- 发布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 GetLocalFileLists(string dirPath, bool onlyZip = false) { Queue fileList = new Queue(); if (Directory.Exists(dirPath)) { string[] files = Directory.GetFiles(dirPath); for (int i = 0; iprivate 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偶现报错,清理浏览器缓存后解决,后续解决后补充。