Unity 资源文件下载器

注意事项:使用需配合UniTask插件 

GitHub - Cysharp/UniTask: Provides an efficient allocation free async/await integration for Unity.

1️⃣ 需求

从服务器或对象存储桶中下载文件到指定文件夹,示例场景:第一次启动游戏时下载初始数据(音视频、图片等)

2️⃣ 优点

  • UniTask异步下载
  • 分块写入操作
  • 支持下载任何文件(图像、视频、pdf、apk等)
  • 支持下载多个文件
  • 支持下载跟踪器(下载进度、文件大小、文件数)
  • 支持链式编程
  • 支持PC、Android下载到Application.persistentDataPath目录下 (其它路径没有试,有测试的小伙伴可以评论一下)

3️⃣ 后期规划

  • 支持MD5文件校验

4️⃣ 关键代码 

using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Cysharp.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;

public class Downloader
{
    private Action onStart;
    private Action onComplete;
    private int filesCount;

    public Downloader()
    {
        Init();
    }

    private void Init()
    {
        filesCount = 0;
        if (!Directory.Exists($"{Application.persistentDataPath}/Resources"))
        {
            Directory.CreateDirectory($"{Application.persistentDataPath}/Resources");
        }
    }
    
    /// <summary>
    /// 开始下载
    /// </summary>
    /// <param name="startDownload"></param>
    /// <returns></returns>
    public Downloader OnStart(Action startDownload)
    {
        onStart = startDownload;
        return this;
    }
    
    /// <summary>
    /// 下载单个文件
    /// </summary>
    /// <param name="url"></param>
    /// <param name="downloadTracker">下载跟踪器</param>
    /// <param name="downLoadError"></param>
    public Downloader DownloadFileAsync(string url, Action<DownloadTracker> downloadTracker = null, Action<string> downLoadError = null)
    {
        DownloadFile(url, PathConfig.ResSavePath(url), downloadTracker,downLoadError).Forget();
        return this;
    }
    
    /// <summary>
    /// 下载多个文件
    /// </summary>
    /// <param name="urls"></param>
    /// <param name="downloadTracker">下载跟踪器</param>
    /// <param name="currenDownloadCont">当前下载文件数量,总文件数量</param>
    /// <param name="downLoadError">下载错误</param>
    public Downloader DownloadMultipleFileAsync(List<string> urls,
       Action<int, int> currenDownloadCont = null, Action<DownloadTracker> downloadTracker = null,
        Action<string> downLoadError = null)
    {
        DownloadMultipleFiles(urls, currenDownloadCont,downloadTracker, downLoadError).Forget();
        return this;
    }

    /// <summary>
    /// 下载完成回调
    /// </summary>
    /// <param name="complete"></param>
    public void OnComplete(Action complete)
    {
        this.onComplete = complete;
    }

    private async UniTask DownloadFile(string url, string savePath,
        Action<DownloadTracker> currentTracker , Action<string> downLoadError, bool isMultiple = false)
    {
        onStart?.Invoke();
        try
        {
            var fileSize = await GetFileSize(url);
            if (fileSize > 0)
            {
                using var request = UnityWebRequest.Get(url);
                request.downloadHandler = new DownloadHandlerBuffer();
                var asyncOperation = request.SendWebRequest();
                while (!asyncOperation.isDone)
                {
                    // 更新下载进度
                    var currentProgress = request.downloadProgress;
                    var currentSize = BytesToMB(request.downloadedBytes);
                    currentTracker?.Invoke(new DownloadTracker(fileSize, currentSize,
                        currentProgress > 0.99f ? 1f : currentProgress));
                    await UniTask.Yield();
                }

                if (request.result != UnityWebRequest.Result.Success)
                {
                    Debug.LogError("Error downloading: " + request.error);
                    downLoadError?.Invoke(request.error);
                    return;
                }

                await WriteFile(request.downloadHandler.data, savePath, currentTracker);
                Debug.Log("File saved to: " + savePath);

                if (isMultiple)
                {
                    // 一种线程安全的操作,用于原子性地递增变量filesCount的值
                    Interlocked.Increment(ref filesCount);
                }
                else
                {
                    await UniTask.Yield();
                    onComplete?.Invoke();
                }
            }
            else if (fileSize < 0)
            {
                downLoadError?.Invoke("获取文件失败");
            }
        }
        catch (Exception e)
        {
            Debug.LogError("DownloadFile exception: " + e.Message);
            downLoadError?.Invoke(e.Message);
        }
    }


