为了保证玩家的体验,我们不推荐再使用同步的方式加载资源,由于 Game Framework 自身使用了一套完整的异步加载资源体系,因此只提供了异步加载资源的接口。不论简单的数据表、本地化字典,还是复杂的实体、场景、界面,我们都将使用异步加载。同时,Game Framework 提供了默认的内存管理策略(当然,你也可以定义自己的内存管理策略)。多数情况下,在使用 GameObject 的过程中,你甚至可以不需要自行进行 Instantiate 或者是 Destroy 操作。

E神

看之前提前准备一些思路

GF的资源模块是一个复杂度比较高的模块,可能是目前GF框架中最复杂的模块。

所以在了解之前,先确定一些优秀资源模块可能会有的功能基调

1.使用方便。

2.对已加载的资源有良好的管理能力。

3.有完整的开发阶段方案,打包方案,版本方案。

在我感受到的E神的推荐方案里,E神非常建议配合GF文件系统使用GF资源模块,同时在E神给出的demo里构建资源会将unity打的ab包资源进一步整合进虚拟文件系统的构建文件里,你可以选择各种加载方式,但理论上来说虚拟文件系统可以让资源读取IO性能更加友好同时更有利于资源加密。

概念

GF的资源模块是一个综合体,不仅会使用到部分前面我们所介绍的模块,而且同时需要配合GF相对应的资源构建策略使用。

GF在资源加载方面只保留了异步加载方式,如果需要同步加载我们可以自己扩充。

为了了解清楚GF资源模块运转方式,让我们从加载一个资源的全流程开始。

流程

初始化流程

ResourceManager

public class ResourceManager
{

    private const string RemoteVersionListFileName = "GameFrameworkVersion.dat";
    private const string LocalVersionListFileName = "GameFrameworkList.dat";

    private VersionListSerializer xxxVersionListSerializer;//版本资源列表序列化器
...
    
    private IFileSystemManager m_FileSystemManager;//文件系统管理器
    private ResourceIniter m_ResourceIniter;//资源初始化
    private VersionListProcessor m_VersionListProcessor;//版本资源列表处理器
    private ResourceChecker m_ResourceChecker;//资源更新检查器
    private ResourceUpdater m_ResourceUpdater;//资源更新器
    private ResourceLoader m_ResourceLoader;//资源加载器
    private IResourceHelper m_ResourceHelper;//资源辅助器

    //资源管理器轮询
    void Update()
    {
        m_ResourceUpdater.Update();
        m_ResourceLoader.Update();
    }
}

在流程中,GF会先进行一次 ResourceIniter 。

让我们来看一下GF在资源初始化中做了什么。

InitResources

1.记录版本信息

2.使用资源加载辅助器从指定路径中加载一次数据

3.回调函数:成功回调,失败回调

 public void InitResources(string currentVariant)
            {
                m_CurrentVariant = currentVariant;

                if (m_ResourceManager.m_ResourceHelper == null)
                {
                    throw new GameFrameworkException("Resource helper is invalid.");
                }

                if (string.IsNullOrEmpty(m_ResourceManager.m_ReadOnlyPath))
                {
                    throw new GameFrameworkException("Read-only path is invalid.");
                }

                m_ResourceManager.m_ResourceHelper.LoadBytes(Utility.Path.GetRemotePath(Path.Combine(m_ResourceManager.m_ReadOnlyPath, RemoteVersionListFileName)), new LoadBytesCallbacks(OnLoadPackageVersionListSuccess, OnLoadPackageVersionListFailure), null);
            }

看看GF提供的默认的资源加载辅助器的相关方法具体定义。

本质上,会根据我们提供的路径来加载

同时可以猜想到,会根据我们选择的资源加载策略,选择从本地或者在线拉取资源

执行相应的回调函数

同时这也是个异步操作

private IEnumerator LoadBytesCo(string fileUri, LoadBytesCallbacks loadBytesCallbacks, object userData)
        {
            bool isError = false;
            byte[] bytes = null;
            string errorMessage = null;
            DateTime startTime = DateTime.UtcNow;

#if UNITY_5_4_OR_NEWER
            UnityWebRequest unityWebRequest = UnityWebRequest.Get(fileUri);
#if UNITY_2017_2_OR_NEWER
            yield return unityWebRequest.SendWebRequest();
#else
            yield return unityWebRequest.Send();
#endif

#if UNITY_2020_2_OR_NEWER
            isError = unityWebRequest.result != UnityWebRequest.Result.Success;
#elif UNITY_2017_1_OR_NEWER
            isError = unityWebRequest.isNetworkError || unityWebRequest.isHttpError;
#else
            isError = unityWebRequest.isError;
#endif
            bytes = unityWebRequest.downloadHandler.data;
            errorMessage = isError ? unityWebRequest.error : null;
            unityWebRequest.Dispose();
#else
            WWW www = new WWW(fileUri);
            yield return www;

            isError = !string.IsNullOrEmpty(www.error);
            bytes = www.bytes;
            errorMessage = www.error;
            www.Dispose();
#endif

            if (!isError)
            {
                float elapseSeconds = (float)(DateTime.UtcNow - startTime).TotalSeconds;
                loadBytesCallbacks.LoadBytesSuccessCallback(fileUri, bytes, elapseSeconds, userData);
            }
            else if (loadBytesCallbacks.LoadBytesFailureCallback != null)
            {
                loadBytesCallbacks.LoadBytesFailureCallback(fileUri, errorMessage, userData);
            }
        }

