项目中没有使用lua脚本,所以热更只能资源更新,一旦逻辑代码上有改变,需要玩家自行更新游戏客户端.
本人把公司项目中的更新模块精简了下,还是画张流程图解释:
一般资源打入安装包有3种情况:
1.安装包没有资源,进入游戏后资源全部从资源服务器下载.
(包体最小 ,但第一次进入游戏耗时最长)
2.安装包有部分资源,剩余的资源还是从服务器下载.
(包体介于1-3之间,耗时也介于1-3之间)
3.安装包包含所有资源.
(包体最大,耗时最短)
所以在资源更新前先判断了是否第一次进入游戏,是否需要做解压缩的操作.
另外为了让逻辑更清晰版本对比和资源更新分开写成了2个单例类.
不扯淡直接干 ̄へ ̄:
1.版本对比管理器逻辑:
获取该文件使用get方式,unity自带有类UnityWebRequest,他专门操作web请求.当然用www也是可以的,不过貌似官方要逐渐舍弃掉www?
(只要能取下文件就行方法不重要,小哥写代码从来都是复制粘贴稳得很 ✧(≖ ◡ ≖✿ )
获取直接使用Application.version
版本号对比也不用傻不拉几的手写实现,c#库中已经提供了这玩意儿(System.Version类)为什么不用呢?
版本管理器类
UpdateVersionManager.cs
public class UpdateVersionManager:MonoSingleton
{
private System.Version curVersion;
private System.Version onlineVersion;
public bool IsNeedUpdate;
public void CheckVersion(UnityAction onComplate = null) {
IsNeedUpdate = false;
StartCoroutine(progress(onComplate));
}
IEnumerator progress(UnityAction onComplate) {
//拉取服务器版本
MsgDispatcher.GetInstance().Fire(GameEvents.Msg_ShowLoadingContent, "检测版本号...");
UnityWebRequest req = UnityWebRequest.Get(GameConfigs.ServerVersionUrl);
yield return req.SendWebRequest();
if(req.isHttpError || req.isNetworkError) {
Debug.LogError(req.error);
yield break;
}
onlineVersion = new System.Version(req.downloadHandler.text);
curVersion = new System.Version( Application.version);
if(onlineVersion != curVersion) {
Debug.LogFormat("当前版本不是最新版本({0}),请及时更新到最新版本({1})",curVersion,onlineVersion);
IsNeedUpdate = true;
}
Debug.Log("版本检测完成!");
if (onComplate != null) {
onComplate(IsNeedUpdate);
}
yield return null;
}
}
2.资源更新逻辑:
解压缩安装包数据(第一次) -> 加载本地ab根目录下的manifest文件,该文件包含所有资源的信息和hash值 ->通过UnityWebRequest获取资源服务器上ab根目录的manifest文件,并加载他
->对比本地和资源的每个文件hash值,只要有hash不同的或者本地没有的统统放入下载列表中 -> 根据下载列表下载资源 ->全部下载完成,更新本地manifset文件.
资源更新管理类
UpdateAssetManager.cs
public class UpdateAssetManager:MonoSingleton
{
private AssetBundleManifest curManifest;
private AssetBundleManifest onlineManifest;
public void CheckAsset(UnityAction onComplete =null) {
MsgDispatcher.GetInstance().Fire(GameEvents.Msg_ShowLoadingContent, "检测资源...");
StartCoroutine(progress(onComplete));
}
IEnumerator progress(UnityAction onComplete) {
//第一次进入游戏 把streamingassets文件夹数据解压缩到指定的下载目录
if(true || PlayerPrefs.GetString("IsFirstLaunch","true") == "true") {
yield return StartCoroutine(streamingAssetfolderCopyToDownloadFolder());
}
// 加载本地 manifest文件
if (File.Exists(GameConfigs.LocalManifestPath)) {
var manifestAB = AssetBundle.LoadFromFile(GameConfigs.LocalManifestPath);
curManifest = manifestAB.LoadAsset("AssetBundleManifest");
manifestAB.Unload(false);
} else {
Debug.Log("本地资源文件丢失:" + GameConfigs.LocalManifestPath);
}
//获取资源服务器端manifest
Debug.Log("获取资源服务器资源manifest :"+ GameConfigs.OnlineManifestPath);
MsgDispatcher.GetInstance().Fire(GameEvents.Msg_ShowLoadingContent, "检测是否更新资源...");
UnityWebRequest webReq = UnityWebRequest.Get(GameConfigs.OnlineManifestPath);
yield return webReq.SendWebRequest();
if (webReq.isNetworkError || webReq.isHttpError) {
Debug.Log(webReq.error);
} else {
if(webReq.responseCode == 200) {
byte[] result = webReq.downloadHandler.data;
AssetBundle onlineManifestAB = AssetBundle.LoadFromMemory(result);
onlineManifest = onlineManifestAB.LoadAsset("AssetBundlemanifest");
onlineManifestAB.Unload(false);
//更新本地manifest
writeFile(GameConfigs.LocalManifestPath, webReq.downloadHandler.data);
}
yield return StartCoroutine(download());
if (onComplete != null) {
onComplete();
}
}
}
// streamingAsset文件夹数据解压缩到下载文件夹
IEnumerator streamingAssetfolderCopyToDownloadFolder() {
Debug.Log("初次运行,解压缩包数据到本地下载文件夹!");
MsgDispatcher.GetInstance().Fire(GameEvents.Msg_ShowLoadingContent, "解压缩包数据...");
string srcmanifestpath = GameConfigs.StreamingAssetManifestPath;
if (Directory.Exists(GameConfigs.GameResExportPath)) {
Debug.Log("存在:" + GameConfigs.GameResExportPath);
//获取该文件夹下所有文件(包含子文件夹)
var list = PathUtils.GetFilesPath(GameConfigs.GameResExportPath);
int total = list.Length;
int count = 0;
foreach (var iter in list) {
string srcPath = iter;
string tarPath = iter.Replace(GameConfigs.GameResExportPath, GameConfigs.LocalABRootPath);
UnityWebRequest req = UnityWebRequest.Get(srcmanifestpath);
yield return req.SendWebRequest();
if (req.isNetworkError || req.isHttpError) {
Debug.Log(req.error);
} else {
if (File.Exists(tarPath)) {
File.Delete(tarPath);
} else {
PathUtils.CreateFolderByFilePath(tarPath);
}
FileStream fs2 = File.Create(tarPath);
fs2.Write(req.downloadHandler.data, 0, req.downloadHandler.data.Length);
fs2.Flush();
fs2.Close();
Debug.LogFormat("->解压缩文件{0}到{1}成功", srcPath, tarPath);
}
count++;
MsgDispatcher.GetInstance().Fire(GameEvents.Msg_DownloadProgress, string.Format("解压缩包数据...({0}/{1})",count,total));
}
} else {
Debug.Log("无需解压缩!");
}
}
IEnumerator download() {
var downloadFileList = getDownloadFileName();
int totalCount = downloadFileList.Count;
int count = 0;
if (totalCount <= 0) {
Debug.Log("没有需要更新的资源");
} else {
foreach (var iter in downloadFileList) {
string path = GameConfigs.ResServerUrl + "/" + GameConfigs.CurPlatformName + "/" + iter;
UnityWebRequest req = UnityWebRequest.Get(path);
yield return req.SendWebRequest();
if (req.isNetworkError) {
Debug.Log(req.error);
yield return null;
} else {
if (req.responseCode == 200) {
byte[] result = req.downloadHandler.data;
//save file
string downloadPath = GameConfigs.LocalABRootPath + "/" + iter;
writeFile(downloadPath, result);
Debug.LogFormat("写入:{0} 成功 -> {1} | len =[{2}]", path, downloadPath, result.Length);
AssetBundle onlineManifestAB = AssetBundle.LoadFromMemory(result);
onlineManifest = onlineManifestAB.LoadAsset("AssetBundlemanifest");
onlineManifestAB.Unload(false);
}
}
count++;
MsgDispatcher.GetInstance().Fire(GameEvents.Msg_DownloadProgress, string.Format("下载资源...({0}/{1})", count, totalCount));
yield return new WaitForEndOfFrame();
}
}
}
//获取需要下载的文件列表
private List getDownloadFileName() {
if(curManifest == null) {
if(onlineManifest == null) {
return new List();
} else {
return new List(onlineManifest.GetAllAssetBundles());
}
}
List tempList = new List();
var curHashCode = curManifest.GetHashCode();
var onlineHashCode = onlineManifest.GetHashCode();
if (curHashCode != onlineHashCode) {
// 比对筛选
var curABList = curManifest.GetAllAssetBundles();
var onlineABList = onlineManifest.GetAllAssetBundles();
Dictionary curABHashDic = new Dictionary();
foreach(var iter in curABList) {
curABHashDic.Add(iter, curManifest.GetAssetBundleHash(iter));
}
foreach(var iter in onlineABList) {
if (curABHashDic.ContainsKey(iter)) { //本地有该文件 但与服务器不同
Hash128 onlineHash = onlineManifest.GetAssetBundleHash(iter);
if(onlineHash != curABHashDic[iter]) {
tempList.Add(iter);
}
} else {
tempList.Add(iter);
}
}
}
return tempList;
}
private void writeFile(string path,byte[] data) {
FileInfo fi = new FileInfo(path);
DirectoryInfo dir = fi.Directory;
if (!dir.Exists) {
dir.Create();
}
FileStream fs = fi.Create();
fs.Write(data, 0, data.Length);
fs.Flush();
fs.Close();
}
}
在运行上面流程之前,我们需要:
1.把资源打成ab包和ab包的使用请参见:https://blog.csdn.net/hl1991825/article/details/84327622
2.搭建一个本地资源服务器(云服务器大善!).
我使用的xampp,一键安装方便.安装好后启动Apache服务(浏览器访问下127.0.0.1 可以看看本地web端口是否开启):
然后把ab包资源放入这个文件夹(具体地址按照你的xampp安装目录来):
整个ResServer文件夹应该是这样,对应平台就看项目需求了:
大功告成,来写个测试类运行:
Launcher.cs
public class Launcher : MonoBehaviour ,IMsgReceiver{
public Text Content;
public Image Img;
public Button Btn;
public GameObject MsgBox;
void OnEnable() {
MsgDispatcher.GetInstance().Subscribe(GameEvents.Msg_ShowLoadingContent, this);
MsgDispatcher.GetInstance().Subscribe(GameEvents.Msg_DownloadProgress, this);
MsgDispatcher.GetInstance().Subscribe(GameEvents.Msg_DownloadFinish, this);
}
void OnDisable() {
MsgDispatcher.GetInstance().UnSubscribe(GameEvents.Msg_ShowLoadingContent, this);
MsgDispatcher.GetInstance().UnSubscribe(GameEvents.Msg_DownloadProgress, this);
MsgDispatcher.GetInstance().UnSubscribe(GameEvents.Msg_DownloadFinish, this);
}
// Use this for initialization
void Start () {
Btn.gameObject.SetActive(false);
Img.gameObject.SetActive(false);
MsgBox.SetActive(false);
Btn.onClick.AddListener(onClickedBtn);
UpdateVersionManager.Instance.CheckVersion((bool needUpdate) => {
if (needUpdate) {
MsgBox.SetActive(true);
} else {
UpdateAssetManager.Instance.CheckAsset(() => {
MsgDispatcher.GetInstance().Fire(GameEvents.Msg_DownloadFinish);
});
}
});
}
public bool OnMsgHandler(string msgName, params object[] args) {
switch (msgName) {
case GameEvents.Msg_ShowLoadingContent:
Content.text = (string)args[0];
break;
case GameEvents.Msg_DownloadProgress:
Content.text = (string)args[0];
break;
case GameEvents.Msg_DownloadFinish: {
AssetManager.Instance.InitMode(GameConfigs.LoadAssetMode);
Content.text = "资源更新完成";
Btn.gameObject.SetActive(true);
Img.gameObject.SetActive(true);
}
break;
}
return true;
}
void onClickedBtn() {
AssetManager.Instance.LoadAssetAsync(GameConfigs.GetSpriteAtlasPath("ui_atlas"), (SpriteAtlas sp) => {
Sprite p = sp.GetSprite("icon_2");
Img.sprite = sp.GetSprite(string.Format("icon_{0}",Random.Range(0,sp.spriteCount-1)));
});
}
}
效果:
注意:
1.跨平台路径问题 , StreamingAssetPath , DataPath , PersisentDataPath , TempDataPath ...
这篇大牛文章讲得非常清除:https://www.cnblogs.com/murongxiaopifu/p/4199541.html
2.资源服务器的地址是直接写入代码的,最好从服务端获取.
3.资源更新和版本号无关了,无论是大版本差异还是小版本差异,反正有差异我就下(大版本直接提醒更新客户端了都,而且这样也不用操心跨版本资源下载的问题,简单暴力)
4.消息派发瞅瞅这个:https://blog.csdn.net/hl1991825/article/details/84112869
移动端仍旧没有测试,码字已经掏空我的热情,请自行测试吧.
以上代码只起抛砖引玉的作用,欢迎各位看官把玉借我瞅瞅 ( ̄▽ ̄)~*
项目工程(unity2017):https://pan.baidu.com/s/1eiySubZm4ZyK8ux8DYYSkA
提取码:d9rx