U3d 资源做热更新

我这里实现了3个文件来处理热更新:

Logo.cs 游戏的启动文件,里面处理下载存放资源路径的初始化。下载列表检查,下载,进入游戏

DownLoader.cs U3d的一个组件,里面包含具体的下载线程,用来处理开启多线程下载

HttpDownLoad.cs 具体的文件下载类,线程运行下载,支持断点下载

 

/* Logo.cs
 * Created By Zhaotao On 2019-4-10
 * Desc:游戏启动文件,处理更新流程,资源加载
 */

using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using DigiskyUnity;
using UnityEngine;


public class Logo : MonoBehaviour
{
    [SerializeField]
    private bool _UseHotUpdate = false;
    
    [SerializeField]
    private string _ResServerHttp = "http://127.0.0.1:8001/";

    private bool _FileListDownFinish = false;

    private ResManager _ResMgr = null;

    private LogoState _CurState;
    
    private Splash _SplashCom = null;

    private const string _VerFileName = "ver.txt";

    private const string _FileListName = "files_list.txt";
    
    private Dictionary _OldDict = new Dictionary();
    private Dictionary _ServerDict = new Dictionary();

    private long _DownByteSize = 0;
    private long _FinishByteSize = 0;

    private DownLoader _DownLoader;

    private int _TotalDownloadCount = 0;

    private string _ServerVer = "";

    private bool _NeedSaveFileList = false;
    
    private enum LogoState
    {
        INIT,
        INIT_FINISH,
        CHECK_UPDATE_LIST,
        CHECK_UPDATE_LIST_FINISH,
        DOWN_FILE,
        DOWN_FILE_FINISH,
        PRE_RES,
        PRE_RES_FINISH,
        ENTER_GAME,
    }
    // Start is called before the first frame update
    void Start()
    {
        _CurState = LogoState.INIT;
        Init();
        _CurState = LogoState.INIT_FINISH;
    }

    // Update is called once per frame
    void Update()
    {
        StartCoroutine(StartServer());
    }

    void Init()
    {    
        //一开始就加载Resmanager ,初始化资源路径
        _ResMgr = this.gameObject.AddComponent();
        _ResMgr.SetBundleFolder(new string[]{"StreamingAssets"});
    }

