本篇主要分享基于热更新的游戏初始化方案。

整体初始化的流程大致为:检查是否需要解压资需要则解压,之后再检查是否存在需要热更新的资源文件需要则更新,更新完成后则初始化结束可进入登录界面。

关于登录这块的处理还涉及到了一个状态机有兴趣可以参考这里《unity游戏开发-简易的状态机》

其中热更新完整的方案本身包含了以下四个部分:

  1. 资源文件打包
  2. 资源文件比较
  3. 资源文件下载
  4. 资源文件更新

其中资源文件打包可以参考这里《Unity游戏开发-Assetbundle打包》

本篇主要会涉及到资源文件比较和更新。

另外关于资源文件的下载会放到这里《Unity游戏开发-客户端Http下载》

整体设计

简单解释下这张泳道流程图

主要的逻辑在就是中间登录流程这张图,一共分三步:解压bundle=>检查并处理热更=>登录就绪

其中关于资源解压的所有逻辑放在了左边流程图对应的模块处理,热更新的所有逻辑放在了右边的流程图对应的模块处理。

另外对于整个游戏内容来说,我们的登录为一个逻辑模块。外部的UI展示界面可以直接实例化一个登录逻辑模块,然后将一个显示用的回调函数传入该模块即可。

逻辑模块内部将解压、更新时的一些关键性数据传入到回调函数中,即可让外部处理进度条等等的展示了。

这样做的好处是,登录的UI界面功能和逻辑模块变成了两个互相独立的模块耦合性更低。

而且将业务相关的UI界面和更接近于框架层面的逻辑分离,有利于多项目迁移。

登录流程模块

根据我们所画的流程图可以看出登录流程并没有直接处理任何具体的事情,只是一个单纯的流程控制模块。

因此我采用了状态机的形式来处理该模块。

内部实际仅仅只初始化了解压和热更两个状态模块,并将之关联起来,最后所有流程结束就调用就绪方法。

利用状态机的形式来处理登录逻辑流程主要为了解压和热更模块之间的衔接关系更清楚的展示再登录流程中也更利于流程的改动。

如果直接模块之间相互调用则每次修改流程会非常的麻烦,每次都需要重新阅读代码找到衔接点修改调用的方法。

如果用事件来衔接各个模块其实也挺麻烦,如果流程状态很多的情况下,事件会零零散散的分布在各个模块,其实也不利于阅读和修改流程。

// 登录流程
public class LoginMachine : IStateMachine
{// 资源包解压模块private UnPackAssetBundle mUnPackBundle = new UnPackAssetBundle();// 热更新模块private HotUpdate mHotUpdate = new HotUpdate();// 流程结束回调private Action mFnOnEnd;// 初始化public void Init(Action<string, float> fnLoginShow, Action fnOnEnd){// 初始化状态mUnPackBundle.Init(fnLoginShow, this);mHotUpdate.Init(fnLoginShow, this);mFnOnEnd = fnOnEnd;// 注册流程AddTransition(mUnPackBundle, mHotUpdate, "end");}// 启动流程public void Start(){mUnPackBundle.DoStart(null);}protected override void DoEnd(string key){if ("hotError".Equals(key)){FDebug.LogError("热更新出错,流程结束!");}else{mFnOnEnd?.Invoke();}}
}

其中有一个fnLoginShow,是留给UI界面展示用的回调。

另外AddTransition(a,b,c)是一个状态迁移的注册方法,其中a代表前一个状态,b代表下一个状态,c代表条件。即a在以c方式结束的情况下迁移到b状态。

在这里即解压模块在以"end"的方式结束的情况下调起热更模块。

最后整个流程结束时若发生了需要处理的错误"hotError"则处理下异常,否则调用mFnOnEnd即我们设计的就绪。

解压模块

设计方案:

中间判定是否需要解压简单的判定一下我们存放资源的路径(Application.persistentDataPath + "/Bundles")下是否存在版本清单文件即可。

