3.默认加载界面处理
3.1 知识点
Loading预设体制作
- 可以包含背景图,进度跳图,说明文本
loading界面逻辑处理,提供开始更新方法,使用AB包更新管理器检查更新,更新完后启动Ilruntime。可以尝试修改AB包更新管理器和Ilruntime管理器,多加几个委托方便查看信息。
public class LoadingPanel : MonoBehaviour
{
// 进度条
public Image imgPro;
// 当前加载信息说明
public Text infoTxt;
void Start()
{
imgPro.rectTransform.sizeDelta = new Vector2(0, 50);
infoTxt.text = "资源加载中";
}
// 需要去更新资源服务器上的AB包
public void BeginUpdate()
{
// 第一个委托是用于 AB包下载更新结束后 处理逻辑的
// 第二个委托是用于更新当前加载信息的
AssetBundleUpdateManager.Instance.CheckUpdate(ABUpdateOverDoSomthing, (info) => { infoTxt.text = info; },
(nowNum, maxNum) => { imgPro.rectTransform.sizeDelta = new Vector2(nowNum / maxNum * 1600, 50); });
}
// AB包更新完毕后需要去处理ILRuntime初始化相关的逻辑
public void ABUpdateOverDoSomthing(bool isOver)
{
if (!isOver)
{
infoTxt.text = "AB包下载更新出错,请检查网络连接或联系服务商";
return;
}
infoTxt.text = "资源加载结束";
// ILRuntime的初始化相关
ILRuntimeManager.Instance.StartILRuntime(() =>
{
// ILRuntime相关内容加载结束 就可以执行游戏逻辑了
infoTxt.text = "游戏初始化完毕";
// 热更相关逻辑执行
}, (info) => { infoTxt.text = info; });
}
}
总结
- Loading预设体可以包含背景图、进度条和说明文本。
- loading界面逻辑处理包括开始更新方法,使用AB包更新管理器检查更新,更新完后启动Ilruntime。可以尝试修改AB包更新管理器和Ilruntime管理器,多加几个委托方便查看信息。
3.2 知识点代码
LoadingPanel
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Serialization;
using UnityEngine.UI;
public class LoadingPanel : MonoBehaviour
{
//进度条
public Image imgPro;
//当前加载信息说明
public Text infoTxt;
void Start()
{
imgPro.rectTransform.sizeDelta = new Vector2(0, 50);
infoTxt.text = "资源加载中";
}
//第一:需要去更新资源服务器上的AB包
public void BeginUpdate()
{
//第一个委托 是用于 AB包下载更新结束后 处理逻辑的
//第二个委托 是用于 更新当前加载信息的
AssetBundleUpdateManager.Instance.CheckUpdate(ABUpdateOverDoSomthing, (info) => { infoTxt.text = info; },
(nowNum, maxNum) => { imgPro.rectTransform.sizeDelta = new Vector2(nowNum / maxNum * 1600, 50); });
}
//第二:AB包更新完毕后 需要去处理ILRuntime初始化相关的逻辑
public void ABUpdateOverDoSomthing(bool isOver)
{
if (!isOver)
{
infoTxt.text = "AB包下载更新出错,请检查网络连接或联系服务商";
return;
}
infoTxt.text = "资源加载结束";
//ILRuntime的初始化相关
ILRuntimeManager.Instance.StartILRuntime(() =>
{
//ILRuntime相关内容加载结束 就可以执行游戏逻辑了
infoTxt.text = "游戏初始化完毕";
//热更相关逻辑执行
}, (info) => { infoTxt.text = info; });
}
}
AssetBundleUpdateManager
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Threading.Tasks;
using BaseFramework;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.Networking;
//AB包更新管理器
public class AssetBundleUpdateManager : BaseSingletonInMonoBehaviour<AssetBundleUpdateManager>
{
//用于存储远端AB包信息的字典 之后 和本地进行对比即可完成 更新 下载相关逻辑
private Dictionary<string, AssetBundleInfo> remoteAssetBundleInfoDictionary = new Dictionary<string, AssetBundleInfo>();
//用于存储本地AB包信息的字典 主要用于和远端信息对比
private Dictionary<string, AssetBundleInfo> localAssetBundleInfoDictionary = new Dictionary<string, AssetBundleInfo>();
//这个是待下载的AB包列表文件 存储AB包的名字
private List<string> assetBundleDownLoadList = new List<string>();
//资源服务器IP
private string serverIP = "ftp://127.0.0.1";
/// <summary>
/// 外部调用的检查AB包更新的方法
/// </summary>
/// <param name="overCallBack">检查完的回调</param>
/// <param name="updateInfoCallBack">更新信息的委托</param>
public void CheckUpdate(UnityAction<bool> overCallBack, UnityAction<string> updateInfoCallBack,UnityAction<float,float> updateProCallBack)
{
//为了避免由于上一次报错 而残留信息 所以我们清空它
remoteAssetBundleInfoDictionary.Clear();
localAssetBundleInfoDictionary.Clear();
assetBundleDownLoadList.Clear();
//1.加载远端资源对比文件
DownLoadABCompareFile((isOver) =>
{
updateInfoCallBack("开始更新资源");
if (isOver)
{
updateInfoCallBack("远端对比文件下载结束");
updateProCallBack(1f, 5f);
string remoteInfo = File.ReadAllText(Application.persistentDataPath + "/ABCompareInfo_TMP.txt");
updateInfoCallBack($"远端对比文件字符串为:{remoteInfo}");
updateInfoCallBack("得到远端对比文件信息并开始解析到字典");
GetABCompareFileInfoToDictionary(remoteInfo, remoteAssetBundleInfoDictionary);
updateInfoCallBack("解析远端对比文件到字典完成");
//2.加载本地资源对比文件
GetLocalABCompareFileInfo((isOver) =>
{
if (isOver)
{
updateInfoCallBack("解析本地对比文件完成");
//3.对比他们 然后进行AB包下载
updateInfoCallBack("开始对比");
updateProCallBack(2f, 5f);
foreach (string abName in remoteAssetBundleInfoDictionary.Keys)
{
//1.判断 哪些资源时新的 然后记录 之后用于下载
//这由于本地对比信息中没有叫这个名字的AB包 所以我们记录下载它
if (!localAssetBundleInfoDictionary.ContainsKey(abName))
{
assetBundleDownLoadList.Add(abName);
updateInfoCallBack($"AB包{abName}本地没有,要下载!");
}
//发现本地有同名AB包 然后继续处理
else
{
//2.判断 哪些资源是需要更新的 然后记录 之后用于下载
//对比md5码 判断是否需要更新
if (localAssetBundleInfoDictionary[abName].md5 !=
remoteAssetBundleInfoDictionary[abName].md5)
{
assetBundleDownLoadList.Add(abName);
//如果md5码相等 证明是同一个资源 不需要更新
updateInfoCallBack($"AB包{abName}md5码不同,要更新!");
}
//3.判断 哪些资源需要删除
//每次检测完一个名字的AB包 就移除本地的信息 那么本地剩下来的信息 就是远端没有的内容
//我们就可以把他们删除了
localAssetBundleInfoDictionary.Remove(abName);
}
}
updateInfoCallBack("对比完成");
updateInfoCallBack("删除无用的AB包文件");
updateProCallBack(3f, 5f);
//上面对比完了 那么我们就先删除没用的内容 再下载AB包
//删除无用的AB包
foreach (string abName in localAssetBundleInfoDictionary.Keys)
{
//如果可读写文件夹中有内容 我们就删除它
//默认资源中的 信息 我们没办法删除
if (File.Exists(Application.persistentDataPath + "/" + abName))
{
File.Delete(Application.persistentDataPath + "/" + abName);
updateInfoCallBack($"AB包{abName}在本地多余,需要删除!");
}
}
updateInfoCallBack("下载和更新AB包文件");
updateProCallBack(4f, 5f);
//下载待更新列表中的所有AB包
//下载
DownLoadABFile((isOver) =>
{
if (isOver)
{
//下载完所有AB包文件后
//把本地的AB包对比文件 更新为最新
//把之前读取出来的 远端对比文件信息 存储到 本地
updateInfoCallBack("更新本地AB包对比文件为最新");
File.WriteAllText(Application.persistentDataPath + "/ABCompareInfo.txt", remoteInfo);
}
updateProCallBack(5f, 5f);
overCallBack(isOver);
}, updateInfoCallBack);
}
else
{
updateProCallBack(4f, 5f);
overCallBack(false);
}
});
}
else
{
overCallBack(false);
}
});
}
/// <summary>
/// 下载服务器中的文件
/// </summary>
/// <param name="fileName">文件名</param>
/// <param name="localPath">本地路径</param>
/// <returns></returns>
private bool DownLoadFile(string fileName, string localPath)
{
try
{
string pInfo =
#if UNITY_IOS
"IOS";
#elif UNITY_ANDROID
"Android";
#else
"PC";
#endif
//1.创建一个FTP连接 用于下载
FtpWebRequest ftpWebRequest = FtpWebRequest.Create(new Uri(serverIP + "/AB/" + pInfo + "/" + fileName)) as FtpWebRequest;
//2.设置一个通信凭证 这样才能下载(如果有匿名账号 可以不设置凭证 但是实际开发中 建议 还是不要设置匿名账号)
NetworkCredential networkCredential = new NetworkCredential("MrTao", "MrTao");
ftpWebRequest.Credentials = networkCredential;
//3.其它设置
// 设置代理为null
ftpWebRequest.Proxy = null;
// 请求完毕后 是否关闭控制连接
ftpWebRequest.KeepAlive = false;
// 操作命令-下载
ftpWebRequest.Method = WebRequestMethods.Ftp.DownloadFile;
// 指定传输的类型 2进制
ftpWebRequest.UseBinary = true;
//4.下载文件
// ftp的流对象
FtpWebResponse ftpWebResponse = ftpWebRequest.GetResponse() as FtpWebResponse;
Stream downLoadStream = ftpWebResponse.GetResponseStream();
using (FileStream fileStream = File.Create(localPath))
{
//一点一点的下载内容
byte[] bytes = new byte[2048];
//返回值 代表读取了多少个字节
int contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
//循环下载数据
while (contentLength != 0)
{
//写入到本地文件流中
fileStream.Write(bytes, 0, contentLength);
//写完再读
contentLength = downLoadStream.Read(bytes, 0, bytes.Length);
}
//循环完毕后 证明下载结束
fileStream.Close();
downLoadStream.Close();
print(fileName + "下载成功");
return true;
}
}
catch (Exception ex)
{
print(fileName + "下载失败" + ex.Message);
return false;
}
}
/// <summary>
/// 异步下载远端资源对比文件的函数,提供回调通知下载完成状态
/// </summary>
/// <param name="overCallBack">下载完成后的回调 通常会调用获取下载下来的AB包中的信息函数</param>
public async void DownLoadABCompareFile(UnityAction<bool> overCallBack)
{
// 打印本地持久化数据路径
print(Application.persistentDataPath);
// 初始化下载是否成功的标志
bool isOver = false;
// 设置重新下载的最大次数
int reDownLoadMaxNum = 5;
// 本地存储路径,由于多线程不能访问Unity主线程的Application,所以在外面声明
string localPath = Application.persistentDataPath;
// 循环下载,直到下载成功或达到最大重新下载次数
while (!isOver && reDownLoadMaxNum > 0)
{
// 使用异步任务Task.Run在后台线程中执行下载操作
await Task.Run(() =>
{
// 执行下载文件的操作,将下载结果存储在isOver中
isOver = DownLoadFile("ABCompareInfo.txt", localPath + "/ABCompareInfo_TMP.txt");
});
// 递减重新下载的次数
--reDownLoadMaxNum;
}
// 通过回调通知外部下载是否完成
overCallBack?.Invoke(isOver);
}
/// <summary>
/// 获取AB包对比文件中的信息并解析到对应的字典中 可能是远程字典也可能是本地字典
/// </summary>
/// <param name="info"></param>
/// <param name="assetBundleInfoDictionary"></param>
public void GetABCompareFileInfoToDictionary(string info, Dictionary<string, AssetBundleInfo> assetBundleInfoDictionary)
{
//这就不去读取文件了 直接让外部读好了 传进来
// 读取资源对比文件的内容
//string info = File.ReadAllText(Application.persistentDataPath + "/ABCompareInfo_TMP.txt");
// 使用竖线分割字符串,将每个AB包的信息分离
string[] strs = info.Split('|');
string[] infos = null;
// 遍历每个分割后的AB包信息
for (int i = 0; i < strs.Length; i++)
{
// 使用空格分割每个AB包的详细信息
infos = strs[i].Split(' ');
// 记录每一个远端AB包的信息,之后用于对比
assetBundleInfoDictionary.Add(infos[0], new AssetBundleInfo(infos[0], infos[1], infos[2]));
}
}
/// <summary>
/// 本地AB包对比文件加载 并解析信息到本地字典
/// </summary>
public void GetLocalABCompareFileInfo(UnityAction<bool> overCallBack)
{
//Application.persistentDataPath;
//如果可读可写文件夹中 存在对比文件 说明之前我们已经下载更新过了
if (File.Exists(Application.persistentDataPath + "/ABCompareInfo.txt"))
{
print($"可读可写文件夹中 存在对比文件");
StartCoroutine(GetLocalABCOmpareFileInfo("file:///" + Application.persistentDataPath + "/ABCompareInfo.txt", overCallBack));
}
//只有当可读可写中没有对比文件时 才会来加载默认资源(第一次进游戏时才会发生)
else if (File.Exists(Application.streamingAssetsPath + "/ABCompareInfo.txt"))
{
print($"默认资源有对比文件");
string path =
#if UNITY_ANDROID
Application.streamingAssetsPath;
#else
"file:///" + Application.streamingAssetsPath;
#endif
StartCoroutine(GetLocalABCOmpareFileInfo(path + "/ABCompareInfo.txt", overCallBack));
}
//如果两个都不进 证明第一次并且没有默认资源
else
{
print($"可读可写文件夹和默认资源都没有对比文件");
overCallBack(true);
}
}
/// <summary>
/// 协同程序 本地AB包对比文件加载 并解析信息到本地字典
/// </summary>
/// <param name="filePath"></param>
/// <returns></returns>
private IEnumerator GetLocalABCOmpareFileInfo(string filePath, UnityAction<bool> overCallBack)
{
//通过 UnityWebRequest 去加载本地文件
UnityWebRequest unityWebRequest = UnityWebRequest.Get(filePath);
yield return unityWebRequest.SendWebRequest();
//获取文件成功 继续往下执行
if (unityWebRequest.result == UnityWebRequest.Result.Success)
{
print($"本地对比文件字符串为:{unityWebRequest.downloadHandler.text}");
GetABCompareFileInfoToDictionary(unityWebRequest.downloadHandler.text, localAssetBundleInfoDictionary);
overCallBack(true);
}
else
{
overCallBack(false);
}
}
/// <summary>
/// 下载远程AssetBundle文件的函数,异步执行。
/// </summary>
/// <param name="overCallBack">下载完成回调,参数为是否全部下载成功。</param>
/// <param name="updatePro">更新下载进度回调,参数为已下载资源数量和总资源数量。</param>
public async void DownLoadABFile(UnityAction<bool> overCallBack, UnityAction<string> updatePro)
{
//// 1. 遍历字典的键,根据文件名将AssetBundle包添加到待下载列表中
//foreach (string name in remoteAssetBundleInfoDictionary.Keys)
//{
// // 直接将文件名放入待下载列表中
// assetBundleDownLoadList.Add(name);
//}
// 本地存储路径,由于多线程不能访问Unity相关内容,因此在外部声明
string localPath = Application.persistentDataPath + "/";
// 是否下载成功的标志
bool isOver = false;
// 下载成功的文件名列表,用于移除下载成功的内容
List<string> tempList = new List<string>();
// 重新下载的最大次数
int reDownLoadMaxNum = 5;
// 下载成功的资源数量
int downLoadOverNum = 0;
// 需要下载的总资源数量
int downLoadMaxNum = assetBundleDownLoadList.Count;
// while循环的目的是进行n次重新下载,避免网络异常时下载失败
while (assetBundleDownLoadList.Count > 0 && reDownLoadMaxNum > 0)
{
for (int i = 0; i < assetBundleDownLoadList.Count; i++)
{
// 通过Task.Run在异步线程中执行下载操作
isOver = false;
await Task.Run(() =>
{
isOver = DownLoadFile(assetBundleDownLoadList[i], localPath + assetBundleDownLoadList[i]);
});
if (isOver)
{
// 2. 更新下载进度,通知外部已下载资源数量和总资源数量
updatePro(++downLoadOverNum + "/" + downLoadMaxNum);
tempList.Add(assetBundleDownLoadList[i]); // 下载成功记录下来
}
}
// 将下载成功的文件名从待下载列表中移除
for (int i = 0; i < tempList.Count; i++)
assetBundleDownLoadList.Remove(tempList[i]);
// 递减重新下载的次数
--reDownLoadMaxNum;
}
// 所有内容都下载完毕,通过回调告知外部是否下载完成
overCallBack(assetBundleDownLoadList.Count == 0);
}
}
ILRuntimeManager
using ILRuntime.Mono.Cecil.Pdb;
using ILRuntime.Runtime;
using ILRuntime.Runtime.Enviorment;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using BaseFramework;
using UnityEngine;
using UnityEngine.Events;
public class ILRuntimeManager : BaseSingletonInMonoBehaviour<ILRuntimeManager>
{
public AppDomain appDomain;
//dll文件和pdb文件的流对象
private MemoryStream dllStream;
private MemoryStream pdbStream;
//是否已经加载了对应的文件
private bool isStart = false;
//是否是调试模式
private bool isDebug = false;
/// <summary>
/// 启动ILRuntime 加载对应的dll和pdb文件
/// </summary>
public void StartILRuntime(UnityAction callBack,UnityAction<string> infoCallBack)
{
if (!isStart)
{
isStart = true;
//初始化AppDomain
appDomain = new AppDomain(ILRuntimeJITFlags.JITOnDemand);
//加载对应的dll和pdb等文件 需要从AB包中去加载
//通过AB包管理器 异步加载DLL文件信息
infoCallBack("开始更新dll文件");
AssetBundleManager.Instance.LoadAssetBundleResourceAsync<TextAsset>("dll_res", "HotFix_Project.dll",
(dll) =>
{
//异步加载完dll后 再去异步加载pdb文件 加载结束后 再使用他们来进行初始化
infoCallBack("开始更新pdb文件");
AssetBundleManager.Instance.LoadAssetBundleResourceAsync<TextAsset>("dll_res", "HotFix_Project.pdb",
(pdb) =>
{
//根据加载的文本信息 初始化 对应的两个流对象
dllStream = new MemoryStream(dll.bytes);
pdbStream = new MemoryStream(pdb.bytes);
//利用初始化的流对象 进行ILRuntime的初始化
appDomain.LoadAssembly(dllStream, pdbStream, new PdbReaderProvider());
infoCallBack("开始初始化Ilruntime相关的操作");
//初始化相关的操作
InitILRuntime();
if (isDebug)
{
infoCallBack("开始等待调试程序接入");
StartCoroutine(WaitDebugger(callBack));
}
else
{
infoCallBack("开始执行回调");
//加载结束 初始化结束 把逻辑交给外部继续处理
callBack?.Invoke();
}
});
});
}
}
private void InitILRuntime()
{
//如果想使用Unity自带的性能调试窗口 调试ILRuntime相关内容 就需要加入该行代码
appDomain.UnityMainThreadID = Thread.CurrentThread.ManagedThreadId;
}
IEnumerator WaitDebugger(UnityAction callBack)
{
while (!appDomain.DebugService.IsDebuggerAttached)
yield return null;
yield return new WaitForSeconds(1f);
callBack?.Invoke();
}
/// <summary>
/// 停止ILRutnime 卸载对应文件
/// </summary>
public void StopILRuntime()
{
if (dllStream != null)
dllStream.Close();
if (pdbStream != null)
pdbStream.Close();
dllStream = null;
pdbStream = null;
appDomain = null;
isStart = false;
}
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com