GameFrameWork+删除指定的(分包)ab资源
这个要结合我之前的GameFrameWork中的分包功能的实现一起看。如果没看,先去看这篇
前言
项目的前提是已经完成了GF框架的分包下载的功能,因为玩法过多,可能有十几个玩法,如果用户要删除的话,如何在不改变整体流程的同时可以完成删除,也就是新的玩法可能是预制的,也可能是全部走下载,或者部分走下载,总之在这个或者多个玩法被删除的情况下,还能通过下载下来进入该玩法。
拆解需求
1.删除的话,首先要找到路径,找到路径了才可以删除;
2.删除了下次点进来的时候,还能再下载到本地。
实现
仔细看了下GF现在的框架是不存在这样的功能,需要先看下他现在的流程,删除的功能实现比较简单,只需要确定路径,删除就可以了,问题主要在于如何下次还能重新删除。
1.0版本
除了通用的共有资源我放在了一个ab里面,其他的把每个玩法拆成了单独的ab,所以我删除的时候直接根据他的持久化路径+ab包的名字就可以了,核心代码如下
string path = Application.persistentDataPath + "/"+abName+".dat";
Debug.Log("当前的路径是-----------------"+path);
if (File.Exists(path))
{
File.Delete(path);
Debug.Log("删除文件--"+path);
}
2.0版本
结果:这样做的话删除了无法在运行的时候下载,只能退出重进才进,因为他每次进来的时候有个校验流程,这个时候他才会发现这个资源是需要下载的,不然你这个资源组如果已经在本地下载好了,你即使这样删除他也认为这个资源组ab无需下载,因为在ProcedureResourcesCheck中我们下载的时候是根据资源组是否已经准备好了,这个逻辑如下:
1.启动的时候他会获取所有的资源组;
foreach (UpdatableVersionList.Resource resource in resources)
{
...
defaultResourceGroup.AddResource(resourceName, resource.Length, resource.CompressedLength);
//在这个循环里面获取所有的资源组;
}
2.当我们指定下载资源组的时候他会改变对应资源组的状态,由!Ready → Ready
GameEntry.Resource.UpdateResources(drScene.AssetName, OnUpdateResourcesComplete);
//这是加载分包的逻辑
↓
m_ResourceUpdater.UpdateResources(resourceGroup);
↓
if (string.IsNullOrEmpty(resourceGroup.Name))
{
foreach (KeyValuePair<ResourceName, UpdateInfo> updateInfo in m_UpdateCandidateInfo)
{
m_UpdateWaitingInfo.Add(updateInfo.Value);
}
}
else
{
resourceGroup.InternalGetResourceNames(m_CachedResourceNames);
foreach (ResourceName resourceName in m_CachedResourceNames)
{
UpdateInfo updateInfo = null;
if (!m_UpdateCandidateInfo.TryGetValue(resourceName, out updateInfo))
{
continue;
}
m_UpdateWaitingInfo.Add(updateInfo);
}
//这一段是不管资源组是不是为空,都会加到m_UpdateWaitingInfo数组中
//然后我们会看到update有这样的逻辑
↓
...
while (m_ApplyWaitingInfo.Count > 0)
{
ApplyInfo applyInfo = m_ApplyWaitingInfo.Dequeue();
if (ApplyResource(applyInfo))
{
return;
}
...
if (m_UpdateWaitingInfo.Count > 0)
{
int freeCount = m_DownloadManager.FreeAgentCount - m_DownloadManager.WaitingTaskCount;
if (freeCount > 0)
{
for (int i = 0, count = 0; i < m_UpdateWaitingInfo.Count && count < freeCount; i++)
{
if (DownloadResource(m_UpdateWaitingInfo[i]))
{
...
//大概意思就是先看看资源组有没有下好,如果下好了,就直接应用,如果没有,那就说明要下载,那么就去下载
↓
private bool DownloadResource(UpdateInfo updateInfo)
{
if (updateInfo.Downloading)
{
return false;
}
updateInfo.Downloading = true;
string resourceFullNameWithCrc32 = updateInfo.ResourceName.Variant != null ? Utility.Text.Format("{0}.{1}.{2:x8}.{3}", updateInfo.ResourceName.Name, updateInfo.ResourceName.Variant, updateInfo.HashCode, DefaultExtension) : Utility.Text.Format("{0}.{1:x8}.{2}", updateInfo.ResourceName.Name, updateInfo.HashCode, DefaultExtension);
Debug.Log("资源组"+updateInfo.ResourceName+"下载地址是----"+updateInfo.ResourcePath);
m_DownloadManager.AddDownload(updateInfo.ResourcePath, Utility.Path.GetRemotePath(Path.Combine(m_ResourceManager.m_UpdatePrefixUri, resourceFullNameWithCrc32)), updateInfo);
return true;
}
↓
//然后她在ApplyResource()和OnDownloadSuccess之后都有个改变状态的过程,标记资源下载完成,后面会修改到这里
m_ResourceManager.m_ResourceInfos[updateInfo.ResourceName].MarkReady()
//所以我们在修改的时候需要修改这个状态
3.改写方法
/// <summary>
/// 标记资源准备完毕。
/// </summary>
public void MarkReady(bool isReady = true)
{
// m_Ready = true;
m_Ready = isReady;
}
综上,修改如下:
1.当我想修改的时候,发现这个类是个内部的密封类,本着修改最小的原则,我准备获取这个ResourceManager,然后通过他来修改状态变化;
2.通过ResourceComponenetExtension来获取这个ResruourceManager,从而修改ready的状态;
public static class ResourceComponentExtension
{
public static void ChangeResourceGroupReadyStateByResourceName(this ResourceComponent resource, string resourceName,
bool isReady = false)
{
var resourceManager = resource.GetResourceManager();
resourceManager.ChangeResourceGroupReadyState(resourceName,isReady);
}
3.同时需要在IResourceManager中添加对应的接口:
/// <summary>
/// 改变当前资源组的状态
/// </summary>
/// <param name="resourceGroupName"></param>
/// <param name="isReady"></param>
public void ChangeResourceGroupReadyState(string resourceGroupName, bool isReady);
4.ResourceManager中的具体实现:
public void ChangeResourceGroupReadyState(string resourceGroupName,bool isReady = false)
{
foreach (var item in m_ResourceInfos)
{
if (item.Key.Name == resourceGroupName)
{
m_ResourceInfos[item.Key].MarkReady(isReady);
m_ReadWriteResourceInfos.Remove(item.Key);
OnCheckerResourceNeedUpdate(item.Key,item.Value.FileSystemName,item.Value.LoadType,item.Value.Length,item.Value.HashCode,item.Value.CompressedLength,m_oldCompressedHashCodeDic[item.Key]);
Debug.Log("当前的资源组==="+resourceGroupName+"-----state-----is---"+isReady);
break;
}
}
}
//m_ReadWriteResourceInfos注意这里
这个在ResourceManager.ResourceUpdater下面的ApplyResource()和DownloadSuccess()里面都把需要更新的资源组加到了这个列表里面,我们在删除的同事,要把他们对应的从列表里面拿出来,这样下次就会重新加,或者你自己加个判断,因为其他也用到了这个列表我选择移除
//m_ResourceManager.m_ReadWriteResourceInfos.Add(updateInfo.ResourceName, new ReadWriteResourceInfo(updateInfo.FileSystemName, updateInfo.LoadType, updateInfo.Length, updateInfo.HashCode));
//OnCheckerResourceNeedUpdate()为什么使用这个方法,因为正常更新的时候就是需要这个方法,这个就是吧这个资源组加到更新列表中
//m_UpdateCandidateInfo 他有一个候选更新的列表
/// <summary>
/// 增加资源更新。
/// </summary>
/// <param name="resourceName">资源名称。</param>
/// <param name="fileSystemName">资源所在的文件系统名称。</param>
/// <param name="loadType">资源加载方式。</param>
/// <param name="length">资源大小。</param>
/// <param name="hashCode">资源哈希值。</param>
/// <param name="compressedLength">压缩后大小。</param>
/// <param name="compressedHashCode">压缩后哈希值。</param>
/// <param name="resourcePath">资源路径。</param>
public void AddResourceUpdate(ResourceName resourceName, string fileSystemName, LoadType loadType, int length, int hashCode, int compressedLength, int compressedHashCode, string resourcePath)
{
m_UpdateCandidateInfo.Add(resourceName, new UpdateInfo(resourceName, fileSystemName, loadType, length, hashCode, compressedLength, compressedHashCode, resourcePath));
}
然后从上面俩张图就能看出来,第一张是把他加到可更新的列表里面,告诉内存他还没有下载下来,然后等更新的时候传入这个要更新的资源组,他就会去这个列表里面取他的对应的信息来下载;
5.最后,要说下我加的一个字典,因为我们使用了这个标记方法
OnCheckerResourceNeedUpdate(item.Key,item.Value.FileSystemName,item.Value.LoadType,item.Value.Length,item.Value.HashCode,item.Value.CompressedLength,m_oldCompressedHashCodeDic[item.Key]);
//m_oldCompressedHashCodeDic这个是用来记录之前所有的资源组的compressedHashCode的值的因为这个循环的item是resource
可以看到RerouceInfo里面没有这个字段,所以我需要记录下,然后每次进来都会要走一遍Check;
6.测试:逻辑部分已经结束测试逻辑如下
public void deleteSpecifyAbByName(string abName)
{
bool flag = GetCurResourceGroupIsLocalComplete(abName);
string path1 = Application.persistentDataPath + "/"+abName+".dat";
Debug.Log("资源组当前路径是----------------------"+path1);
if (flag)
{
Debug.Log("本地有该资源组");
ChangeResourceGroupStatus(abName);
string path = Application.persistentDataPath + "/"+abName+".dat";
Debug.Log("当前的路径是-----------------"+path);
if (File.Exists(path))
{
File.Delete(path);
Debug.Log("删除文件--"+path);
}
}
else
{
Debug.Log("本地没有该资源组");
}
}
//判定本地资源组是否已经资源完整
private bool GetCurResourceGroupIsLocalComplete(string resourceGourpName)
{
bool flag = false;
List<IResourceGroup> results = new List<IResourceGroup>();
GameEntry.Resource.GetAllResourceGroups(results);
for (int i = 0; i < results.Count; i++)
{
if (results[i].Name == resourceGourpName)
{
if (results[i].Ready)
{
flag = true;
break;
}
}
}
return flag;
}
//可以直接在Update中添加按键事件 这个根据自己的需求来,或者是按钮点击,就会看到自己的持久化路径下面的资源组已经删除
//必须不知道资源组在哪,我加了打印
7.PC端和安卓端删除均已验证,不管删除场景包不包括当前场景,均显示正常。。
3.0版本
上面基本已经满足了业务需要,但是还存在一种情况,不过我觉得已经没有必要了,就是玩家在玩的同时,自己删除了app的缓存数据,或者去了对应的文件夹删了对应的ab包,这时候会有问题,为什么?
因为,我们手动删除的时候给对应的资源组做了标记,并且也将他们移除了对应的m_ReadWriteResourceInfos列表,但是你如果物理删除的话,这些标记就没有变化,就会可能崩溃或者黑屏,引发不可控的问题,但是如果你只要删掉进程重新进,又会正常,因为这时候会检测本地的文件,所以都会正常。所以,我觉得问题不大,毕竟这是非常规操作。但是,如果你非要改,有没有办法?
我试了一半,如果非要规避这样的操作,那么就需要在分包逻辑里面不仅判断资源组是否完全,还要判断本地是否有该文件,然后你需要吧通用的ab和当前的ab都判定一遍,如果没有需要把他们标记为!Ready,移除对应的列表,并且要加载通过的ab和当前要加入的ab,我觉得没有必要,所以就不加了。但我本地已经验证过pc端的逻辑,暂未发现异常。
最后,所以2.0版本基本已经可以实现删除指定的玩法(分包资源),不管你是单个删除还是批量删除。
ps:欢迎大家进q群交流游戏开发的问题(632313288)