首页 >Unity3D频道 >【UGUI研究院】 > UGUI研究院之全面理解图集与使用(三)
2014
10-24

UGUI研究院之全面理解图集与使用(三)

UGUI的图集打包与工作原理,整整看了一天多,终于看明白了~晕~还是记录一下我研究的成果,也希望大家在下面给我留言我们一起讨论一下。

先说说UGUI的Atlas和NGUI的Atlas的区别,NGUI是必须先打出图集然后才能开始做界面。这一点很烦,因为始终都要去考虑你的UI图集。比如图集会不会超1024 ,图集该如何来规划等等。而UGUI的原理则是,让开发者彻底模糊图集的概念,让开发者不要去关心自己的图集。做界面的时候只用小图,而在最终打包的时候unity才会把你的小图和并在一张大的图集里面。然而这一切一切都是自动完成的,开发者不需要去care它。

如下图所示,Editor->Project Settings 下面有sprite packer的模式。Disabled表示不启用它,Enabled For Builds 表示只有打包的时候才会启用它,Always Enabled 表示永远启用它。 这里的启用它就表示是否将小图自动打成图集。

Snip20141024_2我的选项是Always Enabled 。因为开发的时候我们需要清楚的看到现在是几个Draw Call,从而才能优化小图。在最终打包的时候unity会自动构建大的图集,可是我开发的时候就想看图集会占几个Draw Call,这怎么办呢?如下图所示,首先将你的图片拖入unity中,将同一图集的所有图片的packing tag设置成一个名子即可。

Snip20141024_3

注意你的图片不能放在Resources文件夹下面,Resources文件夹下的资源将不会被打入图集,切记(也就是在这里混淆了我很久)。然后在Windows->Sprite Packer 里,点击packer 在这里你就可以预览到你的图集信息。图集的大小还有图集的格式等等很多参数我们都是可以控制的,也可以通过脚本来设置。我在下一篇文章里详细说这个(请期待嘿嘿)。

Snip20141024_4

图集的预览紧紧是让你看看你的图集大概张什么样子。那么我们的图集的这张图片保存在了哪里呢?它保存在和Assets文件夹同级的目录,Libary/AtlasCache里面。你不用管它,也不要删除它,就算你删除了也没用因为只要你打包,它就会生成并且会打到包中。

此时在Hierarchy视图中创建两个Image对象。如下图所示,我们可以清楚的看到此时我的draw call已经被合并成了1 。

Snip20141024_5

这两个图片是我是在Editor模式下预先拖入Hierarchy视图中的,可是如果我想运行时根据图片的名子来动态创建精灵该如何?可是unity根本没有提供加载图集的方法,也没有提供加载图集上某个图片的方法。 因为UGUI就不像让开发者有图集的这个概念,可是我们肯定是要实现这个需求的。。怎么办呢?

第一个设想,先把散=小图打包成图集,然后再把所有散图拷贝在Resources文件夹下,这样运行时就能用Resources.load了。

第二个设想,还是先把小图打成图集,然后把所有小图关联在prefab上,拷贝在Resources文件夹下,这样运行时也能用Resources.load了。到底那个靠谱呢? 给大家看一个图大家就知道答案了。

如下图所示,打成图集的图片如果在放在Resources那么资源就变成双份了。。 所以我们只能把小图关联在Prefab上,把所有的Prefab放在Resources下面,这样就不占用多余的空间了。

Snip20141024_6

好了,现在方法我们已经掌握,那么就开始写工具吧。如下图所示可以按文件夹分,每一个文件夹就是一个图集。然后每一张小图创建一个Prefab,Prefab的名子就起小图的名子,文件关联在Resources下面。

Snip20141024_7

 

