UnitySpriteAtlas:使用SpriteAtlas减少游戏DrawCall
- UnitySpriteAtlas
- 开源代码
- 简介
- 什么是DrawCall
- 什么是SpriteAtlas
- 如何打包图集
- 图集类型(Type)
- Include in Build
- 使用和加载图集资源
- Resources加载图集
- AssetBundle加载图集
- 后期绑定(Late Binding)
- atlasRequested事件
- atlasRegisterer事件
- 需要注意的问题
- 总结
- 重点
- 参考
https://gitcode.net/hankangwen/unityspriteatlas
游戏中的图片模型最终是要给到显卡去渲染的,图片开始是要从硬盘读到内存里面去的,然后CPU通知GPU要开始渲染,这一次通知就是一次Drawcall。通知的时候还要把渲染数据告诉GPU,因此会消耗一些性能。
什么是SpriteAtlas在Unity中,通常渲染一个纹理会调用一次DrawCall。一个2D项目,可能包含大量的纹理,如果绘制每个纹理都调用一次DrawCall,这会占用过多的资源,从而影响整个应用程序的性能。
为了降低性能消耗,我们可以使用精灵图集(Sprite Atlas)技术,它能够将多个纹理(texture)合并成一个大纹理,当访问图集中的多个纹理时,也只需要调用一次DrawCall 。
如何打包图集本文主要讲解如何使用SpriteAtlas打包图集。
在使用之前,需要先安装2D Sprite插件,具体步骤为 Window -> Package Manager,将Packages选择为Unity Registry,搜索2D Sprite并且下载,如下图所示:
安装完成后,开启图集打包的功能, Edit -> Project Settings -> Editor -> Sprite Packer-> Mode,默认是Disable,我们可以选择下面四种模式之一(不同版本的选项可能并不一致,我用的是Unity2020.3.18):
- Disabled:禁用图集相关功能。
- Sprite Atlas V1 - Enabled For Builds:仅当发布(Build)的时候构建图集,在Editor和Play模式中引用原始纹理而非图集中的纹理。
- Sprite Atlas V1 - Always Enabled:在运行时(Build和Play模式)引用图集中的纹理,在Editor模式中引用原始纹理。
- Sprite Atlas V2 (Experimental) - Enabled :Unity2020.3版本这个功能还处于实验中,默认在运行时引用图集中的纹理。Sprite Atlas V1 不支持缓存服务器(Cache Server),Unity只能将打包的图集数据存储在Library/AtlasCache文件夹中,并且也不能有依赖项,不支持命名对象导入器(named objects importer),而Sprite Atlas V2提供了对上述功能的支持。具体可以参考 Unity Manual:Sprite Atlas Version 2
这里选用Sprite Atlas V2,选择好之后,创建一个图集,Create -> 2D -> Sprite Atlas。
我们看看新建的图集包含哪些属性,选中新建的图集,下面是它的属性面板
对于其中比较模糊的概念,单独拎出来讲解
图集类型(Type)图集分为主图集和变体图集,那什么是Variant(变体图集)呢?切换成Variant模式看一下,截取属性面板中部分不同的内容
可以发现,它多了Master Atlas和Scale属性。
- Master Atlas:设置主图集内容的副本作为自己的内容。
- Scale:设置变体图集的缩放因子,介于0.1~1之间。
变体图集的作用是什么呢?它的主要目的是创建与主图集不同分辨率的图集,主图集中Sprite的分辨率 * Scale(缩放因子)得到的结果就是变体图集中Sprite的分辨率,它自身不包含Objects for Packing属性,因此变体图集中的内容都是主图集的副本。
当项目中既有主图集,又有该主图集的变体图集时,可以使用这两个图集中任意一个的Sprite。如果要自动从变体图集而不是主图集中加载Sprite,那就仅为变体图集启用Include in Build选项,并关闭主图集的这个选项。
Include in BuildUnity在打包的项目中会包含图集,并且在运行时自动加载它们,取消勾选图集的Include in Build选项可以禁用此行为。
如果禁用Include in Build,Unity仍会将图集打包到项目Assets文件夹中的** .spriteatlas * 文件中,只是运行的时候不会加载到内存中。因此,当精灵引用已禁用的图集中的纹理,由于引用纹理不可用或未加载(not available or loaded),该纹理将无法被找到,引用它的图片将显示为空白。此时要加载精灵图集,必须使用脚本通过**后期绑定(Late Binding)**执行此操作。(后期绑定稍后会讲)
使用和加载图集资源意义:打包的时候是否要把图集打进包体里(选中就是打入包体)。如果不打入包体里就需要代码从服务器下载或者包体外加载需要的图集。
将Sprite添加进图集前需要将自身的Texture Type设置为Sprite(2D and UI),然后才能将其加入图集的Objects for Packing进行打包,添加后记得点击Pack Preview按钮保存更改(因版本而异,使用Sprite Atlas V1不需要点击),打包完成后我们可以对图集进行预览。
图集可以放在Resources文件夹中加载,也可以打包到AssetBundle中进行加载。
Resources加载图集我们依然使用上面例子中的两个Image,假设打包的图集位于Resources文件夹中并且名为FirstAtlas,图集中的两张Sprite名字分别为heart和mail,场景中的两个Image分别是firstImg和secondImg,挂载脚本并为Image赋值。我们可以通过下面代码获取图集并且加载里面的Sprite。
using UnityEngine;
using UnityEngine.U2D;
using UnityEngine.UI;
public class TestLoadAtlas : MonoBehaviour
{
public Image firstImg;
public Image secondImg;
private void Awake()
{
SpriteAtlas atlas = Resources.Load("FirstAtlas"); //图集名称
Sprite message = atlas.GetSprite("heart"); //精灵体名称
Sprite lovely = atlas.GetSprite("mail");
firstImg.sprite = message;
secondImg.sprite = lovely;
}
}
运行后可以看到,虽然有两张Sprite不同的图片,但它们的DrawCall依然为1,说明合理使用图集确实能降低DrawCall。
AssetBundle加载图集下载AssetBundles-Browser解压并添加到Plugins目录下;看到有如下报错
删除AssetsPluginsAssetBundles-Browser-1.7.0Tests目录,报错消失。
给图集(FirstAtlas)设置标签
通过Window>AssetBundles Browser打开AssetBundles Browser,设置好输出位置,点击Build。
AssetBundle加载代码
using UnityEngine;
using UnityEngine.U2D;
using UnityEngine.UI;
public class TestLoadAtlas : MonoBehaviour
{
public Image firstImg;
public Image secondImg;
public bool loadFromAssetBundle = true;
private void Awake()
{
SpriteAtlas atlas = loadFromAssetBundle ? LoadFromAssetBundle("FirstAtlas") : LoadFromResources("FirstAtlas");
Sprite message = atlas.GetSprite("heart"); //精灵体名称
Sprite lovely = atlas.GetSprite("mail");
firstImg.sprite = message;
secondImg.sprite = lovely;
}
public SpriteAtlas LoadFromAssetBundle(string atlasName)
{
//AB包保存路径及名称
AssetBundle bundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"firstatlas.atlas");
//从AB包中加载图集
return bundle.LoadAsset(atlasName);
}
public SpriteAtlas LoadFromResources(string atlasName)
{
return Resources.Load(atlasName);
}
}
后期绑定(Late Binding)
前面我们提到,一个图集如果没有勾选Include in Build选项,只能通过后期绑定来加载里面的Sprite。所谓后期绑定,就是Unity在加载资源时,对于引用到的Sprite,Unity由于不知道Sprite和图集的引用关系,所以会发送一个请求加载这个Sprite的事件。使用者需要监听这个事件(即SpriteAtlasManager.atlasRequested回调),然后把这个Sprite所属的图集返回给Unity。这也就意味着,使用者需要自己加载图集。
依然以上面的两个Image为例,我们将它们的Source Image分别设置为heart和mail,并且取消勾选图集的Include in Build选项,在Editor模式下,图片虽然能正常显示,但运行项目后,会发现图片成了空白的。打包程序并且运行,查看它的Profile,发现图集并没有被加载进内存。这也印证了前面说的禁用Include in Build后Unity将不在运行时自动加载图集,引用的Sprite将不可见。
与此同时还出现了一条警告信息 ⚠️,它提示我们图集的atlasRequested事件没有被监听。
SpriteMananger是在运行期间管理SpriteAtlas的一个类,它包含atlasRequested和atlasRegisterer两个事件:
atlasRequested事件当一个Sprite打包进了图集但是在运行期间无法找到该图集的位置时触发,通常情况下是因为没有勾选Include in Build选项。用户可以通过Action委托在必要的时候进行响应,进行响应的时候需要使用SpriteAtlas参数,下面是这个事件的构造
//string:精灵体打包图集的名称 //Action委托会自动加载图集 public static event Action> atlasRequested;
using UnityEngine.U2D; SpriteAtlasManager.atlasRequested += OnAtlasRequest;
当gameobject需要的图集不存在的时候,unity有提供回调。这个回调不会频繁回调,缺一个图集只会调用一次。就是说虽然有很多物体会报缺失但是如果是缺失的图片属于同一个图集,那么这个缺失汇总成一个回调。
代码如下(这里加载sprite atlas资源采用的是AssetBundle,其他方式也行只要加载到图集资源即可),挂载在场景中即可:
using UnityEngine;
using UnityEngine.U2D;
using System;
public class TestAtlasRequest : MonoBehaviour
{
GameObject panel;
AssetBundle bundle;
void OnEnable()
{
SpriteAtlasManager.atlasRequested += OnAtlasRequested;
SpriteAtlasManager.atlasRegistered += OnAtlasRegistered;
}
private void OnAtlasRequested(string atlasName, Action action)
{
Debug.Log("OnAtlasRequested = " + atlasName);
action(bundle.LoadAsset(atlasName));
// StartCoroutine(LoadAsset(action, tag));
}
private void OnAtlasRegistered(SpriteAtlas atlas)
{
Debug.Log("OnAtlasRegistered = " + atlas.name);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.A))
{
//AB包保存路径及名称
bundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + string.Format(@"firstatlas.atlas"));
//从AB包中加载图集
panel = bundle.LoadAsset("Panel");
Instantiate(panel, this.transform);
}
}
// private SpriteAtlas sa;
// private bool loadingsa;
// private IEnumerator LoadAsset(Action action, string tag)
// {
// yield return new WaitForSeconds(3);
// if (!loadingsa && sa == null)
// {
// loadingsa = true;
// AssetBundleCreateRequest acr = AssetBundle.LoadFromFileAsync(GetPath(tag));
// yield return acr;
// sa = acr.assetBundle.LoadAsset(tag);
// loadingsa = false;
// }
// action(sa);
// }
// private string GetPath(string tag)
// {
// string path = Application.dataPath + "/Texture/Atlas/test.unity3d";
// return path;
// }
void OnDisable()
{
SpriteAtlasManager.atlasRequested -= OnAtlasRequested;
SpriteAtlasManager.atlasRegistered -= OnAtlasRegistered;
}
}
atlasRegisterer事件
当atlasRequested中的Action委托调用后执行。
public static event ActionatlasRegistered;
后期绑定更多时候是和AssetBundle一起使用的,比如我们将UI元素Image放在一个AB包中,Image中引用的图集放在另一个AB包中,我们加载Image的时候并不需要知道两个AB包之间的依赖,这时可以通过后期绑定来解决这个问题。
需要注意的问题图集虽然能够有效降低DrawCall,但使用不当容易造成内存资源的浪费。当图集中有一个Sprite处于活跃状态时,Unity会加载该Sprite所属图集中的所有纹理。如果一个图集中的有许多纹理,就算场景中只有一个纹理被引用,那么整个图集都会被加载,会造成较大的内存消耗。
假设场景中有一张Image,Image的Sprite被打包到一个图集中,这个图集中还包含其他资源。
程序运行的时候可以看到整张图集都被加载进了内存,虽然只引用了其中一张贴图。
为了解决这个问题,可以将场景中激活的所有或大多数纹理都打包到同一个图集,最好根据纹理的常见用途将分别打包成多个较小的图集,不过这样也可能造成DrawCall增加 。
当打包的图集空白空间过多时,可以手动减少打包图集的大小,从而减少占用的内存。通过调整图集Max Texture Size值,然后选择Pack Preview重新生成图集。
当 Max Texture Size 值小于精灵图集纹理的当前尺寸时,Unity 会减小打包纹理尺寸以尽可能与设置的 Max Texture Size 匹配,并自动修剪掉任何多余的空白空间。如果选择的一些精灵纹理超过精灵图集的 Max Texture Size 设置,则精灵图集将忽略 Max Texture Size 设置,并保持能够包含精灵纹理原始尺寸而需要的最小尺寸,此时编辑器也会报错。
但打包的图集仍然是可用的,只是大小与设置的不一致。
总结 重点通过GameObject.Instantiate去实例化某个包含了Sprite的Prefab并不会触发SpriteAtlasManager.atlasRequested,触发只跟Prefab的加载有关
using UnityEngine;
using UnityEngine.U2D;
using System;
public class TestAtlasRequest : MonoBehaviour
{
GameObject panel;
// AssetBundle bundle;
void OnEnable()
{
SpriteAtlasManager.atlasRequested += OnAtlasRequested;
SpriteAtlasManager.atlasRegistered += OnAtlasRegistered;
}
private void OnAtlasRequested(string atlasName, Action action)
{
Debug.Log("OnAtlasRequested = " + atlasName);
AssetBundle bundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + string.Format(@"{0}.atlas", atlasName.ToLower()));
action(bundle.LoadAsset(atlasName));
// StartCoroutine(LoadAsset(action, tag));
}
private void OnAtlasRegistered(SpriteAtlas atlas)
{
Debug.Log("OnAtlasRegistered = " + atlas.name);
}
private void Update()
{
if (Input.GetKeyDown(KeyCode.A))
{
//AB包保存路径及名称
AssetBundle bundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + string.Format(@"panel.prefab"));
//从AB包中加载图集
panel = bundle.LoadAsset("Panel");
// Instantiate(panel, this.transform);
}
if (Input.GetKeyDown(KeyCode.B))
{
Instantiate(panel, this.transform);
}
}
// private SpriteAtlas sa;
// private bool loadingsa;
// private IEnumerator LoadAsset(Action action, string tag)
// {
// yield return new WaitForSeconds(3);
// if (!loadingsa && sa == null)
// {
// loadingsa = true;
// AssetBundleCreateRequest acr = AssetBundle.LoadFromFileAsync(GetPath(tag));
// yield return acr;
// sa = acr.assetBundle.LoadAsset(tag);
// loadingsa = false;
// }
// action(sa);
// }
// private string GetPath(string tag)
// {
// string path = Application.dataPath + "/Texture/Atlas/test.unity3d";
// return path;
// }
void OnDisable()
{
SpriteAtlasManager.atlasRequested -= OnAtlasRequested;
SpriteAtlasManager.atlasRegistered -= OnAtlasRegistered;
}
}
参考
- [UGUI性能优化]精灵图集(Sprite Atlas)保姆式教程
- SpriteAtlasManager.atlasRequested在什么时机被调用
- Unity Sprite Atlas