因为新用户首次打开游戏的时候还没有做过解压,因此可写路径下不会存在我们的资源文件和版本清单文件。而执行完一次解压之后我们就会把资源文件解压到该路径下,并且生成版本清单文件,因此该判定是有效的。

解压的时候这边是考虑开了一个线程来处理,因为解压是一个比较耗时的过程,玩家肯定不希望在这么长的时间内看到游戏界面是卡主的,因此不能阻塞主线程。

整体处理:

// 资源初始化解压模块
public class UnPackAssetBundle : ILoginState
{// 当前正在解压的文件编号private int mUnPackIdx = 0;// 版本文件private AssetVersion version;// 进度显示回调private Action<string, float> mFnLoginShow;#region 流程public override void OnStart(string key){if (CheckInited()){DoEnd("end");}else{InitAsset();}}public override void DoUpdate(){string title = "解压文件中:" + mUnPackIdx.ToString() + "/" + version.files.Count.ToString();float progress = (float)mUnPackIdx / version.files.Count;mFnLoginShow?.Invoke(title, progress);}public override void Init(Action<string, float> fnLoginShow, IStateMachine machine){mFnLoginShow = fnLoginShow;Init(machine);}#endregion// 检查资源文件是否初始化过了public bool CheckInited(){FDebug.Log("资源包文件路径:" + PathMgr.instance.AssetBundlePath);if (!System.IO.Directory.Exists(PathMgr.instance.AssetBundlePath)|| !System.IO.File.Exists(PathMgr.instance.AssetBundlePath + "/version")){mUnPackIdx = 0;// 初始化版本文件FileTool.TruncateDirectory(PathMgr.instance.AssetBundlePath);version = DataWriteRead.ReadJson<AssetVersion>(PathMgr.instance.StreamingPath + "/version");return false;}return true;}// 初始化资源文件public void InitAsset(){// 开线程异步执行Thread thread = new Thread(UnCompressAsset);thread.IsBackground = true;thread.Start();}// 解压所有的资源private void UnCompressAsset(){for(; mUnPackIdx < version.files.Count; mUnPackIdx++){string fileName = version.files[mUnPackIdx].fileName;FileTool.DecompressFileLZMA(PathMgr.instance.StreamingPath + "/" + fileName, PathMgr.instance.AssetBundlePath + "/" + fileName);}// 写入版本文件DataWriteRead.SaveJson(PathMgr.instance.AssetBundlePath + "/version", version);}
}

代码中间又一个块用region包裹起来的"流程",该部分处理的都是状态机中状态的接口实现。

DoUpdate对应了unity的update,里面主要将当前解压文件的进度文字和进度数值传递到mFnLoginShow中供界面展示使用。

其他实现没有什么特别的地方。

热更模块

热更模块相对于解压来说要复杂一些,因为要处理从服务器获取版本清单文件,文件对比校验,下载,解压。

设计方案:

检查是否需要热更:

设计方案:

首先从服务器拉取最新的版本清单文件。

将获取的清单文件逐一和本地做比较,若本地文件md5和服务器不一致则添加到热更列表。

最后若热更列表数量大于0则表示需要热更。