加载到字符串后执行成功回调

成功回调过程代码过多就不一一列出来了

中间会对读取到的二进制流通过选择的资源模式和版本找到对应的反序列化策略进行反序列化

解密

用反序列化的信息初始化

m_ApplicableGameVersion//版本号

m_InternalResourceVersion//内部版本信息

m_AssetInfos//单个资源信息集合

m_ResourceInfos//bundle信息集合

m_ResourceGroups//bundle组信息集合

初始化资源信息数据的数据源头

我们还需要了解数据源头在哪,以便我们理解资源的构建流程

在单机模式下数据源头:GameFrameworkVersion.dat(此文件由打包流程构建)

至此

我们通过初始化拿到了资源信息。

创建资源到加载一个资源的流程

创建一个平平无奇的Cube预制

这个Cube现在是我们所需要加载的资源

构建资源

Unity资源加载里面有个我们很难绕开的机制,就是assetbundle。

GF为我们提供了快速编辑和构建AssetBundle的工具。

在Resource Editor工具里我们把Cube资源编辑进Cubes包体里

接下来使用构建工具构建 AssetBundle

构建结果

这里我把在打包过程中多余的几个文件夹删除了
Cubes的去处配置文件 ResourceCollection

如果我们不在ResourceCollection 配置FileSystem的话GF在构建过程中会默认生成一个Cubes.dat

就像这个资源一样。

现在我们配置了FileSystem参数Cubes包体就被打进Resources.dat文件了

是否会有疑惑

GF构建资源以后留下来的文件显然和Unity默认打ab包出来的资源结构向去甚远,那么它的运行机制到底是怎么样的呢?

《GF虚拟文件系统》

没错,GF在资源管理策略中也引入了文件系统的概念,会在使用unity api打出assetbundle后,再将 assetbundle 写入相应的.dat文件中集中管理。

个人感觉看到一个游戏打包出来的资源文件是几个或者几十个清爽的.dat文件是一件很优雅的事情。

我们在使用过程中可以根据具体需求对这一流程进行选择性的使用。

至此我们已经准备好了我们需要的资源。

加载资源

下面展示主要步骤

1.调用加载函数

2.检查是否有此资源,获取资源信息,依赖关系

if (!CheckAsset(assetName, out resourceInfo, out dependencyAssetNames))
                {
                    string errorMessage = Utility.Text.Format("Can not load asset '{0}'.", assetName);
                    if (loadAssetCallbacks.LoadAssetFailureCallback != null)
                    {
                        loadAssetCallbacks.LoadAssetFailureCallback(assetName, resourceInfo != null && !resourceInfo.Ready ? LoadResourceStatus.NotReady : LoadResourceStatus.NotExist, errorMessage, userData);
                        return;
                    }

                    throw new GameFrameworkException(errorMessage);
                }

3.创建资源加载任务,将资源加载任务加入资源加载任务池中。

 LoadAssetTask mainTask = LoadAssetTask.Create(assetName, assetType, priority, resourceInfo, dependencyAssetNames, loadAssetCallbacks, userData);
 m_TaskPool.AddTask(mainTask);

4.任务池轮询,执行任务

5.在任务执行中 异步加载对应的AssetBundle文件,并将对应的AssetBundle纳入Bundle对象池管理中,触发asset加载流程

 m_FileAssetBundleCreateRequest = AssetBundle.LoadFromFileAsync(fileSystem.FullPath, 0u, (ulong)fileInfo.Offset);