    private async UniTask DownloadMultipleFiles(List<string> urls,
        Action<int, int> currenDownCont,Action<DownloadTracker> downloadTracker,Action<string> downLoadError)
    {
        if (urls == null || urls.Count == 0) return;
        var resUrls = urls.Distinct().ToList();
        onStart?.Invoke();
        for (var i = 0; i < resUrls.Count; i++)
        {
            currenDownCont?.Invoke(i + 1, resUrls.Count);
            await DownloadFile(resUrls[i], PathConfig.ResSavePath(resUrls[i]), downloadTracker, downLoadError, true);
        }
        if (filesCount >= resUrls.Count)
        {
            onComplete?.Invoke();
        }
    }

    private async UniTask<float> GetFileSize(string url)
    {
        try
        {
            // 增加URL合法性检查
            if (!Uri.IsWellFormedUriString(url, UriKind.Absolute))
            {
                Debug.LogError("Invalid URL");
                return -1;
            }

            using var headRequest = UnityWebRequest.Head(url);
            var asyncOperation = headRequest.SendWebRequest();

            // 直接await异步操作,而不是使用UniTask.WaitUntil
            // 等待请求完成
            await UniTask.WaitUntil(() => asyncOperation.isDone);

            if (headRequest.result != UnityWebRequest.Result.Success)
            {
                // 使用UnityWebRequest.error获取详细错误信息
                Debug.LogError($"Request failed: {headRequest.error}");
                return -1;
            }

            if (long.TryParse(headRequest.GetResponseHeader("Content-Length"), out long fileSize))
            {
                var size = fileSize / (1024f * 1024f);
                // 使用格式化字符串
                Debug.Log($"File size: {size} MB");
                return size;
            }
            else
            {
                Debug.LogError("Error parsing file size");
                return -1;
            }
        }
        catch (Exception e)
        {
            // 捕获并处理异步操作中可能抛出的异常
            Debug.LogError($"Exception occurred: {e.Message}");
            return -1;
        }
    }

    /// <summary>
    /// 写入文件
    /// </summary>
    /// <param name="data"></param>
    /// <param name="savePath"></param>
    /// <param name="downloadTracker"></param>
    private async UniTask WriteFile(byte[] data, string savePath, Action<DownloadTracker> downloadTracker)
    {
        // 验证路径是否合法
        if (!Path.IsPathRooted(savePath) || !Directory.Exists(Path.GetDirectoryName(savePath)))
        {
            throw new ArgumentException("Invalid save path.", nameof(savePath));
        }
        
        var chunkSize = 4096 * 100; // 每次写入的块大小
        var totalBytesWritten = 0;
        try
        {
            await using var fileStream =
                new FileStream(savePath, FileMode.Create, FileAccess.Write, FileShare.None, 4096);
            while (totalBytesWritten < data.Length)
            {
                var remainingBytes = data.Length - totalBytesWritten;
                var bytesToWrite = Math.Min(chunkSize, remainingBytes);
                await fileStream.WriteAsync(data, totalBytesWritten, bytesToWrite);
                totalBytesWritten += bytesToWrite;

                // 计算并更新写入进度
                var writeProgress = (float)totalBytesWritten / data.Length;
                downloadTracker?.Invoke(new DownloadTracker(0f, 0f, writeProgress));
            }
        }
        catch (IOException ex)
        {
            // 记录或处理文件IO异常
            Debug.LogError($"File write error: {ex.Message}");
        }
        catch (UnauthorizedAccessException ex)
        {
            // 记录或处理权限异常
            Debug.LogError($"Unauthorized access error: {ex.Message}");
        }
        catch (OperationCanceledException)
        {
            // 处理取消操作
            Debug.LogError("Write operation cancelled.");
        }
    }
    