代码实现:

    // 获取服务器版本文件HttpMgr.instance.HttpPost(HttpEventId.HTTP_POST_HOT_VERSION);// 收到服务器版本消息回调private void OnRecvVersion(int resCode, string jsonData){if (resCode == 200){mServerVersion = LitJson.JsonMapper.ToObject<AssetVersion>(jsonData);if (CheckHotUpdate(mServerVersion)){StartHotUpdate();}else{DoEnd();}}else{FDebug.LogError("服务器版本文件获取失败");DoEnd("hotError");}}// 检查是否需要热更新public bool CheckHotUpdate(AssetVersion serverVersion){if (serverVersion == null){return false;}AssetVersion version = DataWriteRead.ReadJson<AssetVersion>(PathMgr.instance.AssetBundlePath + "/version");// 本地版本不存在则更新所有文件if (version == null){foreach (AssetFile serverFile in serverVersion.files){HotFile hotFile = GetHotFile(serverFile, serverVersion.downPath);mHotFiles.Add(hotFile);}FDebug.Log("本地热更新文件不存在");}// 本地版本存在,则更新本地没有的文件或者md5不一致的文件else{foreach (AssetFile serverFile in serverVersion.files){if (version.TryGetAssetFile(serverFile.fileName, out var localFile)){if (!localFile.md5.Equals(serverFile.md5)){HotFile hotFile = GetHotFile(serverFile, serverVersion.downPath);mHotFiles.Add(hotFile);FDebug.Log(serverFile.fileName + "md5不一致=>本地md5:" + localFile.md5 + "。服务器md5:" + serverFile.md5);}else{FDebug.Log(serverFile.fileName + "md5一致");}}else{HotFile hotFile = GetHotFile(serverFile, serverVersion.downPath);mHotFiles.Add(hotFile);FDebug.Log(serverFile.fileName + "本地文件不存在");}}}return mHotFiles.Count > 0;}public class HotFile
{public string serverFile;public string md5;public int size;public string localFile;public string fileName;public HttpDownPack downPack;
}

代码实现比较简单没啥好说的。。。

中间有一个HotFile hotFile = GetHotFile(serverFile, serverVersion.downPath);

是将资源文件信息的数据结构转换为热更新需要的数据结构。

HotFile结构中的HttpDownPack为下载文件需要的数据结构,该结构会在调用下载的时候由下载模块返回回来。我们就可以通过读取HttpDownPack中的各项数据来显示下载进度了。

serverFile为服务器的下载路径,localFile为本地需要保存的路径。

执行热更新:

执行热更简单来讲就是将热更文件列表中的文件逐一拿去调用下载模块。

中间如果出错就直接以hotError的形式结束热更模块。

中途下载中的时候则不停的回调mFnLoginShow来展示下载的进度。

设计方案:

代码实现:

// 开始下载流程private void StartHotUpdate(){// 设置下载总大小foreach (var oneHotFile in mHotFiles){mTotalSize += oneHotFile.size;}// 下载第一个文件mDownIdx = 0;var hotFile = mHotFiles[mDownIdx];hotFile.downPack = HttpMgr.instance.HttpDownLoad(hotFile);// 修改状态为下载状态mState = EHotState.HotUpdate;}// 下载状态时定时回调private void DoHotUpdate(){var curHotFile = mHotFiles[mDownIdx];HttpDownState state = curHotFile.downPack.state;if (state == HttpDownState.Complete){// 记录已下载的大小mNowDownSize += curHotFile.size;// 下载下一个mDownIdx++;if (mDownIdx < mHotFiles.Count){var hotFile = mHotFiles[mDownIdx];hotFile.downPack = HttpMgr.instance.HttpDownLoad(hotFile);}// 结束下载else{StartUnPack();}}else if (state == HttpDownState.Error ||state == HttpDownState.Md5Failed){DoEnd("hotError");}else{int curSize = mNowDownSize + curHotFile.downPack.downSize;string title = "热更新中:" + CommonTool.GetMBByteSize(curSize) + "/" + CommonTool.GetMBByteSize(mTotalSize);float progress = (float)curSize / mTotalSize;mFnLoginShow?.Invoke(title, progress);}}

代码实现中有一块地方是设置mState = EHotState.HotUpdate;

原因是整个热更模块中有两个小的子模块:下载、解压。

这两个模块中间某些地方都需要被Update调用,因此用了一个状态标记。

再热更模块的Update中根据不同的状态调用不同的子模块逻辑。

执行解压:

热更的解压部分和解压模块非常的相似,都是启动一个线程然后一个一个的解压文件。

update里面不停的获取解压的进度反馈到界面即可。

