参考写在前面!!!

本文是在腾讯大佬花桑的GF解析文章的基础上,自己阅读源码并尝试总结、应用、拓展的个人笔记!水印以示尊敬。

写在前面

其实半年前就知道gf,但是那时候刚入门unity没多久,看了猫仙人的simple gf来学习,很可惜只学到了皮毛也就是一些常见的设计模式比如状态机、对象池、优先队列轮询在游戏中的应用。经过半年的unity学习与项目积累,再回来看gf的源码觉得,不难且实现优雅,但是内容实在是太多了,记不下来。为了让自己学的结构能穿起来,刨析demo并自己做,很必要。

参考的也是花桑和gf官方的demo。

// TODO 目前只做了刨析demo的流程

流程图

img

启动场景全流程

自己的GameEntry.cs

是自己的GameEntry类,而不是UnityGameFramework.Runtime中的GameEntry静态类。

作为游戏的入口,它需要继承MonoBehaviour并在Start方法中进行初始化。因为gf所有控件的初始化是在Awake内结束的,所以Start内拿到他们的时候已经可以安心使用了。

  1. 将所有默认的 GameFrameworkComponent 通过UnityGameFramework.Runtime.GameEntry.GetComponent获取,赋值到静态字段中,提供全局访问。这一块不同项目可以通用。
  2. 将项目中自己创建的 GameFrameworkComponent 派生类 ,与上面一样的操作进行赋值提供访问。

ProcedureLaunch 流程

进行一些游戏启动的必要的初始化,支撑后续的启动流程,如:

1.初始化构建信息:如版本检测和资源更新的URL信息,更新界面资源等
2.语言设置:若有上次设置记录则使用上次设置记录,若没有则使用默认或系统语言
3.初始化变体:根据当前语言设置,通知后续底层加载对应的资源变体
4.初始本地化文本资源:根据当前语言设置选择对应的文本

以上涉及到的资源,包括更新信息文件、更新界面资源、本地化文本等都是build-in资源,也就是发布时就在包内,不可更新的。试想我们发布的游戏是一个仅仅支撑启动的包,所有游戏资源都需要在启动后的热更流程中下载,但一些启动图片,以及热更时的界面本身也是需要资源的,需要给出基本的文本、确认框等,以提供给玩家确认是否下载、下载进度预览,还涉及到更新请求的URL。这部分资源就是我们需要放在包内的不可更新资源,主要用来支撑热更的启动,而ProcedureLaunch流程就是负责初始化这些资源和相关配置。

顺从gf的流程,从这个流程结点开始启动项目。做的是加载在进入热更阶段之前的画面,这些画面和配置不可更新,发布在包内。

流程转变发生在OnUpdate,也就是Awake、Start之后的第一帧 进入ProcedureSplash。

初始化变体

  1. 根据语言选项,设置对应的 m_CurrentVariant (”en-us”、”zh-cn”、”zh-tw” 其中之一)。
  2. m_CurrentVariant 会显示在 LabelField 中。
  3. 在资源加载中起到作用:ResourceManager.ResourceIniter类的 OnLoadPackageVersionListSuccess方法 中,会遍历资源的Variant是否一致。

ProcedureSplash 流程

闪屏流程,该流程会播放一个闪屏动画,然后根据当前的资源模式选择下一个流程,分别有

1.编辑器模式->ProcedurePreload
2.整包模式(不可更新)->ProcedureInitResources
3.可更新模式->ProcedureCheckVersion

编辑器模式下将使用EditorResourceComponent作为资源组件,里面使用的是AssetDatabase的接口直接加载Editor下资源,不涉及任何打包资源,不需要做资源列表初始化等操作。可直接进入ProcedurePreload流程。

这个并非必要流程,若不需要闪屏,去掉此流程,把逻辑挪到上一个流程中即可。

其实就是一个switch,决定下一个去往的流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (GameEntry.Base.EditorResourceMode)
{
// 编辑器模式
ChangeState<ProcedurePreload>(procedureOwner);
}
else if (GameEntry.Resource.ResourceMode == ResourceMode.Package)
{
// 单机模式
ChangeState<ProcedureInitResources>(procedureOwner);
}
else
{
// 可更新模式
ChangeState<ProcedureCheckVersion>(procedureOwner);
}

