GameFramework解析:资源(Resource)
前言
资源这个模块有太多太多的东西,一开始我也想过把这个模块完完整整的去做一下详解,把每一个小模块都拿出来说一下,但这玩意要写的话,能说的地方和能讲的拓展实在太多了,以及伴随着工作上事情的越来越多,这个文章应该会逐步采用逐步更新的方式。
先来看一下GF官方的说明:
为了保证玩家的体验,我们不推荐再使用同步的方式加载资源,由于 Game Framework 自身使用了一套完整的异步加载资源体系,因此只提供了异步加载资源的接口。不论简单的数据表、本地化字典,还是复杂的实体、场景、界面,我们都将使用异步加载。同时,Game Framework 提供了默认的内存管理策略(当然,你也可以定义自己的内存管理策略)。多数情况下,在使用 GameObject 的过程中,你甚至可以不需要自行进行 Instantiate 或者是 Destroy 操作。
打包方式和出包形式
先简单说一下通过ResourceEditor进行分包以及使用ResouceBuilder进行出包。以及这两个工具的使用方式和详细参数含义,可以参考官方文档和烟雨的文档,贴在了参考文档里面,方便自行查阅,这里就不浪费时间了。
打包菜单栏在GameFramework下的ResourceTools选项中。
ResourceEditor:
如果你用的是官方的StarForce项目,那么打开就能看到已经分好的资源列表。这根据是否配置了FileSystem,对于配置了FileSystem的资源,最终在Package中会被整合在对应的包中,添加ResourceGroup参数则是进行会资源分组,如Base、Music。
对应数据的显示和配置都在ResourceCollection.xml文件里面。
这里以StarForce项目举例说明,通过ResourceBuilder打包,在Full文件夹下面可以看见所有已经被打好的包:
在Package文件则可以看到分好类后的包,这里的分类是根据FileSystem来的
- 资源文件总数为128;
- 资源列表数量为21个,总共分成了21个资源包;
- FileSystem总共有3个,分别是GameData,Resources以及UI,最终对应着Package文件夹下3个包;
- ResourceGroups总共有2个,分别为base和Music。
其他的环境配置和使用方式网上有很多资料说明,如果有需要可以在通过参考文档里面的链接进行自行查找。
初次使用的话,注意几个点就行:
关于BuildInfo里面CheckVersionUrl的地址,在请求的时候会自动加上平台信息,没加的话路径就会出错。
1
2
3ProcedureCheckVersion.cs
GameEntry.WebRequest.AddWebRequest(Utility.Text.Format(GameEntry.BuiltinData.BuildInfo.CheckVersionUrl, GetPlatformPath()), this);在单机模式使用Package文件夹里面的资源即可,但是在可更新模式里面,要上传的是Full文件夹里面的资源。
别忘了把流程中的校检流程给勾选上
资源模式
GF的资源模式有四种,编辑器模式,单机模式,预加载的可更新模式以及边玩边下的更新模式。
编辑器模式
在 Unity 编辑器中,可以考虑使用 Game Framework 提供的编辑器模式,直接从磁盘进行资源加载,而避免每次修改资源都要重新构建 AssetBundle,以提高开发效率。
当然,即使在 Unity 编辑器中,依然可以手动关闭编辑器模式,从 AssetBundle 加载资源,以模拟和真实运行环境一样的效果。
编辑器模式并不需要去获取资源包,资源的加载逻辑位于EditorResourceComponent中,资源加载的形式是AssetDatabase,同时通过随机数延迟加载时间模拟异步加载的效果。
综上,编辑器模式在流程初始化的时候并不会做相关资源处理,在流程ProcedureSplash中,检测到如果是编辑器模式便会直接来到ProcedurePreload。
单机模式
此模式游戏流程中资源的初始化:
单机模式并不需要对比版本信息,只需要根据当前版本信息从资源包里面加载对应的资源到内存中,并让它们接受管理。
- 从进程中的ProcedureSplash开始,不同于上面的编辑器模式直接进入Preload,单机模式需要先初始化资源,因此需要先进入ProcedureInitResources中。
- 从此处开始通过ResourceComponent的逻辑执行GF层里面ResourceManager中ResourceIniter的功能进行资源的初始化。
- ResourceIniter里面会加载版本信息,也就是放置于StreamingAssets下的GameFrameworkVersion.dat文件,而这部分的详细逻辑来到了ResouceHelper里面,这里便是DefaultResourceHelper里面的LoadBytes,获取相关的资源的二进制格式数据流。
- 通过单机模式的资源列表序列化器,从指定流中反序列化数据,通过数据头标识进行检查,在通过对应的资版本资源列表回调函数去执行对应的数据流,由此开始存入对应的版本信息,资源信息,资源组,文件系统等并在之后想对应的数据存入到ResourceManager的m_InternalResourceVersion,m_AssetInfos,m_ResourceInfos以及m_ResourceGroups等容器中,提供给全局。
m_AssetInfos对应便是资源文件总数,参考上面也就是128个。
m_ResourceInfos对应的便是资源列表数量,参考上面也就是21个,这种模式下资源包的准备情况(m_Ready)都是true,同样资源也都只在只读区。
m_ResourceGroups对应便是资源组数量,参考上面也就是2个。
以及这里资源列表回调函数是通过ResouceComponent里面手动注册的,包含0,1,2三种version,这是根据打包的具体情况自动决定的,无需额外操作。 - 等上面的一切都完成了,才会触发最初的回调,流程开始进入Preload中。
预加载更新模式
流程
- 对比版本信息,判断是否需要进行更新。在CheckVersion流程中向服务器请求版本信息的时候,也就是GameEntry.WebRequest.AddWebRequset这一步,GF里面加了平台的判断,在命名文件以及书写buildinfo的时候应当注意进行对应修改。版本信息拿到后,主要判断:
- 是否需要强制更新游戏应用,也就是重新通过链接去下载客户端。
- 是否需要更新资源。这是通过对比本地版本文件信息(GameFrameworkVersion.dat)和远程信息文件的版本号是否相同来进行判断的。
如果需要的话,就会进入UpdateVersion流程去更新版本信息,如果不需要则可以直接进行资源的校检流程VerifyResources。
ResourceManager里面将版本检测更新等逻辑整合在了VersionListProcessor这个内部类里面。
- 更新版本信息文件。来到UpdateVersionList这一步首先会去下载远程的GameFrameworkVersion.dat,在确定好当前下载完成后的信息和之前获取的远程版本信息一致后,进行解压,并将解压后的数据写入到文件流中,从而实现本地版本信息文件的更新。
- 获取本地的资源,进行资源的校检。来到VerifyResources流程,到这一步会去获取本地的GameFrameWorkList.dat文件,如果能获取到的话,就会直接通过资源列表序列化器(ReadWriteVersionListSerializer)获得版本列表信息,然后解析后存于本地并进行校检,检查本地资源的完成性,如果有问题就会移除当前有问题的资源信息,并重新写一个资源列表文件到本地;如果不能获取到资源列表,则说明本地没有,直接进入CheckResources流程。
这里的逻辑封装到了ResouceMangaer的内部类ResourceVerifier中。 - 检查本地资源信息是否和远程的资源对的上。首先会获取三个信息,远程的资源版本信息GameFrameworkVersion.dat(经过前面,已更新到本地的读写区),本地的读写区和只读区资源列表信息GameFrameworkList.dat(存在没有的情况)。通过远程的资源版本信息获取此版本应有的资源信息,解析后加入到ResouceMgr对应容器里面,这一点和单机模式一致,不过此时还会加入到m_CheckInfos里面,用于后面进行资源校检。当这三者都加载完成后,会进行资源状态的检查,确定每一个资源的状态,判断其存在位置,是否需要更新,是否需要移除等,最终根据状态去执行对应的操作,比如需要更新的话,就会放入资源更新器(ResourceUpdater)里面去进行更新。统计和执行完成后则会进入UpdateResouces流程。
这一步逻辑的封装在ResourceManager的内部类ResourceChecker中。 - 打开资源更新页面,开始更新资源。更新完成之后就进入到ProcedurePreload流程中了。更新详细逻辑封装在ResourceManager的内部类ResourceUpdater中。
边玩边下更新模式
资源加载
资源加载流程
- 加载的核心逻辑是封装到了ResourceLoader里面,ResourceManager里面各种重载的LoadAsset方法最终都会调用到ResourceLoader的LoadAsset方法。
- CheckAsset,获取当前资源的信息和对应的依赖资源名称。此过程会依次获取AssetInfo以及ResourceInfo,这两者分别是从ResourceManager里面缓存的m_AssetInfos以及m_ResourceInfos里面进行获取的。AssetInfo里面存储的是当前资源的名称,依赖等基本信息,ResourceInfo里面则存储着更加详细的属性信息包括大小,加载方式以及当前资源的状态等。因此,我们检查状态的时候是通过ResourceInfo里面的Ready来进行判断的,而想要通过资源名字获取对应的ResourceInfo,则需要通过名称获取AssetInfo,再通过其字段ResourceName来筛选ResourceInfo,对于依赖资源信息则是通过AssetInfo里面的字段获取。
- LoadDependencyAsset,加载依赖资源。如果当前资源有依赖资源,则会率先加载其依赖资源,当然如果依赖资源也有其依赖资源的话,仍会率先去进行加载,只有所有依赖资源加载完成后,才会加载当前资源。
这里问题来了,先前已经说过GF里面所有的资源加载都是异步的,这里当前资源和其依赖资源的加载都是异步,如何确保资源加载顺序? 这里GF把具体的加载逻辑“外包”给了任务池模块。 - 如果当前的资源还没有“准备好”,那么就会放入资源更新器里面去进行更新状态。
任务池
任务池基础结构图
在资源模块中的应用的结构图
通过结构图可以得知:
- TaskPool存在两个主要内容——Agent和Task。我们通过将Task添加进入TaskPool,然后由Agent去处理每一个Task,最终通过异步和回调的方式来实现资源的加载。
- TaskPool中存储Agent的容器有两个——m_FreeAgent和m_WorkingAgent,顾名思义,m_FreeAgent是空闲的Agent,可以处理Task,而m_WorkingAgent里存储的便是正在处理Task的Agent。而存储Task的容器则只有一个m_WaitingTask,我们添加Task,便是添加到这个容器中,然后由Agent去处理Task。
- Task有两种——LoadAssetTask和LoadDependencyAssetTask。这里我更倾向于将它们分别叫做主任务和子任务,当我们初步想要加载一个资源的时候,我们会创建一个主任务(LoadAssetTask),但这个时候不会立刻把它放进任务池里面,而是先去获取并加载其依赖资源,如果有依赖资源则会为其每个依赖资源创建子任务(dependencyAssetTask),并优先把子任务放进任务池里面,直到所有的依赖资源都添加进任务池,才会把主任务放进任务池。值得注意的是,依赖资源任务添加过程中,同样可能存在其依赖资源,因此子任务(LoadDependencyAssetTask)视情况也会成为其依赖资源加载的“主任务”,通过递归的方式来处理这种形式,就可以实现资源加载任务(Task)有条不絮的加入任务池里面。
- 加载和解析的逻辑位于Agent里面,而处理加载和解析的详细方式实现则是位于UGF层的Helper里面。这使得使用者可以根据实际项目情况去书写加载方式,而不修改GF层的结构。这里我们就以默认的DefaultLoadResourceAgent为例,来看看它是如何处理加载资源的。
任务池任务加载流程:
ProcessWaitingTasks中status的四种状态对应的处理方式:
通过流程图可以更加直观的看到上述加载流程,而加载过程的进行时基于状态的判定,这里的话就是任务的状态,而这里任务的状态又对应着ResourceInfo里面的状态,
接着开始调查ResourceInfo的状态来源和管理器里面两个容器成员的来源吧
四种状态的含义:
- Done表示可以立刻完成此任务,这对应我们想要加载的资源(非场景)以及其依赖资源之前都被加载完成过,而当前资源被缓存到了对象池之中,我们直接拿出来用就可以。这种情况下,可以直接将agent从Working列表中移除,再把当前task从Waiting列表中移除,并进行释放。
- CanResume表示可以继续处理当前任务,对应着两种情况
- 虽然当前要在加载的资源之前未被使用过,但对应的包之前被加载过,可以直接从包里面异步加载此资源,只需要等待这个过程即可。
- 当前资源未被加载过,当前资源所在的资源包也没有被加载过,这就需要先把资源包加载出来,然后再去加载资源。
这两种情况下,只能把task从Waiting列表中移除,当前agent还是处于Working列表中,不会释放任务。
- HasToWait表示不能继续处理此任务,需要等其他任务执行完成。这种情况对应着:
- 当前资源包还没下载好,还在资源更新器里面进行处理;
- 当前资源正在加载中了,说明之前已经有agent处理过这个资源信息了,当前正在Working列表中;
- 当前资源依赖的资源还没加载完成,需要等待依赖资源加载完成;
- 当前资源的资源包还在加载中,对应的agent同样在Working列表中;
这些个情况同样会把agent从Working列表中移除,但不会把task从Waiting列表中移除,也不会释放资task。
- UnknownError表示出现了未知错误,这是方便使用者根据可考虑到的意外情况进行特殊处理。这时候会完全把agent从Working列表中移除,把task从Waiting列表中移除,并释放资源。
资源加载完成后,就可以通过Task里面的回调进行资源的传递和使用了。
同样,有问题欢迎指出。