    //检查更新列表
    IEnumerator CheckUpdateFileList()
    {
        _CurState = LogoState.CHECK_UPDATE_LIST;
        
        _SplashCom.SetDesc("检查文件更新...");
        WWW www = new WWW(_ResServerHttp+_VerFileName);
        yield return www;
        _ServerVer = www.text;
        _CurState = LogoState.CHECK_UPDATE_LIST_FINISH;
        string resRootPath = ResManager.GetResPath();
        
        if (File.Exists(resRootPath + "/" + _VerFileName))
        {
            StreamReader sr = new StreamReader(resRootPath + "/" + _VerFileName);
            string oldVer = sr.ReadToEnd();
            sr.Close();
            if (www.text != oldVer)
            {
                //需要检查每个文件
                _SplashCom.SetDesc("整理下载文件...");
                www = new WWW(_ResServerHttp+_FileListName);
                yield return www;
                //读取包里面的资源版本信息
                sr = new StreamReader(resRootPath + "/" + _FileListName);
                string line = sr.ReadLine();
                while (line != null)
                {
                    string[] patt = line.Split('|');
                    if (patt.Length < 3)
                    {
                        continue;
                    }
                    UpdateFileInfo info = new UpdateFileInfo(patt[0].Trim(),patt[1].Trim(),int.Parse(patt[2]));
                    _OldDict.Add(patt[0].Trim(),info);
                    line = sr.ReadLine();
                }
                sr.Close();
                yield return new WaitForEndOfFrame();
                //解析服务器上的最新资源信息
                string[] fileInfos = www.text.Split('\n');
                foreach (var it in fileInfos)
                {
                    string[] patt = it.Split('|');
                    if (patt.Length < 3)
                    {
                        continue;
                    }
                    UpdateFileInfo info = new UpdateFileInfo(patt[0].Trim(),patt[1].Trim(),int.Parse(patt[2]));
                    _ServerDict.Add(patt[0].Trim(),info);
                }
                yield return new WaitForEndOfFrame();
                //做比对
                Dictionary downList = new Dictionary();
                foreach (var it in _ServerDict)
                {
                    if (!_OldDict.ContainsKey(it.Key) || _OldDict[it.Key].md5 != it.Value.md5)
                    {
                        downList.Add(_ResServerHttp + it.Key,resRootPath+"/"+it.Key);
                        _DownByteSize += it.Value.size;
                    }
                }

                _TotalDownloadCount = downList.Keys.Count;
                if (_TotalDownloadCount > 0)
                {
                    StartDownloadFiles(downList);
                }
                else
                {
                    _CurState = LogoState.DOWN_FILE_FINISH;
                    SaveNewVerFile();
                }
                
            }
            else
            {
                _CurState = LogoState.DOWN_FILE_FINISH;
            }
        }
        else
        {
            //全部下载
            //创建版本好文件
            File.Create(resRootPath + "/" + _VerFileName).Close();
            
            _SplashCom.SetDesc("整理下载文件...");
            www = new WWW(_ResServerHttp + "/" + _FileListName);
            yield return www;
            //解析服务器上的最新资源信息
            string[] fileInfos = www.text.Split('\n');
            foreach (var it in fileInfos)
            {
                
                string[] patt = it.Split('|');
                if (patt.Length < 3)
                {
                    continue;
                }
                    
                UpdateFileInfo info = new UpdateFileInfo(patt[0].Trim(),patt[1],int.Parse(patt[2]));
                _ServerDict.Add(patt[0].Trim(),info);
            }
            
            yield return new WaitForEndOfFrame();
            //做比对
            Dictionary downList = new Dictionary();
            foreach (var it in _ServerDict)
            {
                if (!_OldDict.ContainsKey(it.Key) || _OldDict[it.Key].md5 != it.Value.md5)
                {
                    downList.Add(_ResServerHttp + it.Key,resRootPath+"/"+it.Key);
                    _DownByteSize += it.Value.size;
                }
            }

            _TotalDownloadCount = downList.Keys.Count;
            if (_TotalDownloadCount > 0)
            {
                StartDownloadFiles(downList);
            }
            else
            {
                _CurState = LogoState.DOWN_FILE_FINISH;
                SaveNewVerFile();
            }
        }
    }

    void StartDownloadFiles(Dictionary fileList)
    {
        string desc = string.Format("总进度:{0}/{1}({2}/{3})", 0, _TotalDownloadCount,
            0, _DownByteSize);
        SetSplashDesc(desc);
        _CurState = LogoState.DOWN_FILE;
        _DownLoader = this.gameObject.AddComponent();
        _DownLoader.SetThreadCount(3);
        _DownLoader.StartDownload(fileList,DownloadFinishCallBack);
    }

    void DownloadFinishCallBack(DownLoader.ProgressInfo info)
    {
        foreach (var it in info.FinishFiles)
        {
            string fileName = it.Replace(_ResServerHttp, "");
            Debug.Log("Download Finish:"+fileName);
            _FinishByteSize += _ServerDict[fileName].size;
            //记录下载完的文件,防止退出游戏记录失败
            UpdateFileInfo tmp = new UpdateFileInfo();

            tmp.fileName = _ServerDict[fileName].fileName;
            tmp.md5 = _ServerDict[fileName].md5;
            tmp.size = _ServerDict[fileName].size;
            if (_OldDict.ContainsKey(fileName))
            {
                _OldDict[fileName] = tmp;
            }
            else
            {
                _OldDict.Add(fileName,tmp);
            }

            _NeedSaveFileList = true;
        }
                
        if (info.AllFinish)
        { 
            SaveDownFileList();
            SaveNewVerFile();
            _DownLoader.RemoveListener();
            _CurState = LogoState.DOWN_FILE_FINISH;
        }
    }

    void SaveDownFileList()
    {
        Debug.Log("SaveDownFileList");
        string resRootPath = ResManager.GetResPath();
        StreamWriter sw = new StreamWriter(resRootPath + "/" + _FileListName, false, Encoding.UTF8);
        foreach (var it in _OldDict)
        {
            sw.WriteLine(it.Value.fileName + "|" + it.Value.md5 + "|" + it.Value.size);
        }

        sw.Close();
        sw.Dispose();
        _NeedSaveFileList = false;
        
    }

