Unity资源热更新框架
创始人
2024-05-31 15:13:14
0

什么是热更新?

        游戏上线后,玩家下载第一个版本(1G左右或者更大),在之后运营的过程中,如果需要更换UI显示,或者修改游戏的逻辑,这个时候,如果不使用热更新,就需要重新打包,然后让玩家重新下载,很显然体验非常不好。 热更新可以在不重新下载客户端的情况下,更新游戏的内容。 如王者荣耀,经常有下载补丁的时候。

如何判断哪些文件需要更新?

        为了知道我们需要更新的内容,我们就要知道哪些文件发生了改变,或者新增文件?所以我们需要在本地保存一份 需要热更新文件信息(名称,大小、Md5值)的 配置文件。在添加或改变资源时打新的热更包时我们和这个配置文件进行比较,相同资源名称的Md5值不一致,或者在配置文件中找不到该资源配置,就说明这个资源是发生改变或新增的,需要被加进热更包中。

如何实现热更新?

        上面我们知道了哪些文件需要被热更新,那么我们需要把这些文件放到服务器上,并记录这次补丁包的信息(版本信息、第几次热更,以及这些资源的详细信息配置)。用户打开App后会去拉取这个配置文件,并找到最后一次热更的资源信息与本地的资源进行MD5校验,不通过的就加入到热更列表,下载后保存到本地上,下次进入游戏的时候MD5就校验成功不会在出现重新下载服务器资源的情况,至此我们大致的思路就完成了。

资源热更流程图

打包时记录版本信息及所有资源信息

  • 包名
  • 版本号
  • 资源MD5文件信息(name,文件md5,size)
  • 保存到本地(Xml文件或二进制文件)

一键生成热更资源

如何生成AB包,以及实现: 一键生成热更资源

热更包及配置文件

数据结构:

using System;
using System.Collections.Generic;
using System.Xml.Serialization;namespace Hot
{[Serializable]public class GameVersion{[XmlElement]public ServerVersionInfo[] ServerInfo;}/// /// 当前游戏版本对应的所有补丁/// [Serializable]public class ServerVersionInfo{[XmlAttribute]public string Version;[XmlElement]public List Patches = new List();}/// /// 一个总补丁包信息/// [Serializable]public class Patches{[XmlAttribute]public int Version;        // 第几次热更[XmlAttribute] public string Desc;[XmlElement]public List patches = new List();}/// /// 单个补丁包信息/// [Serializable]public class Patch{[XmlAttribute]public string Name;[XmlAttribute]public string Url;[XmlAttribute]public long Size;[XmlAttribute]public string MD5;}
}

服务器部署

Apache服务器搭建:

我这边使用Apache: Apache Download
下载后将期解压到需要放置的目录下

找到 Apache24/conf/httpd.conf  将 Define SRVROOT改成Apache的解压目录,端口号默认时80,如果被占用可以自行修改

Define SRVROOT "F:\WebServer/Apache24"

 运行 httpd.exe文件,测试可以在浏览器下访问 localhost 可以方位代表成功

服务器文件部署:

在  ...\Apache24\htdocs 文件夹下新建存放需要热更的AssetBundle的文件

 文件夹0.1: 版本文件夹,

文件夹 1: 第一次需要热更的资源

添加在服务器里添加GameVersion.xml文件:对应上面的 ServerInfo数据结构,每次有新的热更包时就往xml里添加 Patche.xml里的内容,需要回退的话只需要删除对应Patches的补丁配置




到这里热更新的准备都已完成,接下来就是实现热更流程

文件下载基类

using System;
using System.Collections;
using System.IO;namespace Hot
{public abstract class DownloadItemBase{protected string url;public string Url => url;protected string fileName;public string FileName => fileName;protected string fileNameWithoutExt;public string FileNameWithoutExt => fileNameWithoutExt;protected string ext;public string Ext;protected string fullName;public string FullName => fullName;protected string fullNameWithoutExt;public string FullNameWithoutExt => fullNameWithoutExt;protected long size;public long Size => size;protected bool isLoading = false;public bool IsLoading = false;public DownloadItemBase(string savePath,string url,long size){isLoading = false;this.url = url;fileNameWithoutExt = Path.GetFileNameWithoutExtension(url);ext = Path.GetExtension(url);fileName = Path.GetFileName(url);fullName = $"{savePath}/{fileName}";fullNameWithoutExt = $"{savePath}/{fileNameWithoutExt}";this.size = size;}public abstract void Destroy();public abstract IEnumerator StartDownload(Action callBack);public abstract float GetCurProgress();}
}