代码比较简单,我就不注释了。

	[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了。这样我们就可以根据图片的名子来运行时加载图片了。

Snip20141024_8

接下来就是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已经打出来了。

Snip20141024_10

然后把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。

Snip20141024_8

 

衷心希望有经验的朋友在留言处给我提提意见, 或者大家一起讨论讨论。。 我们共同为把NGUI干掉的目标而奋斗,嘻嘻。

 

最后编辑:
作者:雨松MOMO
专注移动互联网,Unity3D游戏开发
捐 赠写博客不易,如果您想请我喝一杯星巴克的话?就进来看吧!

UGUI研究院之全面理解图集与使用(三)》有 184 条评论

  1. avi9111说:

    不错,不错,可以参考一下

  2. 王楠说:

    找不到这个路径/Atlas

  3. 张雷说:

    我的怎么没有这么麻烦,直接打成小图就可以用了呀!

  4. 见林说:

    雨松,能不能提供下源码,这个sprite打包的,一直没成功:(

  5. 雅伦说:

    雨松你好,请教一个问题,我将多个sprite打成图集,并打成Assetbundle。然后客户端通过www.loadFromCacheOrDownload下载图集。如果我一个uiimage想要换图的话,继续使用loadFromCacheOrDownload,可以实现功能,但这是异步的。当我加载多个图片的时候会逐个进行显示,表现效果很奇怪。CreateFromFile或者LoadAssetFromFile 都需要知道明确的文件地址,loadFromCache的文件路径地址,有没有什么好的解决方法

  6. john说:

    雨松请教一个材质贴图打图集,打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就不行啦?

    • john说:

      自己解决了。必须把图片的Sprite mode:Multiple,如果选single的话,就会出现从AB加载Material丢失贴图。但是如果不打图集就没事。还是有疑问,发现能打图集的图片,只能是类型是:Sprite (2D and UI),其他类型不支持。不知道雨松用的什么方案?

    • ee说:

      你npc模型的图不应该和ugui的ui图片做混淆。模型的纹理图片你要降低DC的话也应该是多个npc的纹理图合并成一张大纹理图,然后不同的npc模型的uv取值不同。多个npc模型用一个材质,一个材质用一张大纹理。而不是多个npc用到了多个图片,然后把这些图片用ugui做成图集再打包。如果你非要用ugui的合并图集的功能。
      你不把材质贴图打成图集不会丢失的原因是,你多个npc的材质引用了不同的图片,这些图片自然而然的被打包到了material的ab包里了。
      反正就是你想降低DC,就得多个npc共用一张纹理,分配不同的uv。

  7. john说:

    非常感谢雨松的方案:1.我用的5.3.4f1的版本,实验了下你介绍的方案,得出的实验结果和有稍微有点不同。如果不打图集,只是多张图打到一个AB中,然后从这个AB中读出多张小图加载到UI上,这时始终只消耗1个calldraw。也就是说用AB,可以不打图集。其他版本没试过,不知道具体情况。2.我看你在给下面的朋友回复说,你最新的方案是把图集打成AB。请介绍下大致的流程。因为我理解如果打了图集,大图并不在Asset目录下。这个怎么做热更新?以及差异更新包?

    • Dean说:

      理论上来说如果这样的话,首先你创建了一个go,引用了AB里面的一个资源a.png,然后当你清理这个AB并且执行了Resources.UnloadUnusedAssets()以后,别的地方再引用了这个AB的别资源b.png图,这时候应噶就会产生重复资源了。那这时候calldraw原本是1的就会变成2了吧?

      • john说:

        修正下,5.3.4f1最新得出的结果与雨松的一致。也就是既打图集又打AB. 之前为什么出现只打AB,而不打图集就能降低CD的原因是在于我最开始是打了图集的,如果在没有关闭工程的情况下,再取消打图集,打AB,这样CD能降低,但重新打开工程就不能降低CD了。不知道这算不算unity的一个BUG.

  8. 阿举说:

    雨松大神,还有个问题请指导一下:我现在是一个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

  9. 阿举说:

    雨松大神,有个问题请指导一下:我的资源是: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大到不可以接受了。请雨松大神给点建议。

  10. 大神您好,我想问您一下,我想做一个类似内存池的东西,可以根据内存大小来卸载多余的图集,但是我怎么能知道图集占用多少内存,或者图集的大小是多少?

  11. 郭爱民说:

    图片序列化在一个prefab上关联脚本可以分享一下吗

  12. 袁晓林说:

    大神想问一下,打好图集后,Drawcall是正常的,但是把拼好的图旋转以后Drawcall就不正常了,回到了没有打图集时候的Drawcall。

  13. 冬晓说:

    雨松大神 请问你测试这个用的Unity哪个版本?

  14. 冬晓说:

    还有一个问题:我使用Unity5.0.1将制作成图集后的Sprite打包成一个AssetBundle后包的大小反而增大了,比不制作成图集在打包成AssetBundle的包大了几倍,然后我改成Unity5.2.4后就好了,请问这是Unity5.0.1的bug吗?

  15. 冬晓说:

    雨松 你好 请教您个问题:把一些UI预制体(没有Sprite)打包成了AssetBundle,然后将打包成图集后的sprite也打包成AssetBundle,最后将Sprite和UI预制体在AssetBundle加载出来,再将Sprite放到UI预制体上,运行后DrawCall并没有降低。请问该怎么解决?

  16. DAI说:

    NGUI作者多良心,一直源码给你,而且更新频繁,正式有了它我们才没有在ui这一块浪费太多的时间,虽然现在原生的确实比NGUI用,但是不要说干掉NGUI嘛,心酸啊!

  17. TrustTJM说:

    雨松大神,真心求教个问题,不知道是我测试用例的原因,老是没有得到正确的测试结果,所以特来求教,万望解惑呢

    是关于打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出来?

  18. TrustTJM说:

    雨松大神,真心求教个问题,不知道是我测试用例的原因,老是没有得到正确的测试结果,所以特来求教,万望解惑呢

    是关于打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宝地继续实践完回复。

  19. 你好啊,请问一下按照第二种做法,感觉好像只能在代码中对Image设置Sprite,那么在编辑界面中对Image设置的Sprite还管用?

  20. 哈喽,雨松大大,问你个问题吗,当sprite设置了tag,表示他属于某个图集,在设置assetbundle和不设置的时候,我发现对应的预置键的大小都是一样的,只是存在有没有依赖关系,感觉像是预置键里自动拷贝了一份sprite。如果设置assetbundle会增加包大小,不设置,那么在做热更新的时候又不能直接更新sprite。。而且很好奇的是,既然设置了tag,那么系统应该自动打成一张大图吧?那那张大图在打包assetbundle又在哪里呢?

留下一个回复

你的email不会被公开。 必填项已用 * 标注。