代码实现:

    // 开始解压private void StartUnPack(){mState = EHotState.UnPack;mUnPackIdx = 0;// 开启线程解压System.Threading.Thread unPack = new System.Threading.Thread(UnPackTask);unPack.IsBackground = true;unPack.Start();}// 解压任务执行private void UnPackTask(){for (; mUnPackIdx < mHotFiles.Count; ){var hotFile = mHotFiles[mUnPackIdx];FileTool.DecompressFileLZMA(hotFile.localFile, PathMgr.instance.AssetBundlePath + "/" + hotFile.fileName);FileTool.DeleteFile(hotFile.localFile);mUnPackIdx++;}}// 解压时定时器获取进度private void DoUnPack(){// 回调显示string title = "热更文件解压中:" + mUnPackIdx.ToString() + "/" + mHotFiles.Count.ToString();mFnLoginShow?.Invoke(title, (float)mUnPackIdx / mHotFiles.Count);if (mUnPackIdx >= mHotFiles.Count){// 将服务器版本文件写入本地DataWriteRead.SaveJson<AssetVersion>(PathMgr.instance.AssetBundlePath + "/" + "version", mServerVersion);DoEnd();}}

完整代码

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;public class HotFile
{public string serverFile;public string md5;public int size;public string localFile;public string fileName;public HttpDownPack downPack;
}// 热更新
public class HotUpdate : ILoginState
{// 热更状态private enum EHotState{ None,HotUpdate,UnPack,}private EHotState mState;// 服务器版本文件private AssetVersion mServerVersion;// 需要热更的文件private List<HotFile> mHotFiles = new List<HotFile>();// 总大小private int mTotalSize;// 当前已下载完成大小private int mNowDownSize;// 正在下载的文件编号private int mDownIdx;// 正在解压的文件编号private int mUnPackIdx;// 显示回调private Action<string, float> mFnLoginShow;#region 流程public override void OnStart(string key){mState = EHotState.None;// 注册回调HttpEvent.instance.AddEvent<int, string>(HttpEventId.HTTP_POST_HOT_VERSION, OnRecvVersion);// 获取服务器版本文件HttpMgr.instance.HttpPost(HttpEventId.HTTP_POST_HOT_VERSION);}public override void DoUpdate(){if (mState == EHotState.HotUpdate){DoHotUpdate();}else if (mState == EHotState.UnPack){DoUnPack();}}protected override void OnEnd(){}public override void Init(Action<string, float> fnLoginShow, IStateMachine machine){mFnLoginShow = fnLoginShow;Init(machine);}#endregion#region 逻辑// 收到服务器版本消息private void OnRecvVersion(int resCode, string jsonData){if (resCode == 200){mServerVersion = LitJson.JsonMapper.ToObject<AssetVersion>(jsonData);if (CheckHotUpdate(mServerVersion)){StartHotUpdate();}else{DoEnd();}}else{FDebug.LogError("服务器版本文件获取失败");DoEnd("hotError");}}// 检查是否需要热更新public bool CheckHotUpdate(AssetVersion serverVersion){if (serverVersion == null){return false;}AssetVersion version = DataWriteRead.ReadJson<AssetVersion>(PathMgr.instance.AssetBundlePath + "/version");// 本地版本不存在则更新所有文件if (version == null){foreach (AssetFile serverFile in serverVersion.files){HotFile hotFile = GetHotFile(serverFile, serverVersion.downPath);mHotFiles.Add(hotFile);}FDebug.Log("本地热更新文件不存在");}// 本地版本存在,则更新本地没有的文件或者md5不一致的文件else{foreach (AssetFile serverFile in serverVersion.files){if (version.TryGetAssetFile(serverFile.fileName, out var localFile)){if (!localFile.md5.Equals(serverFile.md5)){HotFile hotFile = GetHotFile(serverFile, serverVersion.downPath);mHotFiles.Add(hotFile);FDebug.Log(serverFile.fileName + "md5不一致=>本地md5:" + localFile.md5 + "。服务器md5:" + serverFile.md5);}else{FDebug.Log(serverFile.fileName + "md5一致");}}else{HotFile hotFile = GetHotFile(serverFile, serverVersion.downPath);mHotFiles.Add(hotFile);FDebug.Log(serverFile.fileName + "本地文件不存在");}}}return mHotFiles.Count > 0;}// 开始下载流程private void StartHotUpdate(){// 设置下载总大小foreach (var oneHotFile in mHotFiles){mTotalSize += oneHotFile.size;}// 下载第一个文件mDownIdx = 0;var hotFile = mHotFiles[mDownIdx];hotFile.downPack = HttpMgr.instance.HttpDownLoad(hotFile);// 修改状态为下载状态mState = EHotState.HotUpdate;}// 下载状态时定时回调private void DoHotUpdate(){var curHotFile = mHotFiles[mDownIdx];HttpDownState state = curHotFile.downPack.state;if (state == HttpDownState.Complete){// 记录已下载的大小mNowDownSize += curHotFile.size;// 下载下一个mDownIdx++;if (mDownIdx < mHotFiles.Count){var hotFile = mHotFiles[mDownIdx];hotFile.downPack = HttpMgr.instance.HttpDownLoad(hotFile);}// 结束下载else{StartUnPack();}}else if (state == HttpDownState.Error ||state == HttpDownState.Md5Failed){DoEnd("hotError");}else{int curSize = mNowDownSize + curHotFile.downPack.downSize;string title = "热更新中:" + CommonTool.GetMBByteSize(curSize) + "/" + CommonTool.GetMBByteSize(mTotalSize);float progress = (float)curSize / mTotalSize;mFnLoginShow?.Invoke(title, progress);}}// 开始解压private void StartUnPack(){mState = EHotState.UnPack;mUnPackIdx = 0;// 开启线程解压System.Threading.Thread unPack = new System.Threading.Thread(UnPackTask);unPack.IsBackground = true;unPack.Start();}// 解压任务执行private void UnPackTask(){for (; mUnPackIdx < mHotFiles.Count; ){var hotFile = mHotFiles[mUnPackIdx];FileTool.DecompressFileLZMA(hotFile.localFile, PathMgr.instance.AssetBundlePath + "/" + hotFile.fileName);FileTool.DeleteFile(hotFile.localFile);mUnPackIdx++;}}// 解压时定时器获取进度private void DoUnPack(){// 回调显示string title = "热更文件解压中:" + mUnPackIdx.ToString() + "/" + mHotFiles.Count.ToString();mFnLoginShow?.Invoke(title, (float)mUnPackIdx / mHotFiles.Count);if (mUnPackIdx >= mHotFiles.Count){// 将服务器版本文件写入本地DataWriteRead.SaveJson<AssetVersion>(PathMgr.instance.AssetBundlePath + "/" + "version", mServerVersion);DoEnd();}}private HotFile GetHotFile(AssetFile assetFile, string downPath){HotFile hotFile = new HotFile();hotFile.serverFile = downPath + assetFile.fileName;hotFile.md5 = assetFile.md5;hotFile.size = assetFile.fileSize;hotFile.fileName = assetFile.fileName;hotFile.localFile = PathMgr.instance.DownTmpPath + "/" + assetFile.fileName;return hotFile;}#endregion
}