AB包文件下载类

using System;
using System.Collections;
using Core.Utlis;
using UnityEngine;
using UnityEngine.Networking;namespace Hot
{public class ABDownloadItem:DownloadItemBase{private UnityWebRequest webRequest;public ABDownloadItem(string savePath, string url, long size) : base(savePath, url, size){}public override void Destroy(){webRequest.Dispose();}public override IEnumerator StartDownload(Action callBack = null){webRequest = UnityWebRequest.Get(Url);webRequest.timeout = 30;isLoading = true;yield return webRequest.SendWebRequest();if (webRequest.result == UnityWebRequest.Result.Success){FileUtils.SaveFile(FullName, webRequest.downloadHandler.data);if(null != callBack) callBack(true);}else{Debug.LogError($"download {Url} fail err: {webRequest.error}");if(null != callBack) callBack(false);}isLoading = false;}public override float GetCurProgress(){return webRequest != null ? webRequest.downloadProgress : 0;}}
}

核心热更新管理类

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Build;
using Core.Base;
using Core.Utlis;
using UnityEngine;
using UnityEngine.Networking;namespace Hot
{public class HotManager:Singleton{private string ServerGameVersionPath = $"{Application.persistentDataPath}/GameVersion.xml";private string version;private string packageName;private GameVersion gameVersion;private string hotDesc;public string HotDesc => hotDesc;// 服务器上需要热更的补丁包private List serverHotPatches = new List();private Dictionary serverHotPatchDic = new Dictionary();// 需要下载的补丁包private List downLoadPatchs = new List();// 下载完成的补丁包private List alreadyPatchLists = new List();private ABDownloadItem curDownloadItem;private int reloadCount = 0;private float hotAllSize = 0;public float HotAllSize => hotAllSize;private bool isLoading = false;public bool IsLoading => isLoading;// 加载完成回调private Action hotCompeleteHandler;// 加载失败回调private Action> hotFailHandler;private MonoBehaviour corMono;public void Init(MonoBehaviour mono){corMono = mono;}/// /// 检查版本是否需要更新/// /// public void CheckVersionNeedHot(Action callBack){VersionInfo versionInfo = XmlSerializerOpt.Deserialize(PathUtlis.LOCAL_VERSION_PATH);version = versionInfo.Version;packageName = versionInfo.PackageName;corMono.StartCoroutine(LoadServerGameVersion(() =>{// 判断是否需要热更GetServerPatches();CheckDownloadPatches();hotAllSize = serverHotPatches.Sum(x => x.Size);callBack(downLoadPatchs.Count > 0);}));}private IEnumerator LoadServerGameVersion(Action callBack){UnityWebRequest webRequest = UnityWebRequest.Get("http://127.0.0.1/GameVersion.xml");webRequest.timeout = 30;yield return webRequest.SendWebRequest();if (webRequest.result != UnityWebRequest.Result.Success){Debug.LogError($"加载服务器游戏配置失败: {webRequest.error}");}else{Debug.Log(ServerGameVersionPath);if(File.Exists(ServerGameVersionPath)) File.Delete(ServerGameVersionPath);FileUtils.SaveFile(ServerGameVersionPath,webRequest.downloadHandler.data);gameVersion = XmlSerializerOpt.Deserialize(ServerGameVersionPath);}callBack();}private void GetServerPatches(){if (gameVersion != null && gameVersion.ServerInfo != null){for (int i = 0; i < gameVersion.ServerInfo.Length; i++){if (gameVersion.ServerInfo[i].Version == version){List patches = gameVersion.ServerInfo[i].Patches;if (patches != null && patches.Count > 0){serverHotPatches = patches[patches.Count - 1].patches;hotDesc = patches[patches.Count - 1].Desc;}break;}}}}// 检查需要去下载的补丁private void CheckDownloadPatches(){downLoadPatchs.Clear();for (int i = 0; i < serverHotPatches.Count; i++){serverHotPatchDic.Add(serverHotPatches[i].Name, serverHotPatches[i]);AddDownloadPatch(serverHotPatches[i]);}}private void AddDownloadPatch(Patch patch){string savePath = $"{PathUtlis.LocalAssetBundlePath}/{patch.Name}";if (!File.Exists(savePath)){downLoadPatchs.Add(patch);}else{if (patch.MD5 != MD5Utils.GenerateMD5(savePath)){downLoadPatchs.Add(patch);}}}public void StartHot(Action hotCompeleteHandler,Action> hotFailHandler){this.hotCompeleteHandler = hotCompeleteHandler;this.hotFailHandler = hotFailHandler;corMono.StartCoroutine(StartLoad());}private IEnumerator StartLoad(List patches = null){if (patches == null){patches = downLoadPatchs;}if (!Directory.Exists(PathUtlis.LocalAssetBundlePath))Directory.CreateDirectory(PathUtlis.LocalAssetBundlePath);List downloadItems = new List();for (int i = 0; i < patches.Count; i++){downloadItems.Add(new ABDownloadItem(PathUtlis.LocalAssetBundlePath,patches[i].Url,patches[i].Size));}isLoading = true;for (int i = 0; i < downloadItems.Count; i++){ABDownloadItem item = downloadItems[i];curDownloadItem = item;yield return corMono.StartCoroutine(item.StartDownload((success) =>{if (success){Patch patch = FindPatch(item.FileName);if (patch != null){if(!alreadyPatchLists.Contains(patch)) alreadyPatchLists.Add(patch);}}else{Debug.LogError($"{item.FileName} 下载失败,尝试重新下载");}item.Destroy();}));}// 重新比较文件md5,避免文件下载失败yield return VerifyMD5(downLoadPatchs);}//校验下载后的文件private IEnumerator VerifyMD5(List patches){List downPatchList = new List();for (int i = 0; i < patches.Count; i++){Patch patch = patches[i];string savePath = $"{PathUtlis.LocalAssetBundlePath}/{patch.Name}";if (!File.Exists(savePath)){downPatchList.Add(patch);}else{if (patch.MD5 != MD5Utils.GenerateMD5(savePath)){downPatchList.Add(patch);}}}if (downPatchList.Count > 0){reloadCount++;if (reloadCount < 5){yield return corMono.StartCoroutine(StartLoad(downPatchList));}else{isLoading = false;if (null != hotFailHandler) hotFailHandler(downPatchList);}}else{isLoading = false;if (null != hotCompeleteHandler) hotCompeleteHandler();}}private Patch FindPatch(string name){Patch patch = null;serverHotPatchDic.TryGetValue(name, out patch);return patch;}public float GetProgress(){float loadedSize = alreadyPatchLists.Sum(x => x.Size);float curloadSize = curDownloadItem.GetCurProgress() * curDownloadItem.Size;float progress = (loadedSize + curloadSize) / hotAllSize;return progress;}}
}

UI测试效果图

还有个问题,这样下载下来的资源会直接被别人拿走使用,为了数据的安全,我们可以对资源进行加密处理,我使用的是AES,也没有什么难点,就是在一键生成AB包后使用AES对文件加密,然后加载资源的时候使用 字节数组加载,LoadFromMemory的缺点就是多占一份没存,对于没存吃紧的就不适合用了,或者参考:Unity3D加密Assetbundle(不占内存)

    private void DecryptAssetBundle(){string abPath = Path.Combine(PathUtlis.AssetBundlePath, path);// 解密被加载的AB包byte[] result = AESUtils.AESFileDecryptToByte(abPath,"ENCRYPT_KEY");if (result == null){Debug.LogError($"AES Decrypt {abPath} file fail");  return;}AssetBundle asset = AssetBundle.LoadFromMemory(result);}

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
一帆风顺二龙腾飞三阳开泰祝福语... 本篇文章极速百科给大家谈谈一帆风顺二龙腾飞三阳开泰祝福语,以及一帆风顺二龙腾飞三阳开泰祝福语结婚对应...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
美团联名卡审核成功待激活(美团... 今天百科达人给各位分享美团联名卡审核成功待激活的知识,其中也会对美团联名卡审核未通过进行解释,如果能...