文/张嘉华

  Unity的资源读取相信已经有很多分析的文章,本文将深入资源格式及引擎源码分析unity的资源读取,并尝试给assetbundle添加一个接口AssetBundle.LoadDependentResource(name)加载一个AssetBundle中依赖的但又没有显式路径的资源。

  目的

  对于特大规模的场景来说,我们往往希望能够释放掉一些不常用或远离的被依赖的资源来减少内存。假如我们有一个名为A的fbx角色模型依赖B、C两个Texture和D、E两个AnimChip,当这个A模型离镜头比较远时,并正在播放D这个AnimClip,我们可能希望1)暂时卸载掉Texture B和C中的minmip的精细几层,只保留粗糙的几层,2)卸载掉AnimClip E,并在A重新临近镜头时,3)重新加载纹理B和C中精细的几层minmip和4)重新加载AnimClip E,5)以及在一些特殊需求下直接需要加载资源包中的某个被依赖的资源。很多同学会想到调用Resouces.UnloadUnusedAssets来卸载不用的资源,但是并没有直接的方法能够卸载和重新加载正在被引用的资源中的一部分,如本文需求中的1)~5)。

  Unity引擎的AssetBundle已经为我们带有了Load(path)和LoadAll等接口,其中:

  Load (name : string, type : Type) : Object需要指定一个name/path,即处于包中的资源名字/相对路径,而这名字则是打包的时候往往通过
  BuildPipeline.BuildAssetBundle,
  BuildPipeline.BuildStreamedSceneAssetBundle
  BuildPipeline.BuildAssetBundleExplicitAssetNames
  等函数显示/非显示打包的资源名字,对于依赖的资源通常会通过BuildAssetBundleOptions.CollectDependencies等标记一并打包进AssetBundle,但是我们在加载的时候却无法直接通过Load指定名字来加载,除非把这个依赖资源也通过BuildPipeline.BuildAssetBundleExplicitAssetNames显式添加名字到assetNames集合。本文的主要目的就是让程序实现能够直接加载没有明确显式指定的却又因被依赖关系打包进AssetBundle的资源。

  AssetBundle格式

  AssetFile就是我们导出exe后bin/xxx_Data目录下的各个扩展名为.assets的文件,AssetBundle就是这些.assets文件的集合打包成一个可压缩的.unity3d扩展名的文件。因此我们对Unity源码进行修改,在原有AssetBundle.CreateFromFile基础上也添加AssetBundle.CreateFromFolder允许从磁盘目录上加载一堆.assets文件作为一个映射到AssetBundle进行统一管理。每个AssetBundle包括一个头BlockAssetBundleHeader,

 

  1. public struct AssetBundleHeader
  2. {
  3. public string signature;
  4. public int streamVersion;
  5. public UnityVersion unityVersion;
  6. public UnityVersion unityRevision;
  7. public uint minimumStreamedBytes;
  8. public int headerSize;
  9. public int numberOfLevelsToDownload;
  10. public int numberOfLevels;
  11. public struct LevelInfo {
  12. public uint PackSize;
  13. public uint UncompressedSize;
  14. };
  15. public List<LevelInfo> levellist=new List<LevelInfo>();
  16. public uint completeFileSize;
  17. public uint dataHeaderSize;
  18. public bool compressed;
  19. };


A3563f73<Texture2D>



093646wansyyb2yz33xncb.png
229bed14<Material>

093647nrvsq52oex12oowe.png
Box001<Mesh>

093647llwbl5d0hnbnftzn.png
AlphaTest-VertexList<Shader>

093647ch26i4dnwnh2e86t.png
unknown<AssetBundle>

0936478v9s80xyyovizvmy.png
UVMesh<MonoScript>


常见的一些资源类型及其属性字段


  Unity在加载这些资源的时候就是先反序列化这些字段到对象实例对应的变量上。


  每个Asset对象的元数据只包含了Asset资源的引用对{FileID,localID},Asset对象自身的属性又没有相对路径,那么unity的Resouces.Load等究竟又是如何通过相对路径映射到这各个对象呢?这个相对路径到{FileID,localID}的映射表存在那?我们再次阅读unity源码,发现Unity初始化AssetBundle类实例会首先反序列化m_Container,m_MainAsset,m_PreloadTable等变量,而该类实例正是对应AssetFile中的第一个没名字的<AssetBundle>资源对象。对于目录上而非打包的AssetFile则是对应其中的第一个没名字的<ResourceManager> 资源对象。m_Container变量是一个key-value表保存了所有打包时显式资源从相对路径path到Asset{FileID,localID}的映射。我们想直接加载的依赖资源并没有在m_Container的映射表中,为了能直接加载非显式资源,我们另外建立一个映射表来实现从依赖的隐式对象名字name到引用对{FileID,localID}的映射表。



  实现

  AssetBundle导出给C#的接口定义在Runtime/Export/AssetBundleBindings.txt
具体实现在runtime/misc/AssetBundleUnity.cpp,AssetBundle.CreateFromFile 调用 ExtractAssetBundle加载AssetBundle到内存,具体加载过程是读入文件头后把AssetBundle压缩数据解开,得到多个AssetFile的头(一个AssetBundle包含多个AssetFile),名字可能包括”CAB”以识别,得到AssetFile头后按普通AssetFile调用持久化管理器PersistentManager.LoadExternalStream把AssetFile中的资源对象映射入内存。

  AssetBundle.Load加载对象具体在AssetBundleUtility:oadNamedObjectFromAssetBundle实现,首先调用ResourceManager::GetPathRange获得AssetBundleBindings.txt增加LoadDependent

 

 

  1. // Loads object with /name/ of a given /type/ from the bundle.
    • CSRAW
      • [TypeInferenceRule(TypeInferenceRules.TypeReferencedBySecondArgument)]
        • CONSTRUCTOR_SAFE
          • CUSTOM Object LoadDependent (string name, Type type)
            • {
              • Scripting::RaiseIfNull (type);
                • Object* o = LoadNonNamedObjectFromAssetBundle (*self, name, type);
                  • if (o==0) return SCRIPTING_NULL;
                    • return Scripting::ScriptingWrapperFor(o);
                      •  
复制代码

  AssetBundleUnity.cpp中添加

 

 

  1. Object* LoadNonNamedObjectFromAssetBundle (AssetBundle bundle, const std::string name, ScriptingObjectPtr type)
    • {
      • LocalSerializedObjectIdentifier localID = bundle.GetLocalID(name);
        • vector<Object*> result;
          • ProcessAssetBundleEntries(bundle,localID,type,result,true);
            • if (!result.empty())
              • return result[0];
                •  
                • return NULL;
                  • }
                    •  
复制代码

  在bundle.GetLocalID 里从自定义的另外一个映射表根据名字查找资源引用对LocalSerializedObjectIdentifier {FileID,localID},得到后传入重载的另外一个以LocalSerializedObjectIdentifier为参数ProcessAssetBundleEntries逐个加载具体的Asset对象即可。




锐亚教育

锐亚教育,游戏开发论坛|游戏制作人|游戏策划|游戏开发|独立游戏|游戏产业|游戏研发|游戏运营| unity|unity3d|unity3d官网|unity3d 教程|金融帝国3|8k8k8k|mcafee8.5i|游戏蛮牛|蛮牛 unity|蛮牛