    void SaveNewVerFile()
    {

        string resRootPath = ResManager.GetResPath();
        StreamWriter sw = new StreamWriter(resRootPath + "/" + _VerFileName, false, Encoding.UTF8);
        sw.Write(_ServerVer);
        sw.Close();
        sw.Dispose();
        
    }
    
    //开始流程服务
    IEnumerator StartServer()
    {
        if (_UseHotUpdate && _CurState == LogoState.INIT_FINISH)
        {
            OpenSplashScreen();
            yield return CheckUpdateFileList();
            
            yield return new WaitForEndOfFrame();      
        }
        else if(!_UseHotUpdate && _CurState < LogoState.DOWN_FILE_FINISH)
        {
            _CurState = LogoState.DOWN_FILE_FINISH;
        }

        if (_CurState == LogoState.DOWN_FILE)
        {
            //更新进度条信息
            if (_DownLoader != null)
            {
                Dictionary dict = _DownLoader.GetDownloadingFileInfo();
                float subByte = 0;
                StringBuilder subPerStr = new StringBuilder();
                foreach (var it in dict)
                {
                    string fileName = it.Key.Replace(_ResServerHttp, "");
                    subPerStr.Append(fileName);
                    subPerStr.Append("\t");
                    subPerStr.AppendFormat("{0}%\n",it.Value.progress * 100);

                    subByte += _ServerDict[fileName].size * it.Value.progress;
                }

                subPerStr.AppendFormat("总进度:{0}/{1}({2}/{3})", _DownLoader.FinishCount(), _TotalDownloadCount,
                    _FinishByteSize +subByte, _DownByteSize);
                SetSplashDesc(subPerStr.ToString());
                SetSplashProgress((_FinishByteSize +subByte)/_DownByteSize);
            }
        }

        if ( _CurState == LogoState.DOWN_FILE_FINISH)
        {
            if (_DownLoader != null)
            {
                Destroy(_DownLoader);
                _DownLoader = null;
            }

            SetSplashDesc("下载完成");
            SetSplashProgress(1);
            _CurState = LogoState.PRE_RES;
            yield return new WaitForSeconds(1);
            _ResMgr.OnPreInit();
            _CurState = LogoState.PRE_RES_FINISH;
        }

        if (_CurState == LogoState.PRE_RES_FINISH)
        {
            if (_SplashCom != null)
            {
                Destroy(_SplashCom.gameObject);
                _SplashCom = null;
            }
            this.gameObject.AddComponent();
            _CurState = LogoState.ENTER_GAME;
  
        }
    }
    
    //设置SplashScreen 进度条描述
    private void SetSplashDesc(string desc)
    {
        if (_SplashCom != null)
        {
            _SplashCom.SetDesc(desc);
        }
    }

    //设置SplashSreen 的进度条值[0-1]
    private void SetSplashProgress(float value)
    {
        if (_SplashCom != null)
        {
            _SplashCom.SetProgressValue(value);
        }
    }

    private void OpenSplashScreen()
    {
        GameObject obj = Instantiate(Resources.Load("SplashWnd"));
        _SplashCom = obj.GetComponent();
        _SplashCom.SetProgressValue(0);
    }
    
    private struct UpdateFileInfo
    {
        public string fileName;
        public string md5;
        public int size;

        public UpdateFileInfo(string name, string m, int s)
        {
            fileName = name;
            md5 = m;
            size = s;
        }
    }

    //应用Home出去的时候
    private void OnApplicationPause(bool pauseStatus)
    {
        if (pauseStatus && _NeedSaveFileList)
        {
            SaveDownFileList();
        }
    }

    //应该关闭的时候
    private void OnApplicationQuit()
    {
        if (_NeedSaveFileList)
        {
            SaveDownFileList();
        }
    }
}
/* DownLoader.cs
 * Created By Zhaotao On 2019-4-09
 * Desc:资源下载器
 */

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;

public class DownLoader : MonoBehaviour
{
    //同时开启的下载线程数量
    [SerializeField]
    private int _ThreadCount = 1;
    //下载文件列表
    private Dictionary _FilesDict = null;
    //下载器
    private HttpDownLoad[] _Loader = null;
    
