如何在 Unity 项目中将游戏生成的文件下载到用户设备?
可以实现,也不一定需要 Asset Store 插件。先生成文件内容,再按运行平台提供导出入口:WebGL 通过浏览器下载,Android 调用系统“另存为”界面,iOS 调用文档导出界面。
游戏运行时生成的文件通常可以直接导出,无须先上传服务器。需要跨设备领取、通过链接分享等功能时,再额外设计服务端存储。
为什么写入文件后,用户仍然找不到它?
“保存供游戏读取”和“导出给用户”是两个步骤。Application.persistentDataPath 适合保存游戏需要保留的数据,但它并不是设备的公共下载目录。在 WebGL 中,这个路径对应浏览器的 IndexedDB 文件系统;在 Android 和 iOS 中,它对应应用使用的数据目录。具体路径由平台决定,不能硬编码。Unity:Application.persistentDataPath
实现时可以分两步:
- 游戏生成
byte[],必要时先保存一份。 - 用户点击“导出”,通过浏览器或系统界面完成保存。
| 运行平台 | 导出普通文件的方式 |
|---|---|
| WebGL | JavaScript 创建 Blob,提供浏览器下载链接 |
| Android 原生应用 | 使用 Storage Access Framework,让用户选择保存位置 |
| iOS 原生应用 | 使用 UIDocumentPickerViewController 导出文件 |
在手机浏览器中运行的 Unity WebGL 项目仍然使用浏览器方案。Android 和 iOS 原生应用才使用各自的原生接口。
WebGL:通过 JavaScript 提供下载链接
Unity 可以通过 .jslib 文件调用 JavaScript,C# 侧使用 [DllImport("__Internal")] 声明对应函数。Unity:与浏览器脚本交互
以下示例导出一个小型文本文件。页面先显示下载链接,用户点击后保存,让下载由真实的用户点击触发。
1. 添加 JavaScript 插件
创建 Assets/Plugins/WebGL/FileExport.jslib:
mergeInto(LibraryManager.library, {
PrepareFileDownload: function (dataPtr, length, namePtr, mimePtr) {
// 复制数据,避免在 C# 调用结束后继续依赖 Unity 内存。
var bytes = HEAPU8.slice(dataPtr, dataPtr + length);
var fileName = UTF8ToString(namePtr);
var mimeType = UTF8ToString(mimePtr);
var blob = new Blob([bytes], { type: mimeType });
var linkId = "unity-generated-file-download";
var link = document.getElementById(linkId);
if (!link) {
link = document.createElement("a");
link.id = linkId;
link.style.cssText =
"position:fixed;bottom:16px;left:16px;z-index:9999;" +
"padding:12px;background:white;color:black;";
document.body.appendChild(link);
} else {
// 替换上一份导出内容时,释放旧的对象 URL。
URL.revokeObjectURL(link.href);
}
link.href = URL.createObjectURL(blob);
link.download = fileName;
link.textContent = "下载 " + fileName;
}
});
Blob 保存待下载的数据,download 属性提供建议文件名。浏览器可能直接保存、弹出提示或打开文件,具体行为取决于浏览器和用户设置。MDN:<a> 的 download 属性
对象 URL 会占用资源,因此示例在替换文件时会释放旧 URL。如果添加了“关闭下载入口”功能,也应在不再需要链接时调用 URL.revokeObjectURL(),然后移除元素。MDN:URL.revokeObjectURL()
2. 从 Unity 调用
创建 WebFileExporter.cs,挂到场景中的 GameObject,并把按钮的 OnClick 绑定到 ExportText():
using System.Runtime.InteropServices;
using System.Text;
using UnityEngine;
public class WebFileExporter : MonoBehaviour
{
#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void PrepareFileDownload(
byte[] data,
int length,
string fileName,
string mimeType);
#endif
public void ExportText()
{
byte[] data = Encoding.UTF8.GetBytes("这是游戏生成的文件。\n");
#if UNITY_WEBGL && !UNITY_EDITOR
PrepareFileDownload(
data,
data.Length,
"result.txt",
"text/plain;charset=utf-8");
#else
Debug.Log("请在 WebGL 构建中验证浏览器下载。");
#endif
}
}
构建并运行 WebGL 后,点击游戏中的导出按钮,页面就会出现 result.txt 的下载链接。使用时,将 data 替换为游戏实际生成的字节数组。
下载入口需要在实际托管页面中验证,尤其是在移动端浏览器或嵌入式页面中。如果使用带 sandbox 的 iframe,还要检查宿主页面是否允许下载。
Android:让用户选择保存位置
导出 TXT、JSON、ZIP 等普通文件,可以使用 Storage Access Framework 的 ACTION_CREATE_DOCUMENT。系统显示保存界面,用户选择位置后,应用获得目标文件的 URI,再向其中写入数据。这个流程不需要申请广泛的存储访问权限。Android:访问共享存储中的文档及其他文件
接入流程如下:
- Unity 生成文件,或先保存到应用的数据目录。
- Java/Kotlin 原生插件发起
ACTION_CREATE_DOCUMENT。 - 原生层接收用户选择结果,通过
ContentResolver.openOutputStream()写入文件。 - 将成功、取消或失败结果回传给 Unity。
发起保存界面的 Kotlin 代码需要放在能够接收 Activity 结果的原生层中:
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "text/plain"
putExtra(Intent.EXTRA_TITLE, "result.txt")
}
获得用户选择的 uri 后,再写入数据:
val output = contentResolver.openOutputStream(uri)
?: throw IOException("无法打开目标文件")
output.use { stream ->
stream.write(bytes)
}
这里的 bytes 是待导出的 ByteArray。也可以读取 Unity 已保存的源文件,以流的方式复制。
返回值通常是 content:// URI,需要通过 ContentResolver 访问,不能当作普通磁盘路径直接传给 File.WriteAllBytes()。Android:ContentResolver.openOutputStream
这些片段只包含保存流程中的核心操作,还需要接入 Unity 与原生层之间的桥接、Activity 结果接收和错误回调。使用现成插件时,要确认它支持 Android 文档导出及这些回调。
iOS:导出到用户选择的文件位置
iOS 可以先将生成内容写入应用目录,再通过 UIDocumentPickerViewController 导出。init(forExporting:asCopy:) 接收本地文件 URL,支持以复制方式导出。Apple:init(forExporting:asCopy:)
在 Unity 侧先保存文件:
using System.IO;
using System.Text;
using UnityEngine;
public static class GeneratedFileStorage
{
public static string SaveText(string content)
{
string path = Path.Combine(
Application.persistentDataPath,
"result.txt");
File.WriteAllBytes(path, Encoding.UTF8.GetBytes(content));
return path;
}
}
把返回的绝对路径传给 iOS 原生插件,由原生层创建文件 URL 并展示导出界面。以下 Swift 片段假设 fileURL 指向已写入完成的文件,presenter 是当前可用于展示界面的 UIViewController:
if #available(iOS 14.0, *) {
let picker = UIDocumentPickerViewController(
forExporting: [fileURL],
asCopy: true
)
presenter.present(picker, animated: true)
}
展示 UIKit 界面的操作应在主线程执行。完整插件还需要处理导出完成和取消回调,并在导出结束前保留源文件。如果要支持更早的系统版本,需要另外适配对应接口。
如果还需要 AirDrop、邮件等分享入口,可以使用 UIActivityViewController。可用操作由系统和传入内容决定;在 iPad 上,还需要配置 popover 展示位置。Apple:UIActivityViewController 初始化与展示要求
实现时还要注意什么?
- 文件格式、扩展名和 MIME 类型应保持一致。普通二进制文件可以使用
application/octet-stream。 - 如果用户需要在下次启动游戏后再导出,应先持久化文件。只把
byte[]保留在内存中无法满足这个需求。 - 大文件应考虑流式复制。WebGL 示例会复制数据并创建
Blob,文件较大时需要评估内存占用。 - 移动端应分别处理“成功”“取消”和“失败”。刚打开系统界面时,不能立即提示“已保存”。
- 要把浏览器下载、应用内保存和原生导出操作统一成一个跨平台按钮,还需要用条件编译封装各平台的实现,并在目标设备上验证。
选择插件时,可以搜索“WebGL file download”“Android SAF export”“iOS document picker”。确认插件支持目标平台、当前项目的 Unity 环境和保存结果回调即可,不必由同一个插件完成所有平台的适配。
备注:内容仅供参考。