    private float BytesToMB(ulong bytes)
    {
        return (float)bytes / (1024 * 1024);
    }

    public class DownloadTracker
    {
        private readonly float m_TotalSize;
        private readonly float m_CurrentSize;
        private readonly float m_CurrentProgress;

        public DownloadTracker(float totalSize, float currentSize, float currentProgress)
        {
            m_TotalSize = totalSize;
            m_CurrentSize = currentSize;
            m_CurrentProgress = currentProgress;
        }

        /// <summary>
        /// 获取下载进度
        /// </summary>
        /// <returns></returns>
        public float GetProgress()
        {
            return m_CurrentProgress;
        }

        /// <summary>
        /// 获取文件总大小
        /// </summary>
        /// <returns></returns>
        public float GetTotalSize()
        {
            return m_TotalSize;
        }

        /// <summary>
        /// 获取当前下载大小
        /// </summary>
        /// <returns></returns>
        public float GetCurrentSize()
        {
            return m_CurrentSize;
        }
    }

    public static class PathConfig
    {
        public static string ResSavePath(string url)
        {
            return $"{Application.persistentDataPath}/Resources/{ExtractFileNameFromUrl(url)}";
        }

        /// <summary>
        /// 从Url中提取文件名
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        static string ExtractFileNameFromUrl(string url)
        {
            if (string.IsNullOrEmpty(url))
                return string.Empty;

            if (Uri.TryCreate(url, UriKind.Absolute, out var uri))
            {
                // 提取路径部分
                string path = uri.AbsolutePath;
                // 获取最后一个斜杠后的字符串,即文件名
                int lastSlashIndex = path.LastIndexOf('/');
                if (lastSlashIndex >= 0 && lastSlashIndex < path.Length - 1)
                {
                    return path.Substring(lastSlashIndex + 1);
                }
            }

            return string.Empty;
        }
    }
}

5️⃣ 示例代码

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class Test : MonoBehaviour
{
    private Button btn;
    private Downloader m_Downloader;
    public Image img;
    public Text text, debugText, progressText;
    private void Start()
    {
        m_Downloader = new Downloader();
        btn = GetComponent<Button>();
        btn.onClick.AddListener(DownloadFile);
    }
    private void DownloadFile()
    {
        m_Downloader.OnStart(() =>
        {
            Debug.Log("开始下载");
        }).DownloadFileAsync("https://www.baidu.com/img/PCtm_d9c8750bed0b3c7d089fa7d55720d6cf.png",
            (downloadTracker) =>
            {
                Debug.Log((downloadTracker.GetProgress()));
            }).OnComplete(() =>
        {
            Debug.Log("下载完成");
        });
    }

    public void DownloadMultipleFiles()
    {
        var urls = new List<string>()
        {
            "http://example.com/Resource/Video/GameVieo.mp4",
            "http://example.com/Resource/Video/video.mp4"
        };
        m_Downloader.DownloadMultipleFileAsync(urls,
                (currentCount, totalCount) => 
                { 
                    text.text = $"正在下载资源 [{currentCount}/{totalCount}]"; 
                },
                (Tracker) =>
                {
                    img.fillAmount = Tracker.GetProgress();
                    progressText.text = $"[{Tracker.GetCurrentSize():F2}MB/{Tracker.GetTotalSize():F2}MB]-{Mathf.RoundToInt(Tracker.GetProgress() * 100)}%";
                },
                (error) => { Debug.LogError($"下载资源失败:{error}"); })
            .OnComplete(() =>
            {
                debugText.text = "所有资源下载完成";
            });
    }

}

6️⃣ 运行结果

PC 

Android

7️⃣ 更新

# 2024.05.09 

  1. 新增错误回调
  2. 修复下载多个文件时失败也会计数的BUG

# 2024.05.10

  1. 新增开始下载回调方法
  2. 新增异常处理
  3. 新增下载多文件去重
  4. 重构部分代码

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

今天喝水了嘛.

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

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

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

打赏作者

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

抵扣说明:

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

余额充值