记录打包收集资源中遇到的一个问题。顺便整理下思路
基本逻辑:
双向无依赖资源,单独打一个AB
只被一个资源依赖的资源,比如B只被A依赖,那么A和B一起打一个AB
被多个资源依赖的资源,单独打一个AB,比如A分别被BCD依赖,那么A单独打一个AB
问题
约定:主项目目录,项目用到东西,要进包的东西,都放在该目录下,下面叫GameMain
现在只扫描了GameMain目录下的资源,换言之,如果一个资源的依赖资源不在GameMain目录下,那么它不会被扫描到,也就不会被打AB,但如果他被别的资源(GameMain目录)依赖,在构建资源时,就会把这些非GameMain目录下的资源拷贝一份到依赖他的AB里,如果有多个依赖,就构成了冗余。
解决方案办法
找出所有非GameMain的离散资源,打入AB。
paths: 图,一维:主资源索引,二维:主资源及其依赖资源的路径集合。一维存储主资源及其依赖资源。顶点表示资源,有向边表示依赖
pathDic:paths的字典缓存,加快查找。MainAsset及其依赖的集合
具体操作如下
-
收集资源时,从收集器的源资源中找到所有shader的依赖,保存起来
-
将源资源集合中的shader依赖筛掉,留下的先都保存到双向无依赖资源合集,暂时认为他们都是双向无依赖的(SinglePath)。然后保存到CheckAssets中缓存,用于检测是否为离散资源。
-
遍历处理后的源资源,对于每一个资源,获取它的依赖资源
-
如果这个资源有依赖资源,那么就将他从SinglePath中移除,因为他不是一个双向无依赖资源了。
-
对于该资源的每一个依赖资源,如果他不在CheckAsets中,则说明这个资源是离散资源
-
检查该离散资源是否为有效的离散资源。
检查方法有如下几种情况,举例说明。
视为无效的情况,主资源即是该资源;主资源或该依赖资源是场景资源或场景光照数据;该依赖是脚本文件或hlsl文件;Editor目录下的;包含空格或特殊字符的;不在GameMain目录下的(因为在GameMain目录下的已经扫进去了)shadervariants或shadergraph文件或其他shader文件。
-
如果是有效的游离资源,则视为需要进入占位AB的资源。
-
如果该资源包含在SingleAssets中,则将其移除(因为它是依赖资源),将该依赖资源缓存到depend_list中,表示每一个资源和他携带的依赖资源集合。
-
如果该资源属于占位资源(是有效的离散资源),则将其添加到ScatterSet中。
-
-
然后递归收集依赖关系
遍历ScatterAssets,如果scatterAsset不在pathDic中,先将他添加进去。检查每一个scatterAsset的依赖,对于他的每一个依赖资源,如果是有效的离散资源,那么就放入pathDic中其主资源携带的依赖中。不在GameMain目录下的放入scatterCollector中保存起来。然后这些依赖资源都重复递归上述过程收集他们的依赖然后保存到PathDic中。(将所有离散资源及其递归的依赖资源都放到pathDic里,离散资源的依赖资源同时放到paths里)
-
将上一步中筛出的离散资源也加入到scatterSet中。
-
接下来生成图
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); } } }
-
然后检查循环依赖
//深度搜索图 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; }
-
之后对图进行拓扑排序,进行一次深度搜索后保存到缓存里,这里保存的是后序遍历结果,所以要把结果反过来
-
接下来遍历拓扑序列,确定要进包的资源,获取当前顶点的入度,即被依赖的次数/有多少资源依赖它,并且用一个键值对dict来保存,其中key=主资源,value=依赖主资源的资源集合
-
如果它的入度大于1,则说明该资源被多个资源依赖,单独打一个AB,添加到dict中,主资源是他自己,携带的依赖也是他自己。
- 入度为0,则说明该资源没有被任何资源依赖,也单独打一个AB,做法同上。
- 入度为1,说明它仅被一个资源依赖,这种情况下,它和依赖它的资源打入一个AB。
- 如果是场景依赖该资源,则直接打入,主资源和携带依赖都是他自己添加到dict中。
- 如果不是场景文件,则检查该资源的递归依赖资源是否有场景,如果有的话,就将该资源和该资源的根部依赖资源放到一起。
- 该资源__没有__label的情况下,如果该资源为prefab/playable/mat/fbx这几类,则将该资源和依赖它的资源保存到一起,其他类型的资源不处理,被动打入AB,这是为了在运行时不产生过多的AssetInfo导致性能问题。
- 如果该资源有label,则将该资源和依赖它的资源保存到一起。
-
-
接下来整理有向图资源,遍历dict。
从图中拿到该资源,确定BundleName,确定规则是:GameMain目录下的资源路径一直到文件名,后缀用下划线隔开。然后遍历该资源的依赖资源。
- 对于.fbx/.anim类型的资源替去掉其@符号即可。
- 场景类资源,如果依赖场景类资源则添加 _Data标记,自身添加 _Scene标记后缀
- 其他类型的资源不用特殊处理
逐一处理dict.item.Value,确定资源组、该资源是否放入包体自带、所属的文件系统。
-
遍历SinglePath,处理所有的双向无依赖资源(因为在此之前所有有依赖的已经被剔除了),思路大致和前面的步骤相同,检查资源类型添加不同的后缀以做标识,然后添加到resource中,放到ResourceController里准备后续的构建流程。