资源处理方案

 

记录打包收集资源中遇到的一个问题。顺便整理下思路

基本逻辑:

双向无依赖资源,单独打一个AB

只被一个资源依赖的资源,比如B只被A依赖,那么A和B一起打一个AB

被多个资源依赖的资源,单独打一个AB,比如A分别被BCD依赖,那么A单独打一个AB

问题

约定:主项目目录,项目用到东西,要进包的东西,都放在该目录下,下面叫GameMain

现在只扫描了GameMain目录下的资源,换言之,如果一个资源的依赖资源不在GameMain目录下,那么它不会被扫描到,也就不会被打AB,但如果他被别的资源(GameMain目录)依赖,在构建资源时,就会把这些非GameMain目录下的资源拷贝一份到依赖他的AB里,如果有多个依赖,就构成了冗余。

解决方案办法

找出所有非GameMain的离散资源,打入AB。

paths: 图,一维:主资源索引,二维:主资源及其依赖资源的路径集合。一维存储主资源及其依赖资源。顶点表示资源,有向边表示依赖

pathDic:paths的字典缓存,加快查找。MainAsset及其依赖的集合

具体操作如下

  1. 收集资源时,从收集器的源资源中找到所有shader的依赖,保存起来

  2. 将源资源集合中的shader依赖筛掉,留下的先都保存到双向无依赖资源合集,暂时认为他们都是双向无依赖的(SinglePath)。然后保存到CheckAssets中缓存,用于检测是否为离散资源。

  3. 遍历处理后的源资源,对于每一个资源,获取它的依赖资源

    1. 如果这个资源有依赖资源,那么就将他从SinglePath中移除,因为他不是一个双向无依赖资源了。

    2. 对于该资源的每一个依赖资源,如果他不在CheckAsets中,则说明这个资源是离散资源

    3. 检查该离散资源是否为有效的离散资源。

      检查方法有如下几种情况,举例说明。

      视为无效的情况,主资源即是该资源;主资源或该依赖资源是场景资源或场景光照数据;该依赖是脚本文件或hlsl文件;Editor目录下的;包含空格或特殊字符的;不在GameMain目录下的(因为在GameMain目录下的已经扫进去了)shadervariants或shadergraph文件或其他shader文件。

    4. 如果是有效的游离资源,则视为需要进入占位AB的资源。

    5. 如果该资源包含在SingleAssets中,则将其移除(因为它是依赖资源),将该依赖资源缓存到depend_list中,表示每一个资源和他携带的依赖资源集合。

    6. 如果该资源属于占位资源(是有效的离散资源),则将其添加到ScatterSet中。

  4. 然后递归收集依赖关系

    遍历ScatterAssets,如果scatterAsset不在pathDic中,先将他添加进去。检查每一个scatterAsset的依赖,对于他的每一个依赖资源,如果是有效的离散资源,那么就放入pathDic中其主资源携带的依赖中。不在GameMain目录下的放入scatterCollector中保存起来。然后这些依赖资源都重复递归上述过程收集他们的依赖然后保存到PathDic中。(将所有离散资源及其递归的依赖资源都放到pathDic里,离散资源的依赖资源同时放到paths里)

  5. 将上一步中筛出的离散资源也加入到scatterSet中。

  6. 接下来生成图

    		public AssetGraph(List<List<string>> lists)
            {
                //map和keys组成了图所有顶点间的索引关系
                map = new Dictionary<string, int>();
                foreach (List<string> list in lists)
                {
                    foreach (string key in list)
                    {
                        //建立路径到顶点的索引,key是资源索引,value是map大小,作为顶点索引
                        if (!map.ContainsKey(key))
                            map.Add(key, map.Count);
                    }
                }
                   
                //创建顶点到资源的映射,比如0=MainAsset,1=别的依赖资源
                keys = new string[map.Count];
                foreach (string name in map.Keys)
                    keys[map[name]] = name;
                   
                //创建图,拥有map.count个顶点
                G = new DGraph(map.Count);
                // 给每个顶点,添加邻接表数据
                foreach (List<string> list in lists)
                {
                    //[0]为主资源(也就是顶点)获取到该资源的 顶点数组索引
                    int v = map[list[0]];
                    //从1开始,因为不用处理主资源
                    for (int i = 1; i < list.Count; i++)
                    {
                        int w = map[list[i]];
                        //给顶点v添加边,其实就是将v的邻接顶点添加到邻接表(输入边数据 即:邻接表数据)
                        G.addEdge(v, w);
                    }
                }
            }
    
  7. 然后检查循环依赖

    		//深度搜索图
    		private void DeepFirstSearch(DiGraph g, int begin)
            {
                //将当前顶点标记为已搜索
                isMarked[begin] = true;
                //将当前顶点标记位正在被考虑
                inStack[begin] = true;
                //拿当前顶点的邻接顶点
                foreach (int node in g.getAdj(begin))
                {
                    if (hasCycle())
                    {
                        return;
                    }
                    //邻接顶点没有被搜索,从邻接顶点开始搜索
                    if (!isMarked[node])
                    {
                        //记录邻接顶点的上一个顶点,如果发现环形引用则用他进行回溯
                        edgeTo[node] = begin;
                        DeepFirstSearch(g, node);
                    }
                    //如果邻接顶点是正在被考虑的顶点,则说明形成了环形依赖
                    //回溯所有引用,然后检查cycle是不是空,有没有东西就行了
                    else if (inStack[node])
                    {
                        cycle = new Stack<int>();
                        for (int i = begin; i != node; i = edgeTo[i])
                        {
                            cycle.Push(i);
                        }
                        cycle.Push(node);
                        cycle.Push(begin);
                    }
                }
                inStack[begin] = false;
            }
    
  8. 之后对图进行拓扑排序,进行一次深度搜索后保存到缓存里,这里保存的是后序遍历结果,所以要把结果反过来

  9. 接下来遍历拓扑序列,确定要进包的资源,获取当前顶点的入度,即被依赖的次数/有多少资源依赖它,并且用一个键值对dict来保存,其中key=主资源,value=依赖主资源的资源集合

    • 如果它的入度大于1,则说明该资源被多个资源依赖,单独打一个AB,添加到dict中,主资源是他自己,携带的依赖也是他自己。

    • 入度为0,则说明该资源没有被任何资源依赖,也单独打一个AB,做法同上。
    • 入度为1,说明它仅被一个资源依赖,这种情况下,它和依赖它的资源打入一个AB。
      1. 如果是场景依赖该资源,则直接打入,主资源和携带依赖都是他自己添加到dict中。
      2. 如果不是场景文件,则检查该资源的递归依赖资源是否有场景,如果有的话,就将该资源和该资源的根部依赖资源放到一起。
      3. 该资源__没有__label的情况下,如果该资源为prefab/playable/mat/fbx这几类,则将该资源和依赖它的资源保存到一起,其他类型的资源不处理,被动打入AB,这是为了在运行时不产生过多的AssetInfo导致性能问题。
      4. 如果该资源有label,则将该资源和依赖它的资源保存到一起。
  10. 接下来整理有向图资源,遍历dict。

    从图中拿到该资源,确定BundleName,确定规则是:GameMain目录下的资源路径一直到文件名,后缀用下划线隔开。然后遍历该资源的依赖资源。

    • 对于.fbx/.anim类型的资源替去掉其@符号即可。
    • 场景类资源,如果依赖场景类资源则添加 _Data标记,自身添加 _Scene标记后缀
    • 其他类型的资源不用特殊处理

    逐一处理dict.item.Value,确定资源组、该资源是否放入包体自带、所属的文件系统。

  11. 遍历SinglePath,处理所有的双向无依赖资源(因为在此之前所有有依赖的已经被剔除了),思路大致和前面的步骤相同,检查资源类型添加不同的后缀以做标识,然后添加到resource中,放到ResourceController里准备后续的构建流程。