最近公司里需要用FairyGUI替换掉原来的UI系统,其优势在于将美术和设计的工作与程序一定意义上的隔离,这一块的体会之后还会再写一点什么单独说一下。由于FairyGUI的图集是按package来生成的,并没有动态图集的支持,于是看了一下相关的内容。
关于动态图集
图集的概念就不多介绍了,主要的目的在于降低游戏的drawcall。一般而言大家都会将某一个系统模块的散图打成一张图集,或者将常用的UI组件打到一起,以此达到优化的目的。这些图集都是在游戏发布前就制作好的,也就是静态图集,然而考虑一下下面这个场景:王者荣耀在玩家选择完英雄进入游戏后,玩家的各个技能,英雄头像这些散图如果采用单独的贴图,每一张图片都将是一个单独的drawcall,这显然是有优化空间的。预先把所有技能头像打成图集显然不现实,而动态图集就能解决这样的问题,只需要将这些会用到的散图打到一张图集中,自然就能合并drawcall。
制作动态图集
在NGUI中,提供了可以在运行时制作图集的UIAtlasMaker,但是在UGUI中,图集是Unity自动生成的(或者通过SpritePacker自己管理),并没有提供动态生成的方法。制作图集的原理很简单,只需要把我们原本按需加载的散图打到同一张图集中,从而达到将多个drawcall合并的目的,难点在于其中的分块算法。
好在这个问题早就有一个开源的项目——UnityRuntimeSpriteSheetsGenerator,利用他我们可以很方便的在 运行时制作出我们需要的动态图集。
简单示例
这个开源项目一共只有6个文件,而其中制作动态图集用到的脚本为AssetPacker。1
2
3
4
5
6
7
8
9
10public void PackAtlas(string atlasName, UnityAction callback) {
GameObject gameObj = new GameObject();
AssetPacker assetPacker = gameObj.AddComponent<AssetPacker>();
packerDict.Add(atlasName, assetPacker);
assetPacker.useCache = false; //是否使用缓存
assetPacker.cacheName = atlasName; //图集名称
assetPacker.OnProcessCompleted.AddListener(callback); //打包回调
assetPacker.AddTexturesToPack(prepareToPackDict[atlasName].ToArray()); //添加要打包的散图
assetPacker.Process(); //开始打包
}
如果不使用缓存,那么打出的图集保存在内存中,而使用的话则会存放一张图片和对应的配置文件到应用目录下,并根据版本号进行管理。在使用完成后,只需要将上面的GameObject销毁,AssetPacker会自动调用Dispose销毁对应的资源。以此为基础,可以构建更进一步的管理和编辑器扩展。
额外的,AssetPacker有两处代码需要进行一些优化修改。
第一处,OnProcessCompleted需要给予一个默认值:1
public UnityEvent OnProcessCompleted = new UnityEvent();
第二处,在createPack方法中,将原先的foreach循环改为for循环:1
2
3for(int i=0; i<itemsToRaster.Count; i++){
// xxx
}
结合到FairyGUI装载器
回到这次的出发点,目的是在FairyGUI中支持动态图集,然后和装载器结合起来达到动态加载图片而不增加额外drawcall的目的。
首先根据FairyGUI的文档,我们需要继承GLoader来实现我们自己的装载方法。这里FairyGUI有一个巨大的坑:onExternalLoadSuccess需要传入一个NTexture对象,如果我们简单的使用new NTexture(texture)这个构造函数,会发现最后的drawcall并没有被合并。原因在于这种方式每次texture会被重新包装一次成NTexture,FairyGUI会把他们当作不同的贴图从而没有进行优化。
那么为什么原来的GLoader在读取同一张贴图时就可以呢?一路查看源码到UIPackage.cs的LoadImage方法中,我们发现这个时候使用了NTexture的另一个构造函数,它传入的第一个参数也是NTexture类型。
找到了两者的区别,我们就可以解决之前的困扰了。我们在AssetPacker生成图集后,将这份图集包装成NTexture缓存下来,每次都使用这个NTexture对象即可。1
onExternalLoadSuccess(new NTexture(ntex, rect, false)); //ntex为我们缓存下来的NTexture对象
最后还需要注意一个坐标系的问题,之前创建的图集信息是以左下为坐标原点的,而FairyGUI则是左上为坐标原点,因此我们的rect矩形需要做一下小的转换:1
rect.y = atlas.maxY - rect.y - sprite.height;
参考资料:
https://zhuanlan.zhihu.com/p/38004837
https://blog.csdn.net/cyf649669121/article/details/81534183