UGUI的图集打包与工作原理,整整看了一天多,终于看明白了~晕~还是记录一下我研究的成果,也希望大家在下面给我留言我们一起讨论一下。
先说说UGUI的Atlas和NGUI的Atlas的区别,NGUI是必须先打出图集然后才能开始做界面。这一点很烦,因为始终都要去考虑你的UI图集。比如图集会不会超1024 ,图集该如何来规划等等。而UGUI的原理则是,让开发者彻底模糊图集的概念,让开发者不要去关心自己的图集。做界面的时候只用小图,而在最终打包的时候unity才会把你的小图和并在一张大的图集里面。然而这一切一切都是自动完成的,开发者不需要去care它。
如下图所示,Editor->Project Settings 下面有sprite packer的模式。Disabled表示不启用它,Enabled For Builds 表示只有打包的时候才会启用它,Always Enabled 表示永远启用它。 这里的启用它就表示是否将小图自动打成图集。
我的选项是Always Enabled 。因为开发的时候我们需要清楚的看到现在是几个Draw Call,从而才能优化小图。在最终打包的时候unity会自动构建大的图集,可是我开发的时候就想看图集会占几个Draw Call,这怎么办呢?如下图所示,首先将你的图片拖入unity中,将同一图集的所有图片的packing tag设置成一个名子即可。
注意你的图片不能放在Resources文件夹下面,Resources文件夹下的资源将不会被打入图集,切记(也就是在这里混淆了我很久)。然后在Windows->Sprite Packer 里,点击packer 在这里你就可以预览到你的图集信息。图集的大小还有图集的格式等等很多参数我们都是可以控制的,也可以通过脚本来设置。我在下一篇文章里详细说这个(请期待嘿嘿)。
图集的预览紧紧是让你看看你的图集大概张什么样子。那么我们的图集的这张图片保存在了哪里呢?它保存在和Assets文件夹同级的目录,Libary/AtlasCache里面。你不用管它,也不要删除它,就算你删除了也没用因为只要你打包,它就会生成并且会打到包中。
此时在Hierarchy视图中创建两个Image对象。如下图所示,我们可以清楚的看到此时我的draw call已经被合并成了1 。
这两个图片是我是在Editor模式下预先拖入Hierarchy视图中的,可是如果我想运行时根据图片的名子来动态创建精灵该如何?可是unity根本没有提供加载图集的方法,也没有提供加载图集上某个图片的方法。 因为UGUI就不像让开发者有图集的这个概念,可是我们肯定是要实现这个需求的。。怎么办呢?
第一个设想,先把散=小图打包成图集,然后再把所有散图拷贝在Resources文件夹下,这样运行时就能用Resources.load了。
第二个设想,还是先把小图打成图集,然后把所有小图关联在prefab上,拷贝在Resources文件夹下,这样运行时也能用Resources.load了。到底那个靠谱呢? 给大家看一个图大家就知道答案了。
如下图所示,打成图集的图片如果在放在Resources那么资源就变成双份了。。 所以我们只能把小图关联在Prefab上,把所有的Prefab放在Resources下面,这样就不占用多余的空间了。
好了,现在方法我们已经掌握,那么就开始写工具吧。如下图所示可以按文件夹分,每一个文件夹就是一个图集。然后每一张小图创建一个Prefab,Prefab的名子就起小图的名子,文件关联在Resources下面。
代码比较简单,我就不注释了。
[MenuItem ("MyMenu/AtlasMaker")]
static private void MakeAtlas()
{
string spriteDir = Application.dataPath +"/Resources/Sprite";
if(!Directory.Exists(spriteDir)){
Directory.CreateDirectory(spriteDir);
}
DirectoryInfo rootDirInfo = new DirectoryInfo (Application.dataPath +"/Atlas");
foreach (DirectoryInfo dirInfo in rootDirInfo.GetDirectories()) {
foreach (FileInfo pngFile in dirInfo.GetFiles("*.png",SearchOption.AllDirectories)) {
string allPath = pngFile.FullName;
string assetPath = allPath.Substring(allPath.IndexOf("Assets"));
Sprite sprite = Resources.LoadAssetAtPath<Sprite>(assetPath);
GameObject go = new GameObject(sprite.name);
go.AddComponent<SpriteRenderer>().sprite = sprite;
allPath = spriteDir+"/"+sprite.name+".prefab";
string prefabPath = allPath.Substring(allPath.IndexOf("Assets"));
PrefabUtility.CreatePrefab(prefabPath,go);
GameObject.DestroyImmediate(go);
}
}
}
然后是运行时的代码。
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class UIMain : MonoBehaviour {
void Start ()
{
CreatImage(loadSprite("image0"));
CreatImage(loadSprite("image1"));
}
private void CreatImage(Sprite sprite ){
GameObject go = new GameObject(sprite.name);
go.layer = LayerMask.NameToLayer("UI");
go.transform.parent = transform;
go.transform.localScale= Vector3.one;
Image image = go.AddComponent<Image>();
image.sprite = sprite;
image.SetNativeSize();
}
private Sprite loadSprite(string spriteName){
return Resources.Load<GameObject>("Sprite/" + spriteName).GetComponent<SpriteRenderer>().sprite;
}
}
因为这两个图是在同一个图集上,所以drawcall就是1了。这样我们就可以根据图片的名子来运行时加载图片了。
接下来就是Assetbundle了,如果我们的图集需要在线更新那该怎么办呢? 其实Assetbundle比Resources要更简单一些,无论如何我们要先开始打图集。
[MenuItem ("MyMenu/Build Assetbundle")]
static private void BuildAssetBundle()
{
string dir = Application.dataPath +"/StreamingAssets";
if(!Directory.Exists(dir)){
Directory.CreateDirectory(dir);
}
DirectoryInfo rootDirInfo = new DirectoryInfo (Application.dataPath +"/Atlas");
foreach (DirectoryInfo dirInfo in rootDirInfo.GetDirectories()) {
List<Sprite> assets = new List<Sprite>();
string path = dir +"/"+dirInfo.Name+".assetbundle";
foreach (FileInfo pngFile in dirInfo.GetFiles("*.png",SearchOption.AllDirectories))
{
string allPath = pngFile.FullName;
string assetPath = allPath.Substring(allPath.IndexOf("Assets"));
assets.Add(Resources.LoadAssetAtPath<Sprite>(assetPath));
}
if(BuildPipeline.BuildAssetBundle(null, assets.ToArray(), path,BuildAssetBundleOptions.UncompressedAssetBundle| BuildAssetBundleOptions.CollectDependencies, GetBuildTarget())){
}
}
}
static private BuildTarget GetBuildTarget()
{
BuildTarget target = BuildTarget.WebPlayer;
#if UNITY_STANDALONE
target = BuildTarget.StandaloneWindows;
#elif UNITY_IPHONE
target = BuildTarget.iPhone;
#elif UNITY_ANDROID
target = BuildTarget.Android;
#endif
return target;
}
如下图所示,我的assetbundle已经打出来了。
然后把UIMain.cs在改一改。
using UnityEngine;
using System.Collections;
using UnityEngine.UI;
public class UIMain : MonoBehaviour {
AssetBundle assetbundle = null;
void Start ()
{
CreatImage(loadSprite("image0"));
CreatImage(loadSprite("image1"));
}
private void CreatImage(Sprite sprite ){
GameObject go = new GameObject(sprite.name);
go.layer = LayerMask.NameToLayer("UI");
go.transform.parent = transform;
go.transform.localScale= Vector3.one;
Image image = go.AddComponent<Image>();
image.sprite = sprite;
image.SetNativeSize();
}
private Sprite loadSprite(string spriteName){
#if USE_ASSETBUNDLE
if(assetbundle == null)
assetbundle = AssetBundle.CreateFromFile(Application.streamingAssetsPath +"/Main.assetbundle");
return assetbundle.Load(spriteName) as Sprite;
#else
return Resources.Load<GameObject>("Sprite/" + spriteName).GetComponent<SpriteRenderer>().sprite;
#endif
}
}
如下图所示,依然还是一个drawcall。
衷心希望有经验的朋友在留言处给我提提意见, 或者大家一起讨论讨论。。 我们共同为把NGUI干掉的目标而奋斗,嘻嘻。
- 本文固定链接: https://www.xuanyusong.com/archives/3304
- 转载请注明: 雨松MOMO于 雨松MOMO程序研究院 发表