    private DownLoaderCallBack _onDownFinishCallBack = new DownLoaderCallBack();
    
    private static object _sLockCallback = new  object();

    private int _FinishCount = 0;

    private float _TotalMemery = 0;

    private DownLoadState _CurState = DownLoadState.NONE;

    private int _CurDownIndex = -1;
    
    private Dictionary _DownloadingFilePerDict = new Dictionary();
    
    private enum DownLoadState
    {
        NONE,
        NOTHREAD,
        READY,
        DOWNLOADING,
        FINISH,
    }
    
    void Init()
    {
        if (_ThreadCount <= 0)
        {
            _CurState = DownLoadState.NOTHREAD;
            return;
        }
        _Loader = new HttpDownLoad[_ThreadCount];
        for (int i = 0; i < _Loader.Length; ++i)
        {
            _Loader[i] = new HttpDownLoad();
        }

        _CurState = DownLoadState.READY;
    }

    public Dictionary GetDownloadingFileInfo()
    {
        return _DownloadingFilePerDict;
    }

    private void OnDisable()
    {
        for (int i = 0; i < _Loader.Length; ++i)
        {
            _Loader[i].Close();
        }
    }

    /// 
    /// 开始下载
    /// 
    /// 下载的文件列表
    /// 下载的进度回调
    public void StartDownload( Dictionary dict,UnityAction fileFinishCallBack)
    {
        Init();
        _DownloadingFilePerDict.Clear();
        if (_CurState == DownLoadState.NOTHREAD)
        {
            throw new Exception("No Thread Count,Thread Count must >= 1");
        }
        _FilesDict = dict;
        _onDownFinishCallBack.AddListener(fileFinishCallBack);
        int fileCount = _FilesDict.Keys.Count;
        if (_CurState == DownLoadState.READY)
        {
            _CurState = DownLoadState.DOWNLOADING;
            for (int i = 0; i < _Loader.Length; ++i)
            {
                _CurDownIndex++;
                if (_CurDownIndex >= fileCount && _DownloadingFilePerDict.Keys.Count > 0 )
                {
                    //确定其它线程完成             
                    return;
                }else if (_CurDownIndex >= fileCount && _DownloadingFilePerDict.Keys.Count == 0)
                {
                    _CurState = DownLoadState.FINISH;
                    return;
                }
                string url = _FilesDict.Keys.ToArray()[_CurDownIndex];
                string fileName = _FilesDict[url];
                HttpDownLoad curLoader = _Loader[i];
                curLoader.DownLoad(url,fileName,HttpDownloadCallBack);
                _DownloadingFilePerDict.Add(url,curLoader);
            }
        }
    }

    void HttpDownloadCallBack(object obj)
    {
       
        lock (_sLockCallback)
        {
            HttpDownLoad curLoader = obj as HttpDownLoad;
            _DownloadingFilePerDict.Remove(curLoader.Url);
            _FinishCount++;
            
            int totalCount = _FilesDict.Keys.Count;
            ProgressInfo info = new ProgressInfo(_FinishCount,totalCount == _FinishCount);
            info.FinishFiles.Add(curLoader.Url);
            _onDownFinishCallBack.Invoke(info);
            
            _CurDownIndex++;
            if (_CurDownIndex >= totalCount && _DownloadingFilePerDict.Keys.Count > 0)
            {
                //等到其它线程的完成
                return;
            }else if (_CurDownIndex >= totalCount && _DownloadingFilePerDict.Keys.Count == 0)
            {
                _CurState = DownLoadState.FINISH;
                return;
            }
            
            string url = _FilesDict.Keys.ToArray()[_CurDownIndex];
 
            string fileName = _FilesDict[url];        
            curLoader.DownLoad(url,fileName,HttpDownloadCallBack);

            _DownloadingFilePerDict.Add(url,curLoader);

        }
    }

    public void RemoveListener()
    {
        _onDownFinishCallBack.RemoveAllListeners();
    }

    public void SetThreadCount(int c)
    {
        _ThreadCount = c;
    }

    public int FinishCount()
    {
        return _FinishCount;
    }
    
