一、名词解释
资源包:
点击 LuaFramework | Build XXX(平台名) Resource,框架会自动将自定义指定的资源打包到StreamingAssets文件夹,这个文件夹下的unity3d文件就是资源包,它是一种u3d自己的压缩格式,也被称为AssetBundle包。
资源:
资源经过打包成为资源包,如果在游戏里想用资源包里的内容的话,就需要先加载资源包到内存,然后解压这个资源包,获取资源包里包含的部分资源。
二、热更新涉及文件夹
资源目录:apk文件(安卓安装文件)其实也是一种压缩格式,安装apk之后,安卓系统会为游戏自动创建一个资源目录,用来存放apk解压出来的东西。需要特别注意的一点是,资源目录里的东西是只能读不能写的,也就是说,我们不能随意地从网上下载文件替换资源目录里的资源,所以我们需要一个可读可写的目录以便我们用下载的资源替换原资源实现热更新。由于lua文件可以当作一种资源,所以lua里的代码也是可以热更新的。资源目录路径通过Util.AppContentPath()获取,安卓上大概是这样的:jar:file///data/app.com/com.XXX.XXX-1/base.apk!/assets/(root之后才能看到),用编辑器的话,是工程所在文件夹下的StreamingAssets。
数据目录:安卓创建的用来存放游戏数据用的,可以随意读写。第一次打开游戏的时候,把资源包从资源目录复制到这里,此后每次启动游戏都会比较这里的文件和网络上的资源包,如果不一样的话,就下载下来替换这里的资源包。数据目录路径通过Util.DataPath获取,安卓上大概这样data/user/0/com.XXX.XXX-1/files/mygamename/(root之后才能看到),用编辑器的话,会创建在C盘。
网络资源目录:存放游戏资源的网址,游戏开启后,程序会从网络资源地址下载一些更新的文件到数据目录。版本控制是通过files.txt实现的,里面存放着资源文件的名称和md5码。程序会先下载“网络资源地址”上的files.txt,然后与“数据目录”中文件的md5码做比较,更新有变化的文件。
三、热更新的三个过程
复制资源:将“游戏资源目录”的内容复制到“数据目录中”(只有在第一次打开游戏的时候会有此过程)
GameManager.OnExtractResource
IEnumerator OnExtractResource() {
string dataPath = Util.DataPath; //数据目录
string resPath = Util.AppContentPath(); //游戏包资源目录
if (Directory.Exists(dataPath)) Directory.Delete(dataPath, true);
Directory.CreateDirectory(dataPath);
string infile = resPath + "files.txt";
string outfile = dataPath + "files.txt";
if (File.Exists(outfile)) File.Delete(outfile);
string message = "正在解包文件:>files.txt";
Debug.Log(infile);
Debug.Log(outfile);
if (Application.platform == RuntimePlatform.Android) {
WWW www = new WWW(infile);
yield return www;
if (www.isDone) {
File.WriteAllBytes(outfile, www.bytes);
}
yield return 0;
} else File.Copy(infile, outfile, true);
yield return new WaitForEndOfFrame();
//释放所有文件到数据目录
string[] files = File.ReadAllLines(outfile);
foreach (var file in files) {
string[] fs = file.Split('|');
infile = resPath + fs[0]; //
outfile = dataPath + fs[0];
message = "正在解包文件:>" + fs[0];
Debug.Log("正在解包文件:>" + infile);
facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
string dir = Path.GetDirectoryName(outfile);
if (!Directory.Exists(dir)) Directory.CreateDirectory(dir);
if (Application.platform == RuntimePlatform.Android) {
WWW www = new WWW(infile);
yield return www;
if (www.isDone) {
File.WriteAllBytes(outfile, www.bytes);
}
yield return 0;
} else {
if (File.Exists(outfile)) {
File.Delete(outfile);
}
File.Copy(infile, outfile, true);
}
yield return new WaitForEndOfFrame();
}
message = "解包完成!!!";
facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
yield return new WaitForSeconds(0.1f);
message = string.Empty;
//释放完成,开始启动更新资源
StartCoroutine(OnUpdateResource());
}
下载资源:
从网络比对下载资源
GameManager.OnUpdateResource
IEnumerator OnUpdateResource() {
if (!AppConst.UpdateMode) {
OnResourceInited();
yield break;
}
string dataPath = Util.DataPath; //数据目录
string url = AppConst.WebUrl;
string message = string.Empty;
string random = DateTime.Now.ToString("yyyymmddhhmmss");
string listUrl = url + "files.txt?v=" + random;
Debug.LogWarning("LoadUpdate---->>>" + listUrl);
WWW www = new WWW(listUrl); yield return www;
if (www.error != null) {
OnUpdateFailed(string.Empty);
yield break;
}
if (!Directory.Exists(dataPath)) {
Directory.CreateDirectory(dataPath);
}
File.WriteAllBytes(dataPath + "files.txt", www.bytes);
string filesText = www.text;
string[] files = filesText.Split('\n');
for (int i = 0; i < files.Length; i++) {
if (string.IsNullOrEmpty(files[i])) continue;
string[] keyValue = files[i].Split('|');
string f = keyValue[0];
string localfile = (dataPath + f).Trim();
string path = Path.GetDirectoryName(localfile);
if (!Directory.Exists(path)) {
Directory.CreateDirectory(path);
}
string fileUrl = url + f + "?v=" + random;
bool canUpdate = !File.Exists(localfile);
if (!canUpdate) {
string remoteMd5 = keyValue[1].Trim();
string localMd5 = Util.md5file(localfile);
canUpdate = !remoteMd5.Equals(localMd5);
if (canUpdate) File.Delete(localfile);
}
if (canUpdate) { //本地缺少文件
Debug.Log(fileUrl);
message = "downloading>>" + fileUrl;
facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
/*
www = new WWW(fileUrl); yield return www;
if (www.error != null) {
OnUpdateFailed(path); //
yield break;
}
File.WriteAllBytes(localfile, www.bytes);
*/
//这里都是资源文件,用线程下载
BeginDownload(fileUrl, localfile);
while (!(IsDownOK(localfile))) { yield return new WaitForEndOfFrame(); }
}
}
yield return new WaitForEndOfFrame();
message = "更新完成!!";
facade.SendMessageCommand(NotiConst.UPDATE_MESSAGE, message);
OnResourceInited();
}
加载资源:加载资源包内的资源到内存
PanelManager.CreatePanel
public void CreatePanel(string name, LuaFunction func = null) {
string assetName = name + "Panel";
string abName = name.ToLower() + AppConst.ExtName;
ResManager.LoadPrefab(abName, assetName, delegate(UnityEngine.Object[] objs) {
if (objs.Length == 0) return;
// Get the asset.
GameObject prefab = objs[0] as GameObject;
if (Parent.FindChild(name) != null || prefab == null) {
return;
}
GameObject go = Instantiate(prefab) as GameObject;
go.name = assetName;
go.layer = LayerMask.NameToLayer("Default");
go.transform.SetParent(Parent);
go.transform.localScale = Vector3.one;
go.transform.localPosition = Vector3.zero;
go.AddComponent<LuaBehaviour>();
if (func != null) func.Call(go);
Debug.LogWarning("CreatePanel::>> " + name + " " + prefab);
});
}
四、可更新的loading界面
前提:
1)如果想更新,则必须把loading界面打包进资源包
2)在第一次打开游戏的时候,下载资源阶段之前,还没有loading界面的资源包,但是此刻无疑是必须显示loading的
3)生成loading界面的两种过程:
使用Resource.Load(不可热更新)
public void CreateLoadingPanelFromResource()
{
_MemoryPoolManager_ memMgr = AppFacade.Instance.GetManager<_MemoryPoolManager_>(ManagerName._Pool_);
Debug.Log("--------------------Resources加载loading--------------------------------");
memMgr.AddPrefabPoolRS("LoadingPanel", 1, 1, true, null);
// memMgr.AddPrefabPoolAB ("loading", "LoadingPanel", 1, 1, true, null);
Transform loadingPanel = _MemoryPoolManager_.Spawn("LoadingPanel", true);
loadingPanel.SetParent(GameObject.Find("2DERoot/Resolution").transform);
loadingPanel.localPosition = Vector3.zero;
loadingPanel.localScale = new Vector3(1, 1, 1);
_slider = loadingPanel.FindChild("LoadingSlider").GetComponent<Slider>();
_text = loadingPanel.FindChild("LoadingSlider/Text").GetComponent<Text>();
_text.gameObject.SetActive(true);
_text.transform.localPosition = Vector3.zero;
_slider.value = 5;
_text.text = "准备启动游戏";
_isStartupLoading = true;
KeyFrameWork.MVC.SendEvent(FWConst.E_ShowLog, "Resources load finish");
使用加载AssetBundle资源包的方式
public IEnumerator CreateLoadingPanelFromAssetBundle()
{
string url = Util.GetRelativePath() + "loading.unity3d";
Debug.Log("--------------------AssetBundle加载Loading--------------------------------" + url);
WWW www = WWW.LoadFromCacheOrDownload(url, 0, 0);
yield return www;
AssetBundle bundle = www.assetBundle;
GameObject asset = bundle.LoadAsset<GameObject>("LoadingPanel");
Transform loadingPanel = Instantiate<GameObject>(asset).GetComponent<Transform>();
//memMgr.AddPrefabPoolRS("LoadingPanel", 1, 1, true, null);
//Transform loadingPanel = _MemoryPoolManager_.Spawn ("LoadingPanel", true);
loadingPanel.SetParent(GameObject.Find("2DERoot/Resolution").transform);
loadingPanel.localPosition = Vector3.zero;
loadingPanel.localScale = new Vector3(1, 1, 1);
loadingPanel.name = "LoadingPanel(Clone)001";
_slider = loadingPanel.FindChild("LoadingSlider").GetComponent<Slider>();
_text = loadingPanel.FindChild("LoadingSlider/Text").GetComponent<Text>();
_text.gameObject.SetActive(true);
_slider.value = 5;
_isStartupLoading = true;
//加载loading完成后再开始lua,保证lua能顺利接管loading
AppFacade.Instance.StartUp(); //启动游戏
KeyFrameWork.MVC.SendEvent(FWConst.E_ShowLog, "AssetBundle load finish");
}
游戏开始时,判断是否为第一次开启游戏:
使用(Directory.Exists(Util.DataPath) && Directory.Exists(Util.DataPath + “lua/”) && File.Exists(Util.DataPath + “files.txt”)判断,如果是,则使用Resource.Load,如果不是则使用加载AssetBundle资源包的方式。
AssetBundle资源包时的顺序
由于ResourceManager需要Manifest关联文件(用以确定各个资源包之间的引用关系)进行初始化,所以ResourceManager初始化必在下载资源之后,而下载资源时无疑是需要显示loading的,故顺序是:加载AssetBundle资源包-》下载资源-》ResourceManager初始化。
在这个顺序下,虽然ResourceManager有加载AssetBundle资源包的接口,但是不能用。
而加载AssetBundle资源包是需要异步加载的,也就是说,显示loading需要1秒左右的时间(根据具体loading资源和手机硬件而定),若是需要在lua里接管loading(通过Find函数找到loading界面然后在lua里做一个引用),则必须在loading资源包加载完毕后才能执行lua接管loading的代码。