Unity游戏开发-游戏热更新以及登录流程相关推荐

  1. 1.15 从0开始学习Unity游戏开发--游戏UI

    上一章中,我们剩下最后一个任务,需要支持鼠标控制准心来进行设计,那么准心本质上就是一个始终呈现在屏幕上的一个图片,你当然可以用一个3D物体来制作,之前讲解渲染概念的时候也提到过,我们的屏幕就是相机的近 ...

  2. Unity应用的iOS热更新

    Unity应用的iOS热更新 作者:丁治宇 Unity TechnologiesChina Agenda ? 什么是热更新 ? 为何要热更新 ? 如何在iOS 上对Unity 应用进行热更新 ? 支持 ...

  3. wechat小游戏开发游戏引擎选择

    目前主流三家 :  1.Cocos Creator :http://cn.cocos2d-x.org/  2.Egret:http://www.egret-labs.org/  3. Layabox ...

  4. Android游戏开发–游戏循环

    游戏循环是每个游戏的心跳. 到目前为止,我们仅使用了非常简单的一种(您可以在此处找到),无法控制我们更新游戏状态的速度或速度以及要渲染的帧. 概括地说,最基本的游戏循环是while循环,该循环一直执行 ...

  5. android_Android游戏开发–游戏循环

    android 游戏循环是每个游戏的心跳. 到目前为止,我们使用的是非常简单的游戏(您可以在此处找到),无法控制我们更新游戏状态的速度或速度以及要渲染的帧. 概括地说,最基本的游戏循环是while循环 ...

  6. android游戏开发_Android游戏开发–游戏循环

    android游戏开发 游戏循环是每个游戏的心跳. 到目前为止,我们仅使用了非常简单的一种(您可以在此处找到),无法控制我们更新游戏状态的速度或速度以及要渲染的帧. 概括地说,最基本的游戏循环是whi ...

  7. H5数独游戏开发——游戏通关及重玩

    如何判断游戏是否通关?通关后如何重新开始游戏,让用户重玩?今天给大家详细介绍如何实现以上功能.先看一下数独游戏通关后的提示界面,截图如下: 首先,我们需要判断游戏是否通关,那么如何判断呢?有两个条件: ...

  8. H5数独游戏开发——游戏中的事件处理

    假设你正在玩一款角色扮演游戏(RPG),主角走进了一家店铺,你用鼠标点击店铺里管家,这时屏幕显示一个待售物品菜单,然后你可以选购你需要的物品.那么,你知道从你点击鼠标的那一刻起,游戏是怎样响应的吗?让 ...

  9. 【游戏开发高阶】从零到一教你Unity使用ToLua实现热更新(含Demo工程 | LuaFramework | 增量 | HotUpdate)

    文章目录 零.前言 一.我做的热更新Demo 1.效果演示 2.流程图 3.工程源码 二.为什么要有热更新 三.Unity如何支持热更新 1.热更C#代码 2.热更lua代码与资源 四.Unity中集 ...

