游戏上线后,玩家下载第一个版本(1G左右或者更大),在之后运营的过程中,如果需要更换UI显示,或者修改游戏的逻辑,这个时候,如果不使用热更新,就需要重新打包,然后让玩家重新下载,很显然体验非常不好。 热更新可以在不重新下载客户端的情况下,更新游戏的内容。 如王者荣耀,经常有下载补丁的时候。
为了知道我们需要更新的内容,我们就要知道哪些文件发生了改变,或者新增文件?所以我们需要在本地保存一份 需要热更新文件信息(名称,大小、Md5值)的 配置文件。在添加或改变资源时打新的热更包时我们和这个配置文件进行比较,相同资源名称的Md5值不一致,或者在配置文件中找不到该资源配置,就说明这个资源是发生改变或新增的,需要被加进热更包中。
如何实现热更新?
上面我们知道了哪些文件需要被热更新,那么我们需要把这些文件放到服务器上,并记录这次补丁包的信息(版本信息、第几次热更,以及这些资源的详细信息配置)。用户打开App后会去拉取这个配置文件,并找到最后一次热更的资源信息与本地的资源进行MD5校验,不通过的就加入到热更列表,下载后保存到本地上,下次进入游戏的时候MD5就校验成功不会在出现重新下载服务器资源的情况,至此我们大致的思路就完成了。
如何生成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);}