if (m_FileAssetBundleCreateRequest != null)
            {
                if (m_FileAssetBundleCreateRequest.isDone)
                {
                    AssetBundle assetBundle = m_FileAssetBundleCreateRequest.assetBundle;
                    if (assetBundle != null)
                    {
                        AssetBundleCreateRequest oldFileAssetBundleCreateRequest = m_FileAssetBundleCreateRequest;
                        LoadResourceAgentHelperReadFileCompleteEventArgs loadResourceAgentHelperReadFileCompleteEventArgs = LoadResourceAgentHelperReadFileCompleteEventArgs.Create(assetBundle);
                        m_LoadResourceAgentHelperReadFileCompleteEventHandler(this, loadResourceAgentHelperReadFileCompleteEventArgs);
                        ReferencePool.Release(loadResourceAgentHelperReadFileCompleteEventArgs);
                        if (m_FileAssetBundleCreateRequest == oldFileAssetBundleCreateRequest)
                        {
                            m_FileAssetBundleCreateRequest = null;
                            m_LastProgress = 0f;
                        }
                    }
                    else
                    {
                        LoadResourceAgentHelperErrorEventArgs loadResourceAgentHelperErrorEventArgs = LoadResourceAgentHelperErrorEventArgs.Create(LoadResourceStatus.NotExist, Utility.Text.Format("Can not load asset bundle from file '{0}' which is not a valid asset bundle.", m_FileName == null ? m_FileFullPath : Utility.Text.Format("{0} | {1}", m_FileFullPath, m_FileName)));
                        m_LoadResourceAgentHelperErrorEventHandler(this, loadResourceAgentHelperErrorEventArgs);
                        ReferencePool.Release(loadResourceAgentHelperErrorEventArgs);
                    }
                }
                else if (m_FileAssetBundleCreateRequest.progress != m_LastProgress)
                {
                    m_LastProgress = m_FileAssetBundleCreateRequest.progress;
                    LoadResourceAgentHelperUpdateEventArgs loadResourceAgentHelperUpdateEventArgs = LoadResourceAgentHelperUpdateEventArgs.Create(LoadResourceProgress.LoadResource, m_FileAssetBundleCreateRequest.progress);
                    m_LoadResourceAgentHelperUpdateEventHandler(this, loadResourceAgentHelperUpdateEventArgs);
                    ReferencePool.Release(loadResourceAgentHelperUpdateEventArgs);
                }
          }
 private void OnLoadResourceAgentHelperReadFileComplete(object sender, LoadResourceAgentHelperReadFileCompleteEventArgs e)
                {
                    ResourceObject resourceObject = ResourceObject.Create(m_Task.ResourceInfo.ResourceName.Name, e.Resource, m_ResourceHelper, m_ResourceLoader);
                    m_ResourceLoader.m_ResourcePool.Register(resourceObject, true);
                    s_LoadingResourceNames.Remove(m_Task.ResourceInfo.ResourceName.Name);
                    OnResourceObjectReady(resourceObject);
                }

6.从assetbundle中异步加载单个asset

 m_AssetBundleRequest = assetBundle.LoadAssetAsync(assetName, assetType);

7.加载完成后将asset纳入资源对象池

 private void OnLoadResourceAgentHelperLoadComplete(object sender, LoadResourceAgentHelperLoadCompleteEventArgs e)
                {
                    AssetObject assetObject = null;
                    if (m_Task.IsScene)
                    {
                        assetObject = m_ResourceLoader.m_AssetPool.Spawn(m_Task.AssetName);
                    }

                    if (assetObject == null)
                    {
                        List<object> dependencyAssets = m_Task.GetDependencyAssets();
                        assetObject = AssetObject.Create(m_Task.AssetName, e.Asset, dependencyAssets, m_Task.ResourceObject.Target, m_ResourceHelper, m_ResourceLoader);
                        m_ResourceLoader.m_AssetPool.Register(assetObject, true);
                        m_ResourceLoader.m_AssetToResourceMap.Add(e.Asset, m_Task.ResourceObject.Target);
                        foreach (object dependencyAsset in dependencyAssets)
                        {
                            object dependencyResource = null;
                            if (m_ResourceLoader.m_AssetToResourceMap.TryGetValue(dependencyAsset, out dependencyResource))
                            {
                                m_Task.ResourceObject.AddDependencyResource(dependencyResource);
                            }
                            else
                            {
                                throw new GameFrameworkException("Can not find dependency resource.");
                            }
                        }
                    }

                    s_LoadingAssetNames.Remove(m_Task.AssetName);
                    OnAssetObjectReady(assetObject);
                }

8.将任务状态设置为完成,执行资源加载完成回调

 private void OnAssetObjectReady(AssetObject assetObject)
                {
                    m_Helper.Reset();

                    object asset = assetObject.Target;
                    if (m_Task.IsScene)
                    {
                        m_ResourceLoader.m_SceneToAssetMap.Add(m_Task.AssetName, asset);
                    }

                    m_Task.OnLoadAssetSuccess(this, asset, (float)(DateTime.UtcNow - m_Task.StartTime).TotalSeconds);
                    m_Task.Done = true;
                }

至此在单机模式下一个完整的资源加载流程走完了。

写一个简单的资源加载回调函数。

 private void LoadAssetCallbacksSucess(string assetName, object asset, float duration, object userData)
    {
        Instantiate((GameObject)asset);
    }
执行效果