最新文章

  1. de casteljau算法_泊松分布算法的应用:开一家4S店
  2. 2018.07.11 线段树基本模板复习
  3. 操作系统:Linux虚拟内存知识介绍
  4. python发送邮件 退回_python 发送邮件(收到的邮件要有发送方才能回复)
  5. 轻量高效的开源JavaScript插件和库 【转】
  6. Android静态代码扫描效率优化与实践
  7. 视图的数据存放在哪里_分布式 | DBLE 是如何实现视图的?
  8. java eav_动态自定义字段属性–Magento的EAV模型 | 学步园
  9. weblogic 文件打开数_Weblogic任意文件读取和任意文件上传
  10. PostgreSql之在group by查询下拼接列字符串
  11. Redis内存分配简单分析
  12. mysql in range_mysql 的 RANGE 分区有价值吗?
  13. 【报告分享】2021抖音数据报告.pdf(附下载链接)
  14. 〖强暴贴〗号称全国最大开发者网络的CSDN多次出现低级错误 验证码形同虚设 开发人员竟是MVP
  15. dell云存储服务器,dell云存储服务器(戴尔存储服务器)
  16. 仙道服务器维护,【正式服】2月14日例行更新维护公告
  17. arm低功耗模式种类
  18. 计算机网络设备接地规范,网络机房防雷接地的四种方式及静电要求
  19. 换网线IP地址会改变吗?
  20. 晶圆在低温探针台中的安装方式——N-Grease真空油脂和夹具

热门文章

  1. 从这些相声艺人身上,我们能学到什么?
  2. 卸载DreamScene
  3. cr06660服务器显示一个u,CR06660-JW正品优惠
  4. cdn服务器搭建步骤
  5. IOTE 2019第十一届国际物联网展--苏州站
  6. 基于QWidget绘制蛛网图
  7. 差分方程模型(二):蛛网模型
  8. 输入半径r求圆的面积java_已知圆的半径r,求圆的面积S_学小易找答案
  9. OpenWrt 18.06编译龙尚模块驱动
  10. 产业与金融如何融合创新?百度智能云阐述产融一体化解决方案