ProcedureInitResources 流程

对于整包模式这个分支流程很简单,主要逻辑就是调用ResourceManager.InitResources(),加载包内资源列表,并解析到ResourceManager中,这样就初始化完毕资源的相关信息了,包括AB包、Asset、资源组、文件系统、版本号等。因为整包模式下所有资源都在包体内,只需要一步初始化即可获得所有资源信息,且后续不需要进行更新,所以初始化后便进入ProcedurePreload流程。

单机模式,不需要资源热更的情况,只需要加载资源。

看代码层层包装,实际做的事就是:

  1. 将所给的地址转化成 带有file://http:// 前缀的远端格式。

  2. 根据1是否成功的结果,启用携程来发送web请求,获取到返回值后执行对应的 Success 或 Failed 函数(我个人觉得这里可以换成用异步等待)

  3. Success函数:OnLoadPackageVersionListSuccess ,将请求的返回值反序列化成一个PackageVersionList,从中获得版本信息、Asset、Resource、FileSystem、ResourceGroup并装载。

    Failed函数:OnLoadPackageVersionListFailure,抛出GameFrameworkException。

  4. 切换状态到 ProcedurePreload 流程。

ProcedureCheckVersion 流程

此流程主要是调用ResourceManager.UpdateVersionList(),更新版本资源列表,其实就是更新最新的GameFrameworkVersion.dat文件,此文件记录了服务器上最新资源的信息,包括一些校验信息等,这些信息将被用来在下一个ProcedureCheckResources流程中进行资源校验。

  1. 向EventPool添加成功失败情况的2个回调。

  2. 向服务器请求版本信息。

  3. 根据版本信息来决策:

    需要版本更新:设置VersionList并切换流程到 ProcedureUpdateVersion 流程。

    不需要版本更新:切换流程到 ProcedureCheckResources 流程。

ProcedureUpdateVersion 流程

此流程主要是调用ResourceManager.UpdateVersionList(),更新版本资源列表,其实就是更新最新的GameFrameworkVersion.dat文件,此文件记录了服务器上最新资源的信息,包括一些校验信息等,这些信息将被用来在下一个ProcedureCheckResources流程中进行资源校验。

基本上面就是全部了,唯一值得补充的是下载用的是 DownloadTask 类,然后放到 DownloadManager 的任务池里。

成功后执行回调,进入 ProcedureCheckResources 流程。

ProcedureCheckResources 流程

资源检测流程,核心逻辑是调用ResourceManager.CheckResources(),GF内部会解析以下3个文件:

​ 1.可读写路径下的GameFrameworkVersion.dat,文件记录着服务器上最新的资源信息
​ 2.只读路径下的GameFrameworkList.dat,文件记录着只读路径(包内)下的资源信息
​ 3.可读写路径下的GameFrameworkList.dat,文件记录着可读写路径下(以前通过热更下载的)的资源信息

资源模块内部会根据本地资源信息和服务器资源信息作对比,标记出每个资源的状态,包括是否需要更新、是否可用、是否需要删除等。后续可以根据这些状态来加载或更新资源。

检测完资源后,有哪些资源是需要更新的就已经明确了,这个时候可以根据项目具体需求选择是否进入更新流程。

​ a.若游戏要求所有资源都为最新时才能进入游戏,则有资源变化就必须进入更新流程更新资源
​ b.若游戏做了分包下载,进入初始场景不需要更新(初始场景资源无变化),待用到对应资源时才更新,也可以不进入更新流程,在游戏中玩家需要访问未更新资源时,再在后台更新。

看上面就够。实际执行的代码就是:

1
2
3
4
5
6
7
8
9
10
11
12
OnEnter:
// 3项加载解析
m_ResourceManager.m_ResourceHelper.LoadBytes(Utility.Path.GetRemotePath(Path.Combine(m_ResourceManager.m_ReadWritePath, RemoteVersionListFileName)), new LoadBytesCallbacks(OnLoadUpdatableVersionListSuccess, OnLoadUpdatableVersionListFailure), null);
m_ResourceManager.m_ResourceHelper.LoadBytes(Utility.Path.GetRemotePath(Path.Combine(m_ResourceManager.m_ReadOnlyPath, LocalVersionListFileName)), new LoadBytesCallbacks(OnLoadReadOnlyVersionListSuccess, OnLoadReadOnlyVersionListFailure), null);
m_ResourceManager.m_ResourceHelper.LoadBytes(Utility.Path.GetRemotePath(Path.Combine(m_ResourceManager.m_ReadWritePath, LocalVersionListFileName)), new LoadBytesCallbacks(OnLoadReadWriteVersionListSuccess, OnLoadReadWriteVersionListFailure), null);
// 完成后执行回调,标记出每个资源的状态,包括是否需要更新、是否可用、是否需要删除等。
private void OnCheckResourcesComplete(...);

OnUpdate:
// 根据上面标记的值:是否需要更新,来确定下一个状态。
需要版本更新:设置UpdateResourceCount并切换流程到 ProcedureUpdateResources 流程。
不需要版本更新:切换流程到 ProcedurePreload 流程。

需要版本更新:设置UpdateResourceCount并切换流程到 ProcedureUpdateResources 流程。
不需要版本更新:切换流程到 ProcedurePreload 流程。

ProcedureUpdateResources 流程

资源更新流程,如上文所说,在本流程可以根据项目具体需求,更新此刻需要更新的资源。例如游戏内做了资源分组,把一些活动、副本等资源单独分组了,则可以在此时只更新基础资源,待玩家访问到还没更新的活动、副本相关资源时,再在后台更新活动、副本的资源。更新的最小单位为一个资源组。

另外这个流程还需要实现热更时的当前进度、下载速度、剩余大小等界面表现。

更新完成后正式进入游戏业务流程。

获取上个流程传来的 UpdateResourceCount,之后传入委托到EventPool。这4个委托的核心是维护一个List<UpdateLengthData>队列 m_UpdateLengthData(下称进度队列) 。而这个进度队列的数据结构,只有 Name、Length、TotalZipLength 三个属性。

花桑的demo里用了4个委托的情况:

  1. OnResourceUpdateStart:为进度队列添加内容。
  2. OnResourceUpdateChanged:为进度队列更新进度。
  3. OnResourceUpdateSuccess:修改 m_UpdateSuccessCount++,并为进度队列更新进度。
  4. OnResourceUpdateFailure:Retry并为进度队列回退进度。如果Retry次数超过设定次数,就直接返回。

设置好委托后,执行 StartUpdateResources方法。这个方法内将资源列表放到了一个 资源等待更新队列。ResourceManager的Update里会去轮询这个 资源等待更新队列,一帧一个(TODO why?)地,加入DownloadManager的TaskPool里。

等待更新任务完成后,切换到最终流程 ProcedurePreload流程。

ProcedurePreload 流程

预加载流程,这一流程已经属于游戏业务层,但大部分游戏其实都需要这么一个流程,所以这里也把他规划到通用流程中,流程中主要负责设置框架的功能模块,预加载数据表,初始化游戏中的功能系统等。

找到 Config(配置) 和 Localization(本地化组件) 的路径,传入GF进行异步加载。再对所有Data进行Preload。

完成后进入 ProcedureLoadingScene流程。

ProcedureLoadingScene 流程

加载Scene流程,也是通用的。先根据id获取一个SceneData类型,这个SceneData类型是装在一个字典中的,在ProcedurePreload 流程的时候会初期化。

SceneData类型附带了场景名字路径等,以及,下一个流程的名字。当Scene加载完后,根据上面取得的配置,切换到下一个流程中。

按正常顺序走,第一个场景是 Menu场景。

ProcedureMenu 流程

对应的是游戏选关卡的界面。其实就是主城啦。

展示UI、展示关卡等等。当选择关卡后,会进入 ProcedureLoadingScene流程,一样读取SceneData切换到对应关卡并进入 ProcedureLevel流程。

ProcedureLevel 流程

战斗流程。

根据关卡配置创建关卡 LevelControl类型,里面包含了所有战斗需要的东西,有点类似于自己常写的“BattleMgr”。

LevelControl并不继承Mono,只继承一个IReference接口。它由ProcedureLevel.OnUpdate驱动每一帧做的事,同时OnUpdate还会检测一个flag判断是否要执行切换场景了(进入 ProcedureLoadingScene流程)。