UGUI的图集处理方式-SpriteAtlas的前世今生

https://zhuanlan.zhihu.com/p/80584461

 

说到UGUI的图集初学者可能觉得没什么难度,包括我刚开始接触的时候也是,甚至你在开发的时候只需要把图片导入到项目中,拖拖拽拽就能做出能用的东西来。因为UGUI刚出的时候就打出了“Unity会自动帮你维护图集”的旗号。可现实真的是这样的吗?要解释这个问题就需要从Unity4.6说起了,那我们来捋一下!


Sprite Packer

散图的加载问题

这功能是从Unity4.6版本随着UGUI的问世一起发布的,我们在做开发的时候只需要把图片导入工程,设置一下,然后在通过tag标签Unity就会自动打成图集。但是,这种方法有个问题,就是我们在运行时无法通过代码取出某张图集中的一张小图。

所以后来开发者们做了一套prefab引用sprite的解决方案,做法就是:建一个空prefab挂一个自定义脚本,脚本里有一个sprite数组,我们在编辑时把要打的图集中的sprite添加到这个sprite数组中,然后在把prefab打成bundle,这样这个bundle中的脚本就会有atlas中的Sprite的引用,我们在脚本里写一个GetSprite方法即可。这样我们在运行时加载完bundle后就可以通过脚本取出atlas里的sprite。

using System.Collections.Generic;
using UnityEngine;
public class Test : MonoBehaviour
{
    public List<Sprite> sprites;
    public Sprite GetSprite(string spriteName)
    {
        return sprites.Find(s => s.name.Equals(spriteName));
    }
}

但是,讲真,就这么个函数Unity提供一个API很难嘛!这个问题导致从Unity4.6到Unity2017.1每次做图集动态加载的时候都要用上面的方法搞一遍。当然要是仅仅是这点工作量也就忍了,重要的是这么做还会影响到打包、依赖的管理。打包依赖又是个Unity的大坑。。。一个坑跳到了另一个更深的坑。。。当然也是有解决办法的,打包依赖问题我们以后再专门讲,这里就不多说了。

图集的拼接算法问题

SpritePacker的TightPackerPolicy(紧凑),根本就不能用!SpritePacker打图集时有两个选项:默认、紧凑,然而使用过你会知道其实这个“紧凑”选项是不能用的!!打出来的图会串,你用一张Image指定了一张Sprite A,运行时你会发现如果用“紧凑”打出来的图集会把A的范围算错,表现就是显示的图片不是A,可能只包括A的一部分,其他部分会显示别的图片,其实就是取A这张图片的时候位置没算对!!!最终你会发现也只能使用”默认”方式打图集了。如果想优化图集的空间利用率,只有一个办法就是自己写打图集的排列算法,当然Unity也提供了这个接口!


Sprite Atlas

延迟绑定

SpriteAtlas是2017.1版本以后更新的一个新功能,它可以把图片“手动”打成一张图集。如果你觉得这是Unity的一个新功能或者一个强化图集的功能那你就错了,如果仅仅是打张图集也没必要单独搞这么个玩意儿,其实这也是Unity在“还SpritePacker的帐”,当然我们上面也讲到了,可以肯定的一点是SpritePacker在商业项目中:是能用的!但是不可避免的你会为了填补SpritePacker的一些坑。

SpritePacker时代“图集”我们是看不到的,因为设计者的初衷是想让开发者完全不用考虑图集的事。但是,这跟游戏开发时的动态加载图集时矛盾的!我们要动态加载就必须要知道atlas名字和sprite名,这样才能在运行时动态的找到一张sprite!所以U3D程序员必须要清楚的知道你当前界面用的是哪atlas的哪个sprite。但是按照SpritePacker的做法他的初衷应该是想把图集干掉(干掉的意思让开发者不用关心),只需要考虑sprite即可,但是这是行不通的!

SpriteAtlas其实就是为了解决上面说的问题而发布的功能。通过它我们可以将atlas和UIprefab“解耦”。同时,在Unity编辑器状态下我们仍然可以利用未打成SpriteAtlas的Sprite进行开发。等开发完成我们再发布成SpriteAtlas。Unity提供了所谓“延迟绑定”(late bind)的技术来让我们实现这个功能。具体做法如下:

在Unity2017.1之后,UI prefab的Bundle在被加载的时候会有一个回调函数被调用:SpriteAtlasManager.atlasRequested,这个委托的定义是这样的:void RequestLateBindingAtlas(string atlasName, System.Action output);

(代码不严格,当成伪码看就行))

SpriteAtlasManager.atlasRequested += (atlasName, output) => {
  string path;//省略,atlas bundle加载路径
  AssetBundle bundle;//省略,加载atlas的asset bundle
  var atlas = bundle.LoadAsset<SpriteAtlas>(path + "/" + atlasName);
  output(atlas);
}

我们需要在运行时把atlas绑定到当前的prefab中,这个绑定是通过第一个参数"atlasName"关联的。如果prefab中引用了多张atlas他会被调用多次,每次都会把atlasName传过来,由“用户”决定加载哪张atlas。最后调用output(atlas)把图集传给prefab。

延迟绑定的缺陷(bug)

Include in Build选项

根据实测,这个选项的含义是:选,打包时不考虑依赖。不选,打包时考虑依赖。

举例:

文件夹"A"存放着要使用的图片,SpriteAtlas文件“A_Atlas”为文件A下所有图片打成的图集,Prefab文件“A_Prefab”引用了A下的图片。

此时,

如果选择Include in Build,那么打包的时A_Prefab打出来的bundle文件会包含A下面的图片,A_Atlas打出来的bundle也会包含A下面的图片,也就是说此时会打包双份资源。这是我们不想看到的。

如果不选Include in Build,只有A_Atlas打出来的bundle会包含A下的图片,而A_Prefab不会包含A下的图片。这样资源只打包了一份,是我们想要的结果。

但是,

有个地方需要注意就是,如果A_Atlas第一次勾选了Include in Build,打包后A_Prefab和A_Atlas的bundle都会包含图片资源,但是如果此时再把A_Atlas的Include in Build的勾去掉重新打包,Unity只会把A_Atlas的bundle重打,因为Unity认为你只修改了A_Atlas文件,而A_Prefab被认为”没有更改”,所以之前打进去的图片资源依然会存在,正确的做法应该是:如果Include in Build修改了,那么打包的时候所有用到这个SpriteAtlas的prefab也要重新打包(删掉bundle文件即可)

Unity编辑器状态下运行UI全部白图(bug)

这应该是Unity的一个bug,在这里分享一下!

在我们的实测中发现:在加载资源的时候如果使用延迟绑定的方式显示UI,在UnityEditor模式下UI的prefab加载显示后所有图片都是白图!但是发布到设备中能正确显示。

莫非延迟绑定只能运行时使用?


Texture Packer

Texture Packer最终的解决方案,当然这也是很早以前就使用的方法了。如果Unity能把这件事做好,我相信大家都不想自己单独用第三方软件去搞图集。TexturePacker具体功能和使用方法就不多说了,网上相关教程有很多。

这里我只说一下TexturePacker解决了Unity的哪些问题:

1.图集打包算法,选择很多,也更专业。

2.动态加载图片,简单明了。编辑时加载:AssetDatabase.LoadAllAssetsAtPath()或者AssetDatabase.LoadAllAssetRepresentationsAtPath();运行时加载:AssetBundle.LoadAllAssets()

3.图集的依赖关系,很直观。

当然也有缺点:

需要维护TexturePacker工程文件。

总结一下:

SpritePacker,商业项目可用,需要自己做点东西“填坑”。

SpriteAtlas,商业项目可用,他解决了SpritePacker的小问题,但是自己有带来了新问题,如果“白图”的bug解决不了,可能需要单独写一套编辑时的加载流程。感觉和SpritePacker填的坑差不多!

TexturePacker,商业项目可以,相对来说比较“健康”,不用额外写代码填坑。

从本文还可以看出一点,Unity对UI的重视程度还是比较小的!

就说到这里吧!Have a good day!

posted @ 2021-05-17 10:15  porter_代码工作者  阅读(447)  评论(0编辑  收藏  举报