    public struct ProgressInfo
    {
        //全部下载完的文件数量
        public int FinishCount;
        //当前此帧完成的文件
        public List FinishFiles;
        //是否全部下载完成
        public bool AllFinish;

        public ProgressInfo(int finishCount,bool allFinish)
        {
            FinishCount = finishCount;
            FinishFiles = new List();
            AllFinish = allFinish;
        }
    }
    
    private class DownLoaderCallBack : UnityEvent
    {
        
    }
}
/* HttpDownLoad.cs
 * Created By Zhaotao On 2019-4-08
 * Desc:文件斷點下載
 */

using System.Threading;
using System.IO;
using System.Net;
using System;
using JetBrains.Annotations;

/// 
/// 通过http下载资源
/// 
public class HttpDownLoad
{
    //下载进度
    public float progress { get; private set; }

    //涉及子线程要注意,Unity关闭的时候子线程不会关闭,所以要有一个标识
    private bool isStop;

    //子线程负责下载,否则会阻塞主线程,Unity界面会卡主
    private Thread thread;

    //表示下载是否完成
    public bool isDone { get; private set; }

    //下载链接
    public string Url { get; private set; }
    /// 
    /// 文件下载
    /// 
    /// 下载链接
    /// 存储地址
    /// 回调函数
    public void DownLoad(string url, string saveFile, [CanBeNull] Action callBack)
    {
        isStop = false;
        Url = url;
        progress = 0;
        //开启子线程下载,使用匿名方法
        thread = new Thread(delegate()
        {
            //判断保存路径是否存在
            string filePath = Path.GetDirectoryName(saveFile);
            if (!Directory.Exists(filePath))
            {
                Directory.CreateDirectory(filePath);
            }
            //使用流操作文件
            FileStream fs = new FileStream(saveFile, FileMode.OpenOrCreate, FileAccess.Write);
            //获取文件现在的长度
            long fileLength = fs.Length;
            //获取下载文件的总长度
            long totalLength = GetLength(url);

            //如果没下载完
            if (fileLength < totalLength)
            {
                //断点续传核心,设置本地文件流的起始位置
                fs.Seek(fileLength, SeekOrigin.Begin);

                HttpWebRequest request = HttpWebRequest.Create(url) as HttpWebRequest;
                
                //断点续传核心,设置远程访问文件流的起始位置
                request.AddRange((int) fileLength);
                Stream stream = request.GetResponse().GetResponseStream();

                byte[] buffer = new byte[1024];
                //使用流读取内容到buffer中
                //注意方法返回值代表读取的实际长度,并不是buffer有多大,stream就会读进去多少
                int length = stream.Read(buffer, 0, buffer.Length);
                while (length > 0)
                {
                    //如果Unity客户端关闭,停止下载
                    if (isStop) break;
                    //将内容再写入本地文件中
                    fs.Write(buffer, 0, length);
                    //计算进度
                    fileLength += length;
                    progress = (float) fileLength / (float) totalLength;
                    
                    //类似尾递归
                    length = stream.Read(buffer, 0, buffer.Length);
                }

                stream.Close();
                stream.Dispose();
            }
            else
            {
                progress = 1;
            }

            fs.Close();
            fs.Dispose();
            //如果下载完毕,执行回调
            if (progress == 1)
            {
                isDone = true;
                if (callBack != null) callBack(this);
            }
        });
        //开启子线程
        thread.IsBackground = true;
        thread.Start();
    }


    /// 
    /// 通过url 获取文件大小
    /// 
    /// 文件URL
    /// 
    long GetLength(string url)
    {
        HttpWebRequest requet = HttpWebRequest.Create(url) as HttpWebRequest;
        requet.Method = "HEAD";
        HttpWebResponse response = requet.GetResponse() as HttpWebResponse;
        return response.ContentLength;
    }

    /// 
    /// 退出游戏或者关闭gameobject的时候调用
    /// 
    public void Close()
    {
        isStop = true;
    }
}

这些文件里面,包含了一些项目工程使用的代码,比如自己的Splash 屏的进度显示等等。但不妨碍看下载功能。

你可能感兴趣的:(Unity3d,U3d,热更新,断点下载)