不错,不错,可以参考一下
找不到这个路径/Atlas
我的怎么没有这么麻烦,直接打成小图就可以用了呀!
雨松,能不能提供下源码,这个sprite打包的,一直没成功:(
雨松你好,请教一个问题,我将多个sprite打成图集,并打成Assetbundle。然后客户端通过www.loadFromCacheOrDownload下载图集。如果我一个uiimage想要换图的话,继续使用loadFromCacheOrDownload,可以实现功能,但这是异步的。当我加载多个图片的时候会逐个进行显示,表现效果很奇怪。CreateFromFile或者LoadAssetFromFile 都需要知道明确的文件地址,loadFromCache的文件路径地址,有没有什么好的解决方法
你可以把图集的assetbundle打成压缩包,然后下载到本地目录,然后根据createfromfile加载,不就好了
雨松请教一个材质贴图打图集,打AB后,加载出来贴图丢失的问题:我把材质贴图打成了图集,并打成了AB。接着Material引用这些贴图,Material打成AB,新建一个cube的立方体,材质用刚才的Material,cube打成AB。这时我有3个AB,一个是贴图的AB,Material的AB,cube的AB,以及一个材质贴图的图集。然后分别加载这三个AB,再从cube的AB中取出cube并实例化,结果发现材质cube上的贴图丢失。反复试验后,发现如果不把材质贴图打成图集,就不会丢失。请问这时怎么回事? 难道unity不支持这样?感觉应该至于吧,同一个npc的材质贴图应该是要打到一个图集中用于降低CD的呀。难道用了AB就不行啦?
自己解决了。必须把图片的Sprite mode:Multiple,如果选single的话,就会出现从AB加载Material丢失贴图。但是如果不打图集就没事。还是有疑问,发现能打图集的图片,只能是类型是:Sprite (2D and UI),其他类型不支持。不知道雨松用的什么方案?
你npc模型的图不应该和ugui的ui图片做混淆。模型的纹理图片你要降低DC的话也应该是多个npc的纹理图合并成一张大纹理图,然后不同的npc模型的uv取值不同。多个npc模型用一个材质,一个材质用一张大纹理。而不是多个npc用到了多个图片,然后把这些图片用ugui做成图集再打包。如果你非要用ugui的合并图集的功能。
你不把材质贴图打成图集不会丢失的原因是,你多个npc的材质引用了不同的图片,这些图片自然而然的被打包到了material的ab包里了。
反正就是你想降低DC,就得多个npc共用一张纹理,分配不同的uv。
非常感谢雨松的方案:1.我用的5.3.4f1的版本,实验了下你介绍的方案,得出的实验结果和有稍微有点不同。如果不打图集,只是多张图打到一个AB中,然后从这个AB中读出多张小图加载到UI上,这时始终只消耗1个calldraw。也就是说用AB,可以不打图集。其他版本没试过,不知道具体情况。2.我看你在给下面的朋友回复说,你最新的方案是把图集打成AB。请介绍下大致的流程。因为我理解如果打了图集,大图并不在Asset目录下。这个怎么做热更新?以及差异更新包?
理论上来说如果这样的话,首先你创建了一个go,引用了AB里面的一个资源a.png,然后当你清理这个AB并且执行了Resources.UnloadUnusedAssets()以后,别的地方再引用了这个AB的别资源b.png图,这时候应噶就会产生重复资源了。那这时候calldraw原本是1的就会变成2了吧?
修正下,5.3.4f1最新得出的结果与雨松的一致。也就是既打图集又打AB. 之前为什么出现只打AB,而不打图集就能降低CD的原因是在于我最开始是打了图集的,如果在没有关闭工程的情况下,再取消打图集,打AB,这样CD能降低,但重新打开工程就不能降低CD了。不知道这算不算unity的一个BUG.
雨松大神,还有个问题请指导一下:我现在是一个UI使用自己的图集,但是按钮这些通用的,所有UI使用一个Common图集。打bundle时,只打UI.prefab的bundle,这时候它会自动使用自己的图集没问题,但是因为“每个UI”都会带上通用的图,导致bundle变大,并且这样貌似每个UI加载都会再通用图集Common的图集。我的解决是:1.把这个Common图集打成一个bundle,但是这样打出来的UI还是原样大小,并不起啥使用。 (这个common.bundle和UI并无依赖,我白加了貌似)2.为了让他们有依赖,然后我用你的方式,一个Common图片打一个bundle,发现每个图片的bundle都得1M多,并且ui.bundle也是原样大小,也无改善。3.再想着,如果我把所有图片像你这样关联到一个pic.prefab,再将它打bundle,运行时先加载它和它上面的图片,再加载ui.bundle,不知道能不能这样做,但如果就算这样,我的bundle貌似还是会关联么通用的图片,还是那么大,没办法解决这个问题。请momo指导一下思路。
雨松,我测试发现个问题,就是如果只要我ui用了这个图集的一个图片,打出的ui.bundle我用bundle export解开看了下,它是只有这一个图片,但它的大小,是只要用了哪个图集,就把这个图集的大小全打进去了,如果用了a图集一张和b图集的一张,和用了a图集的n张和b图集的n张没有区别,会有其它的不用的无名称的sprite占位。不知道是不是和版本有关系,我是 5.0.1
BuildPipeline.PushAssetDependencies()这种老的方法打出公共依赖包貌似能解决,但5,0以上都不行了,希望有知道的朋友能回复momo这里。
要对图集单独命名
雨松大神,有个问题请指导一下:我的资源是:Asserts/Texture/UINameTexture (这里放的是UI的图片,我每个设置了tag,相同的这一个UI是一个静态图集) Asserts/Resources/UIName (这里放的是UI的prefab,它的图是直接使用UINameTexture里的图直接拖上去的2dsprite) (UI是动态更新的)Asserts/Resources/UIDynamic (这里是我的动态资源,比如英雄头像,装备图标), 我用的时候是在UI脚本里2dSprite = Resources.Load()加载赋值的. (它们每种类型放进一个图集)我的策略:所有的UI是一个xml,查找最新的UI,然后只更新UI.prefab,如果UI只更新了一个图,那么也是彻底更新这个UI的assertbundle。我的问题:1.我现在动态更新的UIName.prefab->打成的uiname.assertbundle 这个文件很大(最大的有7M左右),请帮我看下我有什么可以优化的地方。2.为什么我打出来的bundle这么大,有什么不合理的地方吗?我应该再怎么组织资源?3.我最初的时候是把UIName的prefab放在Resources外面的,然后直接所有的UI都打成bundle使用StreamingAssserts里(1)这样打出来的apk很大,但打成的uiname.assertbundle只有500K很小,更新出完全可以。(2)但这样打出来的包很大,就改成现在放Resources目录,现在apk是小了,但是uiname.assertbundle大到不可以接受了。请雨松大神给点建议。
我建议你用uwa的工具查看一下你的assetbundli都有什么。
谢谢雨松回复,看了一下,应该是我把字体放到Resources下面的原因,这样它就直接依赖了,那我就把字体直接使用assetbundle的形式放到StreamingAsserts应该就能减少到字体4M的大小。它这个uwa也有问题:不能看到隐藏的结点及下面的问题,收费贵死人啊哈哈。谢谢雨松,实践后再回复你。
免费的啊。 uwa看assetbunld
就是这个原因。
大神您好,我想问您一下,我想做一个类似内存池的东西,可以根据内存大小来卸载多余的图集,但是我怎么能知道图集占用多少内存,或者图集的大小是多少?
profiler里可以看
但是代码怎么能取到,因为是要做一个管理内存的东西,需要知道每个图集的大小,或者分辨率也行。
抱歉再打扰一下,我使用IPackerPolicy的时候可以知道最终实际的atlas长宽大小么,如果不可以,有什么其他方法知道atlas最终实际大小么?我的想法是打包的时候把atlas长宽写在一个文件里,然后运行时候读这个文件。
sprite.texture.width 应该就可以取出来
可以了,太感谢了!
图片序列化在一个prefab上关联脚本可以分享一下吗
大神想问一下,打好图集后,Drawcall是正常的,但是把拼好的图旋转以后Drawcall就不正常了,回到了没有打图集时候的Drawcall。
我实例化很多并排显示,没有旋转的情况下Drawcall不会增加,当旋转以后,Drawcall就不正常了
调整右边面板上的transform组件上的旋转就不会增加DC,但用场景窗口上面的那个按钮旋转就会增加DC
雨松大神 请问你测试这个用的Unity哪个版本?
还有一个问题:我使用Unity5.0.1将制作成图集后的Sprite打包成一个AssetBundle后包的大小反而增大了,比不制作成图集在打包成AssetBundle的包大了几倍,然后我改成Unity5.2.4后就好了,请问这是Unity5.0.1的bug吗?
有可能是5.0.1哪个版本不太稳定。
雨松 你好 请教您个问题:把一些UI预制体(没有Sprite)打包成了AssetBundle,然后将打包成图集后的sprite也打包成AssetBundle,最后将Sprite和UI预制体在AssetBundle加载出来,再将Sprite放到UI预制体上,运行后DrawCall并没有降低。请问该怎么解决?
NGUI作者多良心,一直源码给你,而且更新频繁,正式有了它我们才没有在ui这一块浪费太多的时间,虽然现在原生的确实比NGUI用,但是不要说干掉NGUI嘛,心酸啊!
雨松大神,真心求教个问题,不知道是我测试用例的原因,老是没有得到正确的测试结果,所以特来求教,万望解惑呢
是关于打Atlas的AssetBunlde问题,(用你这篇文章的demo工程目录结构):
(1)假如我有一堆英雄头像作为一个图集Atals_HeroIcon,然后这些英雄头像是需要热更新的,比如说精灵A,因为某些特殊需求,在某个界面上一定只需要显示A,那么这时候假如我不用代码动态读取AssetBunlde中的A来放上去,而是直接把(目录:Atlas/../A.png)拖到对应的Image上。然后问题来了,这个时候,如果ipa或者apk是会包括Atals_HeroIcon这个大图吗?如果成立,可不可以反过来说,假如某个图集需要热更新或者全部都用代码来动态加载sprite,那么就可以节省包体大小?
不过我测试了貌似没有什么效果咧,求指点。
(2)假如有个图集Atals_Common,里面有些sprite需要代码动态Resources Load出来,有些只需要把小图给直接拖上去就好了,但这个图集不需要热更新,那么,其实是不用打AssetBunlde的是吧?只需把这个图集生成一份prefab,确保可以让某些sprite可以代码动态Resources Load出来,其他的该拖的拖?
(3)假如我上面两点猜的没有错,那一般管理的处理流程是不是就为这样呢?热更新的图集全部用代码,保证没有任何关联的饮用;其他的图集都直接生成一份prefab,以供能动态用Resources给Load出来?
雨松大神,真心求教个问题,不知道是我测试用例的原因,老是没有得到正确的测试结果,所以特来求教,万望解惑呢
是关于打Atlas的AssetBunlde问题,(用你这篇文章的demo工程目录结构):
(1)假如我有一堆英雄头像作为一个图集Atals_HeroIcon,然后这些英雄头像是需要热更新的,比如说精灵A,因为某些特殊需求,在某个界面上一定只需要显示A,那么这时候假如我不用代码动态读取AssetBunlde中的A来放上去,而是直接把(目录:Atlas/../A.png)拖到对应的Image上。然后问题来了,这个时候,如果ipa或者apk是会包括Atals_HeroIcon这个大图吗?如果成立,可不可以反过来说,假如某个图集需要热更新或者全部都用代码来动态加载sprite,那么就可以节省包体大小?
不过我测试了貌似没有什么效果咧,求指点。
(2)假如有个图集Atals_Common,里面有些sprite需要代码动态Resources Load出来,有些只需要把小图给直接拖上去就好了,但这个图集不需要热更新,那么,其实是不用打AssetBunlde的是吧?只需把这个图集生成一份prefab,确保可以让某些sprite可以代码动态Resources Load出来,其他的该拖的拖?
(3)假如我上面两点猜的没有错,那一般管理的处理流程是不是就为这样呢?热更新的图集全部用代码,保证没有任何关联的饮用;其他的图集都直接生成一份prefab,以供能动态用Resources给Load出来?
你说的是正确的, 我说说我现在的方法把。 每个图集做成assetbundle. 然后每个界面的布局prefab也做成assetbundle. 来进行依赖图集。打开界面实际上就是 从assetbundle里把ui的prefab实例化进来。至于prefab上一些比如头像这种用代码根据名字动态创建。
打包成图集比原包大了不少,请问大图集应如何压缩呢
应该是你的UI使用不同图集的原因,我也有这个问题,本来UI图片总只有100k,因为使用的公共图片的2M,导致我的uiprefab.assertbundle也会变成2M多(如果依赖图片不在Resources里,它公共一依赖的东西在不同UI里就是多份)感觉:1.把公共依赖的图再单独打成一个bundle,UI使用全bundle的方式加载 (如果预加载公共bundle的话,之后的UI加载会快好多)2.共同图放到resources,不能更新,但会减少UI打出bundle的大小。借momo宝地继续实践完回复。
你好啊,请问一下按照第二种做法,感觉好像只能在代码中对Image设置Sprite,那么在编辑界面中对Image设置的Sprite还管用?
哈喽,雨松大大,问你个问题吗,当sprite设置了tag,表示他属于某个图集,在设置assetbundle和不设置的时候,我发现对应的预置键的大小都是一样的,只是存在有没有依赖关系,感觉像是预置键里自动拷贝了一份sprite。如果设置assetbundle会增加包大小,不设置,那么在做热更新的时候又不能直接更新sprite。。而且很好奇的是,既然设置了tag,那么系统应该自动打成一张大图吧?那那张大图在打包assetbundle又在哪里呢?