PAYATHON 2026

如何在 Unity 项目中将游戏生成的文件下载到用户设备?

技术老齐

可以实现,也不一定需要 Asset Store 插件。先生成文件内容,再按运行平台提供导出入口:WebGL 通过浏览器下载,Android 调用系统“另存为”界面,iOS 调用文档导出界面。

游戏运行时生成的文件通常可以直接导出,无须先上传服务器。需要跨设备领取、通过链接分享等功能时,再额外设计服务端存储。

为什么写入文件后,用户仍然找不到它?

“保存供游戏读取”和“导出给用户”是两个步骤。Application.persistentDataPath 适合保存游戏需要保留的数据,但它并不是设备的公共下载目录。在 WebGL 中,这个路径对应浏览器的 IndexedDB 文件系统;在 Android 和 iOS 中,它对应应用使用的数据目录。具体路径由平台决定,不能硬编码。Unity:Application.persistentDataPath

实现时可以分两步:

  1. 游戏生成 byte[],必要时先保存一份。
  2. 用户点击“导出”,通过浏览器或系统界面完成保存。
运行平台导出普通文件的方式
WebGLJavaScript 创建 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:访问共享存储中的文档及其他文件

接入流程如下:

  1. Unity 生成文件,或先保存到应用的数据目录。
  2. Java/Kotlin 原生插件发起 ACTION_CREATE_DOCUMENT。
  3. 原生层接收用户选择结果,通过 ContentResolver.openOutputStream() 写入文件。
  4. 将成功、取消或失败结果回传给 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 环境和保存结果回调即可,不必由同一个插件完成所有平台的适配。

备注:内容仅供参考。