不管对于单机还是网络游戏,热更新已经成了标配。所谓热更,指的就是在无需重新打包的情况下完成资源、数据和代码的更新。
本篇文章主要针对的是Unity3D开发的项目,其热更思路也可以应用到其他引擎诸如Cocos2D中。当然对于网页游戏或者小程序而言,开发语言使用lua、TyppScript、JavaScript等解释性语言,可以边运行边转换,资源和代码放到网络空间实时更新即可。
登录后复制
热更包含代码热更、表格数据热更和美术资源热更三部分。
使用MD5效验文件版本,删除不在版本控制内的资源,如有变动替换并下载新的资源。
以下热更均在启动界面完成,热更完毕之后再切换到登录界面。
代码热更:
替换lua脚本后,开启lua解释器。
表格数据热更:
热更完毕后,替换表格数据资源。
------------美术资源分为图片资源和模型资源热更------------
图片资源热更:
热更完毕后进入游戏。
模型资源热更:
热更完毕后进入游戏。
以上流程和思路可以根据具体项目自行调整。
这个本来属于运维服务器的事情,但我们在开发的时候可以先用免费的软件来代替。我选用的是hfs网络文件管理器,在 我的资源中有相关链接,需要的同学可以自己下载使用。
这里对于网络空间的部署,只是为了方便在客户端中进行热更下载。不管使用什么都可以,只要能提供下载就行。
将链接拷贝下来,留着测试使用。
按照设计思路,我们可以先划分出不同平台的不同目录,把Bytes、Scripts、Module、UI文件夹,在同级目录下补充一个效验用的Bundle.txt文件。各个文件夹下面具体的资源划分,可以根据项目需要调整。
这里的文件夹划分仅仅是提供一个思路,只需要有相对应的解析方法,资源的划分完全可以使用自己的设计。
MD5最初是为加密而设计,但由于其存在漏洞而被舍弃。但它可以为我们提供数据完整性的效验,因为可以用来对比和效验需要热更的文件。
多数脚本语言中,已经为我们提供好了解析MD5的库,在C#中它是:
登录后复制
using System.Security.Cryptography;//包含MD5库
由此我们可以总结出“遍历该文件夹下所有子文件,并生成相对应的MD5”的需求,从而引申开发如下脚本,记录所有文件的json并存储为Bundle.txt。
登录后复制
using System;
//MD5信息
[Serializable]
public class MD5Message
{
public string file;//文件位置及名字
public string md5;//MD5效验结果
public string fileLength;//文件长度
}
//MD5全部信息
[Serializable]
public class FileMD5
{
public string length;//总长度
public MD5Message[] files;
}
登录后复制
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.IO;
using System.Text;
using System.Security.Cryptography;//包含MD5库
/// <summary>
/// MD5 效验器
/// 遍历该文件夹下所有子文件,并生成相对应的MD5
/// </summary>
public class MD5ACharm : MonoBehaviour
{
[MenuItem("MD5效验器/平台/IOS平台")]
static void BuildReleaseIOSBundle()
{
BuildBundleStart("iOS");
}
[MenuItem("MD5效验器/平台/Android平台")]
static void BuildReleaseAndroidBundle()
{
BuildBundleStart("Android");
}
[MenuItem("MD5效验器/平台/Windows平台")]
static void BuildReleaseWindowsBundle()
{
BuildBundleStart("Win");
}
static void BuildBundleStart(string _path)
{
ABPath = _path;
Caching.ClearCache();//清除所有缓存
string path = GetTempPath();
DeleteTempBundles(path); //删除旧的MD5版本文件
CreateBundleVersionNumber(path);
AssetDatabase.Refresh();
}
private static Dictionary<string, string> m_BundleMD5Map = new Dictionary<string, string>();
/// <summary>
/// 删除指定文件
/// </summary>
/// <param name="target"></param>
static void DeleteTempBundles(string path)
{
if (!Directory.Exists(path))
Directory.CreateDirectory(path);
string[] bundleFiles = Directory.GetFiles(path, "*.*", SearchOption.AllDirectories);
foreach (string s in bundleFiles)
{
if(s== "Bundle.txt") File.Delete(s);
}
}
static void CreateBundleVersionNumber(string bundlePath)
{
FileMD5 _file = new FileMD5();
string[] contents = Directory.GetFiles(bundlePath, "*.*", SearchOption.AllDirectories);
string extension;
string fileName = "";
string fileMD5 = "";
long allLength = 0;
int fileLen;
m_BundleMD5Map.Clear();
for (int i = 0; i < contents.Length; i++)
{
fileName = contents[i].Replace(GetTempPath(), "").Replace("\\", "/");
extension = Path.GetExtension(contents[i]);
if (extension != ".meta")
{
fileMD5 = GetMD5HashFromFile(contents[i]);
fileLen = File.ReadAllBytes(contents[i]).Length;
allLength += fileLen;
m_BundleMD5Map.Add(fileName, fileMD5 + "+" + fileLen);
}
}
var _list = new List<MD5Message>();
foreach (KeyValuePair<string, string> kv in m_BundleMD5Map)
{
string[] nAndL = kv.Value.Split('+');
MD5Message _md5 = new MD5Message();
_md5.file = kv.Key;
_md5.md5 = nAndL[0];
_md5.fileLength = nAndL[1];
_list.Add(_md5);
}
var _md5All = new MD5Message[_list.Count];
for (var _i = 0; _i < _list.Count; _i++)
{
_md5All[_i] = _list[_i];
}
_file.length = "" + allLength;
_file.files = _md5All;
var _filePath = JsonUtility.ToJson(_file);
Debug.LogError(_filePath);
File.WriteAllText(GetTempPath() + "Bundle.txt", _filePath);
m_BundleMD5Map.Clear();
}
/// <summary>获取文件的md5校验码</summary>
static string GetMD5HashFromFile(string fileName)
{
if (File.Exists(fileName))
{
FileStream file = new FileStream(fileName, FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
file.Close();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
sb.Append(retVal[i].ToString("x2"));
return sb.ToString();
}
return null;
}
//指定下载路径
static string ABPath = "Win";
static string GetTempPath(string _path="")
{
var _str = GetPathName() + "/MD5" + ABPath + "/"+_path;
return _str;
}
//网络空间的位置
static string GetPathName()
{
return "E:/MyServer/Chief";
}
}
这里提供了三个常见的IOS、Android和Win平台,以提供不同平台的使用。
有个知识点就是使用Directory.GetFiles(bundlePath, "*.*", SearchOption.AllDirectories);的方法,可以获取到该文件夹下的所有文件。
我在需要热更的文件夹下放了一些测试资源,测试步骤及结果如下:
文件目录:
点击生成:
生成结果:
根据设计思路,我们需要先下载Bundle.txt,从而获得热更资源的版本信息。首先对于不在版本管理内的资源进行删除,尔后对于不符合MD5效验的旧资源进行删除和替代:
登录后复制
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Text;
using System.Security.Cryptography;//包含MD5库
using System;
using UnityEngine.UI;
/// <summary>
/// 获取到更新脚本
/// </summary>
public class Get : MonoBehaviour
{
private bool IS_ANDROID = false;
private static Text fillAmountTxt;
void Start()
{
transform.Find("Button").GetComponent<Button>().onClick.AddListener(delegate { StartUpdate(); });
fillAmountTxt = transform.Find("Text").GetComponent<Text>();
TestPrint();
}
//输出测试
void TestPrint()
{
Debug.Log("*******测试打印所有文件目录*******");
var _str = "";
string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
foreach (string idx in bundleFiles)
{
var _r = @"\Android\";
if (IS_ANDROID) _r = "/Android/";
var _s = idx.Replace(GetTerracePath() + _r, "");
_s = _s.Replace(@"\", "/");
Debug.Log("替换过程:" + idx + " " + GetTerracePath() + " " + _s);
_str += _s + "\n";
}
transform.Find("CeshiText").GetComponent<Text>().text = _str;
Debug.Log("**************结束打印************");
}
//更新版本
private void StartUpdate()
{
StartCoroutine(VersionUpdate());
}
private int allfilesLength = 0;
/// <summary>
/// 版本更新
/// </summary>
/// <returns></returns>
IEnumerator VersionUpdate()
{
WWW www = new WWW("http://192.168.6.178/Chief/MD5Android/Bundle.txt");
yield return www;
if (www.isDone && string.IsNullOrEmpty(www.error))
{
List<BundleInfo> bims = new List<BundleInfo>();
FileMD5 date = JsonUtility.FromJson<FileMD5>(www.text);
DeleteOtherBundles(date);//删除所有不受版本控制的文件
Debug.LogError(www.text);
//Debug.Log(data.Contains());
var _list = date.files;
string md5, file, path;
int lenth;
for (int i = 0; i <_list.Length; i++)
{
MD5Message _md5 = _list[i];
Debug.Log(_md5.file + " " + _md5.fileLength + " " + _md5.md5);
file = _md5.file;
path = PathUrl(file);
md5 = GetMD5HashFromFile(path);
if (string.IsNullOrEmpty(md5) || md5 != _md5.md5)
{
bims.Add(new BundleInfo()
{
Url = HttpDownLoadUrl(file),
Path = path
});
lenth = int.Parse(_md5.fileLength);
allfilesLength += lenth;
}
}
if (bims.Count > 0)
{
Debug.LogError("开始尝试更新");
StartCoroutine(DownLoadBundleFiles(bims, (progress) => {
OpenLodingShow("自动更新中...", progress, allfilesLength);
}, (isfinish) => {
if (isfinish)
StartCoroutine(VersionUpdateFinish());
else
{
StartCoroutine(VersionUpdate());
}
}));
}
else
{
StartCoroutine(VersionUpdateFinish());
}
}
}
// 删除所有不受版本控制的所有文件
void DeleteOtherBundles(FileMD5 _md5)
{
Debug.LogError("~~~~~~~~~~开始删除~~~~~~~");
string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
foreach (string idx in bundleFiles)
{
var _r = @"\Android\";
if (IS_ANDROID) _r = "/Android/";
var _s = idx.Replace(GetTerracePath() + _r, "");
_s = _s.Replace(@"\", "/");
if (!FindNameInFileMD5(_md5,_s))
{
File.Delete(idx);
Debug.LogError(_s + "不存在");
}
}
Debug.Log("~~~~~~~结束删除~~~~~~~");
}
/// <summary>获取文件的md5校验码</summary>
public string GetMD5HashFromFile(string fileName)
{
if (File.Exists(fileName))
{
FileStream file = new FileStream(fileName, FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
file.Close();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
sb.Append(retVal[i].ToString("x2"));
return sb.ToString();
}
return null;
}
static bool FindNameInFileMD5(FileMD5 date,string _name)
{
foreach (var _m in date.files)
{
if (_m.file == _name) return true;
}
return false;
}
//脚本替换(lua等)验证
IEnumerator VersionUpdateFinish()
{
Debug.Log("lua验证");
string sPath = GetTerracePath(IS_ANDROID) + "/Android/Scripts/ceshi.lua.txt";
WWW www = new WWW(sPath);
yield return www;
Debug.LogError(www.text);
transform.Find("CeshiText").GetComponent<Text>().text = www.text;
//StartCoroutine(VersionUpdateImage());
//StartCoroutine(VersionUpdateModel());
}
图片验证
//IEnumerator VersionUpdateImage()
//{
// Debug.Log("图片替换验证");
// string sPath = GetTerracePath(IS_ANDROID) + "/Android/UI/shop/1.png";
// WWW www = new WWW(sPath);
// yield return www;
// if (www.isDone && string.IsNullOrEmpty(www.error))
// {
// var _img = www.texture;
// Texture2D tex = new Texture2D(_img.width,_img.height);
// www.LoadImageIntoTexture(tex);
// Sprite sprite = Sprite.Create(tex, new Rect(0, 0, _img.width, _img.height), Vector2.zero);
// transform.Find("Button").GetComponent<Image>().sprite = sprite;
// }
//}
模型验证
//IEnumerator VersionUpdateModel()
//{
// string s = GetTerracePath(IS_ANDROID) + "/Android/Module/0000001/bql";
// string progress = null;
// WWW w = new WWW(s);
// while (!w.isDone)
// {
// progress = (((int)(w.progress * 100)) % 100) + "%";
// Debug.Log("加载模型:" + progress);
// yield return null;
// }
// yield return w;
// if (w.error != null)
// {
// Debug.Log("error:" + w.url + "\n" + w.error);
// }
// else
// {
// AssetBundle bundle = w.assetBundle;
// GameObject modelPre = bundle.LoadAsset<GameObject>("bql");
// GameObject modelClone = Instantiate(modelPre);
// AssetBundle.UnloadAllAssetBundles(false);
// }
//}
/// <summary>
/// 路径比对
/// </summary>
public IEnumerator DownLoadBundleFiles(List<BundleInfo> infos, Action<float> LoopCallBack = null, Action<bool> CallBack = null)
{
//Debug.Log("开始路径对比");
int num = 0;
string dir;
for (int i = 0; i < infos.Count; i++)
{
BundleInfo info = infos[i];
Debug.LogError(info.Url);
WWW www = new WWW(info.Url);
yield return www;
if (www.isDone && string.IsNullOrEmpty(www.error))
{
try
{
string filepath = info.Path;
dir = Path.GetDirectoryName(filepath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.WriteAllBytes(filepath, www.bytes);
num++;
if (LoopCallBack != null)
LoopCallBack.Invoke((float)num / infos.Count);
Debug.Log(dir+"下载完成");
}
catch (Exception e)
{
Debug.Log("下载失败"+e);
}
}
else
{
Debug.Log("下载错误"+www.error);
}
}
if (CallBack != null)
CallBack.Invoke(num == infos.Count);
}
/// <summary>
/// 记录信息
/// </summary>
public struct BundleInfo
{
public string Path { get; set; }
public string Url { get; set; }
public override bool Equals(object obj)
{
return obj is BundleInfo && Url == ((BundleInfo)obj).Url;
}
public override int GetHashCode()
{
return Url.GetHashCode();
}
}
/// <summary>
/// loadpage展示
/// </summary>
/// <param name="text"></param>
/// <param name="progress"></param>
/// <param name="filealllength"></param>
public void OpenLodingShow(string text = "", float progress = 0, long filealllength = 0)
{
Debug.LogError(text+" "+ progress + " " + filealllength);
fillAmountTxt.text = text + progress + " 文件序列:" + filealllength;
if (progress >= 1) fillAmountTxt.text = "更新完成";
}
void Update()
{
}
private void OnDestroy()
{
//if (DestoryLua != null)
// DestoryLua();
//UpdateLua = null;
//DestoryLua = null;
//StartLua = null;
// LuaEnvt.Dispose();
}
string HttpDownLoadUrl(string _str)
{
return "http://192.168.6.178/Chief/MD5Android/" + _str;
}
//根据不同路径,对下载路径进行封装
string PathUrl(string _str)
{
var _path= GetTerracePath() + "/Android/" + _str;
return _path;
}
//获得不同平台的路径
string GetTerracePath(bool _isAndroid=false)
{
string sPath = Application.persistentDataPath;
if(_isAndroid) sPath = "file://" + sPath;
return sPath;
}
}
这里为了方便阅读,屏蔽了图片加载和模型加载的测试。点击运行结果如下:
这里只做简单的引申,在我们项目中,常用的也就是图片、txt文件以及AB包。
除了第二章中使用txt文件热更,在这里添加了图片与AB包的测试;具体打包方法可以使用官方插件,全部代码如下:
登录后复制
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Text;
using System.Security.Cryptography;//包含MD5库
using System;
using UnityEngine.UI;
/// <summary>
/// 获取到更新脚本
/// </summary>
public class Get : MonoBehaviour
{
private bool IS_ANDROID = false;
private static Text fillAmountTxt;
void Start()
{
transform.Find("Button").GetComponent<Button>().onClick.AddListener(delegate { StartUpdate(); });
fillAmountTxt = transform.Find("Text").GetComponent<Text>();
TestPrint();
}
//输出测试
void TestPrint()
{
Debug.Log("*******测试打印所有文件目录*******");
var _str = "";
string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
foreach (string idx in bundleFiles)
{
var _r = @"\Android\";
if (IS_ANDROID) _r = "/Android/";
var _s = idx.Replace(GetTerracePath() + _r, "");
_s = _s.Replace(@"\", "/");
Debug.Log("替换过程:" + idx + " " + GetTerracePath() + " " + _s);
_str += _s + "\n";
}
transform.Find("CeshiText").GetComponent<Text>().text = _str;
Debug.Log("**************结束打印************");
}
//更新版本
private void StartUpdate()
{
StartCoroutine(VersionUpdate());
}
private int allfilesLength = 0;
/// <summary>
/// 版本更新
/// </summary>
/// <returns></returns>
IEnumerator VersionUpdate()
{
WWW www = new WWW("http://192.168.6.178/Chief/MD5Android/Bundle.txt");
yield return www;
if (www.isDone && string.IsNullOrEmpty(www.error))
{
List<BundleInfo> bims = new List<BundleInfo>();
FileMD5 date = JsonUtility.FromJson<FileMD5>(www.text);
DeleteOtherBundles(date);//删除所有不受版本控制的文件
Debug.LogError(www.text);
//Debug.Log(data.Contains());
var _list = date.files;
string md5, file, path;
int lenth;
for (int i = 0; i <_list.Length; i++)
{
MD5Message _md5 = _list[i];
Debug.Log(_md5.file + " " + _md5.fileLength + " " + _md5.md5);
file = _md5.file;
path = PathUrl(file);
md5 = GetMD5HashFromFile(path);
if (string.IsNullOrEmpty(md5) || md5 != _md5.md5)
{
bims.Add(new BundleInfo()
{
Url = HttpDownLoadUrl(file),
Path = path
});
lenth = int.Parse(_md5.fileLength);
allfilesLength += lenth;
}
}
if (bims.Count > 0)
{
Debug.LogError("开始尝试更新");
StartCoroutine(DownLoadBundleFiles(bims, (progress) => {
OpenLodingShow("自动更新中...", progress, allfilesLength);
}, (isfinish) => {
if (isfinish)
StartCoroutine(VersionUpdateFinish());
else
{
StartCoroutine(VersionUpdate());
}
}));
}
else
{
StartCoroutine(VersionUpdateFinish());
}
}
}
// 删除所有不受版本控制的所有文件
void DeleteOtherBundles(FileMD5 _md5)
{
Debug.LogError("~~~~~~~~~~开始删除~~~~~~~");
string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
foreach (string idx in bundleFiles)
{
var _r = @"\Android\";
if (IS_ANDROID) _r = "/Android/";
var _s = idx.Replace(GetTerracePath() + _r, "");
_s = _s.Replace(@"\", "/");
if (!FindNameInFileMD5(_md5,_s))
{
File.Delete(idx);
Debug.LogError(_s + "不存在");
}
}
Debug.Log("~~~~~~~结束删除~~~~~~~");
}
/// <summary>获取文件的md5校验码</summary>
public string GetMD5HashFromFile(string fileName)
{
if (File.Exists(fileName))
{
FileStream file = new FileStream(fileName, FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
file.Close();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
sb.Append(retVal[i].ToString("x2"));
return sb.ToString();
}
return null;
}
static bool FindNameInFileMD5(FileMD5 date,string _name)
{
foreach (var _m in date.files)
{
if (_m.file == _name) return true;
}
return false;
}
//脚本替换(lua等)验证
IEnumerator VersionUpdateFinish()
{
Debug.Log("lua验证");
string sPath = GetTerracePath(IS_ANDROID) + "/Android/Scripts/ceshi.lua.txt";
WWW www = new WWW(sPath);
yield return www;
Debug.LogError(www.text);
transform.Find("CeshiText").GetComponent<Text>().text = www.text;
StartCoroutine(VersionUpdateImage());
StartCoroutine(VersionUpdateModel());
}
//图片验证
IEnumerator VersionUpdateImage()
{
Debug.Log("图片替换验证");
string sPath = GetTerracePath(IS_ANDROID) + "/Android/UI/shop/1.png";
WWW www = new WWW(sPath);
yield return www;
if (www.isDone && string.IsNullOrEmpty(www.error))
{
var _img = www.texture;
Texture2D tex = new Texture2D(_img.width, _img.height);
www.LoadImageIntoTexture(tex);
Sprite sprite = Sprite.Create(tex, new Rect(0, 0, _img.width, _img.height), Vector2.zero);
transform.Find("Button").GetComponent<Image>().sprite = sprite;
}
}
//模型验证
IEnumerator VersionUpdateModel()
{
string s = GetTerracePath(IS_ANDROID) + "/Android/Module/0000001/bql";
string progress = null;
WWW w = new WWW(s);
while (!w.isDone)
{
progress = (((int)(w.progress * 100)) % 100) + "%";
Debug.Log("加载模型:" + progress);
yield return null;
}
yield return w;
if (w.error != null)
{
Debug.Log("error:" + w.url + "\n" + w.error);
}
else
{
AssetBundle bundle = w.assetBundle;
GameObject modelPre = bundle.LoadAsset<GameObject>("bql");
GameObject modelClone = Instantiate(modelPre);
AssetBundle.UnloadAllAssetBundles(false);
}
}
/// <summary>
/// 路径比对
/// </summary>
public IEnumerator DownLoadBundleFiles(List<BundleInfo> infos, Action<float> LoopCallBack = null, Action<bool> CallBack = null)
{
//Debug.Log("开始路径对比");
int num = 0;
string dir;
for (int i = 0; i < infos.Count; i++)
{
BundleInfo info = infos[i];
Debug.LogError(info.Url);
WWW www = new WWW(info.Url);
yield return www;
if (www.isDone && string.IsNullOrEmpty(www.error))
{
try
{
string filepath = info.Path;
dir = Path.GetDirectoryName(filepath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.WriteAllBytes(filepath, www.bytes);
num++;
if (LoopCallBack != null)
LoopCallBack.Invoke((float)num / infos.Count);
Debug.Log(dir+"下载完成");
}
catch (Exception e)
{
Debug.Log("下载失败"+e);
}
}
else
{
Debug.Log("下载错误"+www.error);
}
}
if (CallBack != null)
CallBack.Invoke(num == infos.Count);
}
/// <summary>
/// 记录信息
/// </summary>
public struct BundleInfo
{
public string Path { get; set; }
public string Url { get; set; }
public override bool Equals(object obj)
{
return obj is BundleInfo && Url == ((BundleInfo)obj).Url;
}
public override int GetHashCode()
{
return Url.GetHashCode();
}
}
/// <summary>
/// loadpage展示
/// </summary>
/// <param name="text"></param>
/// <param name="progress"></param>
/// <param name="filealllength"></param>
public void OpenLodingShow(string text = "", float progress = 0, long filealllength = 0)
{
Debug.LogError(text+" "+ progress + " " + filealllength);
fillAmountTxt.text = text + progress + " 文件序列:" + filealllength;
if (progress >= 1) fillAmountTxt.text = "更新完成";
}
void Update()
{
}
private void OnDestroy()
{
//if (DestoryLua != null)
// DestoryLua();
//UpdateLua = null;
//DestoryLua = null;
//StartLua = null;
// LuaEnvt.Dispose();
}
string HttpDownLoadUrl(string _str)
{
return "http://192.168.6.178/Chief/MD5Android/" + _str;
}
//根据不同路径,对下载路径进行封装
string PathUrl(string _str)
{
var _path= GetTerracePath() + "/Android/" + _str;
return _path;
}
//获得不同平台的路径
string GetTerracePath(bool _isAndroid=false)
{
string sPath = Application.persistentDataPath;
if(_isAndroid) sPath = "file://" + sPath;
return sPath;
}
}
测试结果如下:
真机测试结果如下:
在实际测试过程中,AB包可以使用官方插件来处理,但需要注意的是导出的时候,shader要放入设置里,否则容易出现材质球丢失的问题。
本篇博客的设计思路是,上传效验的文件,本地比对更新。测试用例中引申了图片、模型和txt文本的读取,但就实际代码热更的时候,需要考虑所用的lua框架;这部分暂时不放入热更之中。
简而言之,本篇博客只提供上传和下载的方法。具体的使用,还需要根据项目实际划分。最后,使用链接已经上传到我的资源中。
如果现在用新版本的朋友,应该会发现WWW类提示已过时的字样,原因是IOS9.0以上版本已经禁止了Http协议。部分安卓手机如三星高版本,在海外测试的时候也有类似问题。
所以当Unity官方提示你更新的时候,别犟~按照官方要求去改就行。新的测试方法代码如下:
登录后复制
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using System.Text;
using System.Security.Cryptography;//包含MD5库
using System;
using UnityEngine.UI;
using UnityEngine.Networking;
/// <summary>
/// 获取到更新脚本
/// </summary>
public class Get : MonoBehaviour
{
private bool IS_ANDROID = false;
private static Text fillAmountTxt;
void Start()
{
transform.Find("Button").GetComponent<Button>().onClick.AddListener(delegate { StartUpdate(); });
fillAmountTxt = transform.Find("Text").GetComponent<Text>();
TestPrint();
}
//输出测试
void TestPrint()
{
Debug.Log("*******测试打印所有文件目录*******");
var _str = "";
string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
foreach (string idx in bundleFiles)
{
var _r = @"\Android\";
if (IS_ANDROID) _r = "/Android/";
var _s = idx.Replace(GetTerracePath() + _r, "");
_s = _s.Replace(@"\", "/");
Debug.Log("替换过程:" + idx + " " + GetTerracePath() + " " + _s);
_str += _s + "\n";
}
transform.Find("CeshiText").GetComponent<Text>().text = _str;
Debug.Log("**************结束打印************");
}
//更新版本
private void StartUpdate()
{
StartCoroutine(VersionUpdate());
}
private int allfilesLength = 0;
/// <summary>
/// 版本更新
/// </summary>
/// <returns></returns>
IEnumerator VersionUpdate()
{
var uri = GameProp.Inst.HttpDownLoadUrl("http://192.168.6.178/Chief/MD5Android/Bundle.txt");
var request = UnityWebRequest.Get(uri);
yield return request.SendWebRequest();
var _isGet = !(request.isNetworkError || request.isNetworkError);
if (_isGet)
{
List<BundleInfo> bims = new List<BundleInfo>();
FileMD5 date = JsonUtility.FromJson<FileMD5>(request.downloadHandler.text);
DeleteOtherBundles(date);//删除所有不受版本控制的文件
Debug.LogError(request.downloadHandler.text);
//Debug.Log(data.Contains());
var _list = date.files;
string md5, file, path;
int lenth;
for (int i = 0; i < _list.Length; i++)
{
MD5Message _md5 = _list[i];
Debug.Log(_md5.file + " " + _md5.fileLength + " " + _md5.md5);
file = _md5.file;
path = PathUrl(file);
md5 = GetMD5HashFromFile(path);
if (string.IsNullOrEmpty(md5) || md5 != _md5.md5)
{
bims.Add(new BundleInfo()
{
Url = HttpDownLoadUrl(file),
Path = path
});
lenth = int.Parse(_md5.fileLength);
allfilesLength += lenth;
}
}
if (bims.Count > 0)
{
Debug.LogError("开始尝试更新");
StartCoroutine(DownLoadBundleFiles(bims, (progress) => {
OpenLodingShow("自动更新中...", progress, allfilesLength);
}, (isfinish) => {
if (isfinish)
StartCoroutine(VersionUpdateFinish());
else
{
StartCoroutine(VersionUpdate());
}
}));
}
else
{
StartCoroutine(VersionUpdateFinish());
}
}
}
// 删除所有不受版本控制的所有文件
void DeleteOtherBundles(FileMD5 _md5)
{
Debug.LogError("~~~~~~~~~~开始删除~~~~~~~");
string[] bundleFiles = Directory.GetFiles(GetTerracePath(), "*.*", SearchOption.AllDirectories);
foreach (string idx in bundleFiles)
{
var _r = @"\Android\";
if (IS_ANDROID) _r = "/Android/";
var _s = idx.Replace(GetTerracePath() + _r, "");
_s = _s.Replace(@"\", "/");
if (!FindNameInFileMD5(_md5, _s))
{
File.Delete(idx);
Debug.LogError(_s + "不存在");
}
}
Debug.Log("~~~~~~~结束删除~~~~~~~");
}
/// <summary>获取文件的md5校验码</summary>
public string GetMD5HashFromFile(string fileName)
{
if (File.Exists(fileName))
{
FileStream file = new FileStream(fileName, FileMode.Open);
MD5 md5 = new MD5CryptoServiceProvider();
byte[] retVal = md5.ComputeHash(file);
file.Close();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < retVal.Length; i++)
sb.Append(retVal[i].ToString("x2"));
return sb.ToString();
}
return null;
}
static bool FindNameInFileMD5(FileMD5 date, string _name)
{
foreach (var _m in date.files)
{
if (_m.file == _name) return true;
}
return false;
}
//脚本替换(lua等)验证
IEnumerator VersionUpdateFinish()
{
Debug.Log("lua验证");
var uri = GetTerracePath(IS_ANDROID) + "/Android/Scripts/ceshi.lua.txt";
var request = UnityWebRequest.Get(uri);
yield return request.SendWebRequest();
var _isGet = !(request.isNetworkError || request.isNetworkError);
if (_isGet)
{
Debug.LogError(request.downloadHandler.text);
transform.Find("CeshiText").GetComponent<Text>().text = request.downloadHandler.text;
}
StartCoroutine(VersionUpdateImage());
StartCoroutine(VersionUpdateModel());
}
//图片验证
IEnumerator VersionUpdateImage()
{
Debug.Log("图片替换验证");
var uri = GetTerracePath(IS_ANDROID) + "/Android/UI/shop/1.png";
var request = UnityWebRequest.Get(uri);
yield return request.SendWebRequest();
var _isGet = !(request.isNetworkError || request.isNetworkError);
if (_isGet)
{
var _img = DownloadHandlerTexture.GetContent(request); ;
Sprite sprite = Sprite.Create(_img, new Rect(0, 0, _img.width, _img.height), Vector2.zero);
transform.Find("Button").GetComponent<Image>().sprite = sprite;
}
}
//模型验证
IEnumerator VersionUpdateModel()
{
var uri = GetTerracePath(IS_ANDROID) + "/Android/Module/0000001/bql";
var request = UnityWebRequest.Get(uri);
yield return request.SendWebRequest();
var _isGet = !(request.isNetworkError || request.isNetworkError);
if (_isGet)
{
AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(request);
GameObject modelPre = bundle.LoadAsset<GameObject>("bql");
GameObject modelClone = Instantiate(modelPre);
AssetBundle.UnloadAllAssetBundles(false);
}
else
{
Debug.Log("error:" + request.url + "\n" + request.error);
}
}
/// <summary>
/// 路径比对
/// </summary>
public IEnumerator DownLoadBundleFiles(List<BundleInfo> infos, Action<float> LoopCallBack = null, Action<bool> CallBack = null)
{
//Debug.Log("开始路径对比");
int num = 0;
string dir;
for (int i = 0; i < infos.Count; i++)
{
BundleInfo info = infos[i];
Debug.LogError(info.Url);
var uri = info.Url;
var request = UnityWebRequest.Get(uri);
yield return request.SendWebRequest();
var _isGet = !(request.isNetworkError || request.isNetworkError);
if (_isGet)
{
try
{
string filepath = info.Path;
dir = Path.GetDirectoryName(filepath);
if (!Directory.Exists(dir))
Directory.CreateDirectory(dir);
File.WriteAllBytes(filepath, request.downloadHandler.data);
num++;
if (LoopCallBack != null)
LoopCallBack.Invoke((float)num / infos.Count);
Debug.Log(dir + "下载完成");
}
catch (Exception e)
{
Debug.Log("下载失败" + e);
}
}
else
{
Debug.Log("下载错误" + request.error);
}
}
if (CallBack != null)
CallBack.Invoke(num == infos.Count);
}
/// <summary>
/// 记录信息
/// </summary>
public struct BundleInfo
{
public string Path { get; set; }
public string Url { get; set; }
public override bool Equals(object obj)
{
return obj is BundleInfo && Url == ((BundleInfo)obj).Url;
}
public override int GetHashCode()
{
return Url.GetHashCode();
}
}
/// <summary>
/// loadpage展示
/// </summary>
/// <param name="text"></param>
/// <param name="progress"></param>
/// <param name="filealllength"></param>
public void OpenLodingShow(string text = "", float progress = 0, long filealllength = 0)
{
Debug.LogError(text + " " + progress + " " + filealllength);
fillAmountTxt.text = text + progress + " 文件序列:" + filealllength;
if (progress >= 1) fillAmountTxt.text = "更新完成";
}
void Update()
{
}
private void OnDestroy()
{
//if (DestoryLua != null)
// DestoryLua();
//UpdateLua = null;
//DestoryLua = null;
//StartLua = null;
// LuaEnvt.Dispose();
}
string HttpDownLoadUrl(string _str)
{
return "http://192.168.6.178/Chief/MD5Android/" + _str;
}
//根据不同路径,对下载路径进行封装
string PathUrl(string _str)
{
var _path = GetTerracePath() + "/Android/" + _str;
return _path;
}
//获得不同平台的路径
string GetTerracePath(bool _isAndroid = false)
{
string sPath = Application.persistentDataPath;
if (_isAndroid) sPath = "file://" + sPath;
return sPath;
}
}
这里将所有WWW方法替换为了UnityWebRequest,另外API:DownloadHandlerAssetBundle.GetContent十分好用。
不过由于项目中代码,现在已经与测试版本相差过大,所以这个测试版本没测试,可能有部分细微差别,请大家自行取用。
免责声明:本文系网络转载或改编,未找到原创作者,版权归原作者所有。如涉及版权,请联系删