之前看到一个 小米超神写的关于Moba UI的优化(地址:https://zhuanlan.zhihu.com/p/38004837),由于自己项目也是Moba,觉得很实用, 所以决定偷过来试试。首先 肯定是要打动态图集,把各种散图合在一起肯定是减少DrawCall的重要途径。下面就开始慢慢学习吧。
涉及到一个开源项目:https://github.com/DaVikingCode/UnityRuntimeSpriteSheetsGenerator;
1、下载项目
看起来需要阅读的部分不很多。
在他的Demo里面有个RectanglePacking的场景,打开来就是所需要的动态图集的打包算法;而AssetPacker的场景则是实现的效果。在 运行之后可以看到, 程序自动把多张图片进行了合并并用于UGUI,正是我们 想要的效果:
2、API使用说明
在AssetPacker的场景里面,在Demo的GameObject上挂载的一个 脚本AssetPacker就是打动态图集的脚本了。
这个打包 脚本的工作流程,先是用代码赋值需要打包的图片(比如以.png结尾的文件),由代码进行打包。可以在 属性面板上赋值打包完成的回调函数,也可以在代码里面写。
UseChahe:勾选之后会在项目的persistentDataPath下生成图集的缓存,包含一张大图集和对应的序列化文件(存储各个图片的UV,以Json格式存储),在下次使用的时候,如果ChcheName 和 CacheVersion一样的,则会使用老图,不会新生成。不勾选则不会生成,可以理解为释放掉之后就没有了。
CacheName:缓存的名字;
ChcheVersion:缓存的版本;
DeletePreviousCacheVersion: 是否删除旧版本的缓存。
其示例的动态图集的打包方法如下:
//将指定文件夹的图片复制到项目文件夹下面;
CopyPasteFoldersAndPNG(Application.dataPath + "/RuntimeSpriteSheetsGenerator/Demos/Sprites", Application.persistentDataPath);
//获取所有需要打图集的文件路径;
string[] files = Directory.GetFiles(Application.persistentDataPath + "/Textures", "*.png");
//动态图集工具
assetPacker = GetComponent();
//设置回调;
assetPacker.OnProcessCompleted.AddListener(LaunchAnimations);
//设置需要打包的图片;
assetPacker.AddTexturesToPack(files);
//开始打图集;
assetPacker.Process();
在其图集打完之后,会自动调用回调函数;关于打完图集之后,精灵图片的获取有两种方式:
//获取以 "walking" 开始的图片数组;
Sprite[] sprites = assetPacker.GetSprites("walking");
//获取名字为 "waling0001" 的单张图片;
Sprite sprite = assetPacker.GetSprite("waling0001");
当动态图集使用完毕之后,就可以将图集清空,直接将AssetPacker挂载的GameObject删除,他会自己调用其Dispose接口进行释放。当然, 我们也可以自己写个方法进行动态图集的释放。
3、插件移植
将以下6个脚本复制到项目中的一个新文件夹就可以使用了;
4、准备工作;
在使用动态图集之前,先要有一些准备工作。因为我现在的项目已经有UI界面了,引用了 各种各样的图片,现在需要把这些图片都标记出来,在运行时进行打包。这个自动打包的脚本需要的是文件路径,所以需要获得所有需要的文件路径。当然一个一个写太费劲了,所以我们需要用脚本来处理。
首先写 一个脚本挂载在所有的Image上,标记他们,设置为目标图片。他们身上挂载的Sprite会在程序设定 的时间运行后达成一张图,之后 再返回赋值到这些对应的Image上,从而达到减少DrawCall的目的。
using UnityEngine;
using UnityEngine.UI;
///
/// 动态图集的目标图片;
///
[RequireComponent(typeof(Image))]
public class AssetPackerTargetImage : MonoBehaviour
{
///
/// 目标指引的图片;
///
internal Image TargetImage;
///
/// 在动态图集中的机灵图片;
///
[HideInInspector]
public Sprite AssetSprite;
///
/// 图集标签名字;
///
public string PakerTagName="Default";
///
/// 精灵图片名字;
///
public string SpriteName;
///
/// 精灵图片路径;
///
public string SpritePath;
private void Start()
{
Init();
}
bool IsInited = false;
///
/// 初始化
///
public void Init()
{
if (IsInited) return;
IsInited = true;
//属性获取;
TargetImage = GetComponent();
SpriteName = TargetImage.sprite != null ? TargetImage.sprite.name : "White";
}
///
/// 设置成一张新的图片;
///
///
public void SetNewSprite(Sprite aSp)
{
Init();
AssetSprite = aSp;
TargetImage.sprite = aSp;
SpriteName = aSp != null ? aSp.name : "";
}
#if UNITY_EDITOR
///
/// 在编辑器下的路径位置;
///
[ContextMenuItem("初始化", "InitInEditorMode")]
public string InEditorPath;
///
/// 在编辑器模式下获取文件路径;
///
[ExecuteInEditMode]
public void InitInEditorMode()
{
//获取路径;
Sprite mSp = GetComponent().sprite;
InEditorPath = UnityEditor.AssetDatabase.GetAssetPath(mSp).Substring(6);
SpritePath = InEditorPath;
InEditorPath = Application.dataPath + InEditorPath;
Debug.Log("获取到路径:" + InEditorPath);
SpriteName = mSp.name;
}
#endif
}
这个脚本可以针对某个Image单独编辑,但是这样还是太麻烦了。所以需要一个工具来对所有的这些目标图片进行统一编辑。值得注意的是,有的作为背景使用的Image其本身没有图片,其Sprite设置为null,但是这仍然让Unity认为是使用了一个新的图集,从而增加DrawCall,所以需要在设置的时候将所有设置为Null的Image的Sprite设置为一张白图。这种大家自己随便用 画图软件框几个像素的白色图片就可以了。
using UnityEngine;
using UnityEngine.UI;
///
/// 动态图集的辅助工具;
///
public class AssetPackerInEditorHelper : MonoBehaviour
{
#if UNITY_EDITOR
///
/// 默认图片;
///
[ContextMenuItem("自动设置", "AutoSetAllTargetImageAndInit")]
public Sprite DefaultSprite;
///
/// 图集标签名字;
///
[ContextMenuItem("设置子对象名字", "SetAllName")]
public string PakerTagName = "Default";
///
/// 所有的指向图片;
///
[ContextMenuItem("初始化所有", "InitAllTargetImage")]
public AssetPackerTargetImage[] ArrAllTargetImage;
///
/// 初始化所有的目标图片;
///
[ExecuteInEditMode]
void InitAllTargetImage()
{
//搜索中包含未激活的物品;
ArrAllTargetImage = transform.GetComponentsInChildren(true);
for (int i = 0; i < ArrAllTargetImage.Length; i++)
{
//初始化所有;
ArrAllTargetImage[i].InitInEditorMode();
}
}
///
/// 自动设置所有的目标图片,并且初始化;
///
[ExecuteInEditMode]
void AutoSetAllTargetImageAndInit()
{
//获取所有图片;
Image[] mArrImage = transform.GetComponentsInChildren(true);
for (int i = 0; i < mArrImage.Length; i++)
{
//判定图片,如果是空,则设置为默认图片;
if (mArrImage[i].sprite == null) mArrImage[i].sprite = DefaultSprite;
//之后进行组件处理;
if (mArrImage[i].GetComponent() != null) continue;
mArrImage[i].gameObject.AddComponent();
}
//搜索中包含未激活的物品;
ArrAllTargetImage = transform.GetComponentsInChildren(true);
for (int i = 0; i < ArrAllTargetImage.Length; i++)
{
//初始化所有;
ArrAllTargetImage[i].InitInEditorMode();
ArrAllTargetImage[i].PakerTagName = PakerTagName;
}
}
///
/// 一次性设置所目标图片的名字;
///
[ExecuteInEditMode]
void SetAllName()
{
for (int i = 0; i < ArrAllTargetImage.Length; i++)
{
//初始化所有;
ArrAllTargetImage[i].PakerTagName = PakerTagName;
}
}
#endif
}
好了,有了这两个工具, 就可以进行图集打包了。
我们在编辑器下,在需要管理的图片根节点挂载脚本:AssetPackerInEditorHelper,选定好 默认图片之,在默认图片上右击自动设置就可以进行目标图片的标记了:
可以看到还是有很多图片需要设置的。到这里我们的准备工作就完成了。
5、替换成单图;
此外,有的项目中(比如我现在的项目)的UI资源并不是以一张一张的散图的形式存在在,而是在一开始就用如TexturePaker这样的第三方 工具打包成了一个完成的图集。所以在真正开始之前,还需要将在图集中的图换回散图。
同样,我在AssetPackerInEditorHelper中写一段代码进行批量替换。由于是在编辑器下运行,所以代码可以随意一点,不用考虑一些性能问题了。
#region 图集替换成单图;
///
/// 目标单图的文件夹;
///
[ContextMenuItem("替换成单图", "ReplaceSpriteInPackerBySinglePacke")]
public string TargetSingleSpriteFloder = "Resources/";
///
/// 进行替换:
///
[ExecuteInEditMode]
void ReplaceSpriteInPackerBySinglePacke()
{
for (int i = 0; i < ArrAllTargetImage.Length; i++)
{
//获取用来替换的Sprite;
Image tempImg = ArrAllTargetImage[i].GetComponent();
string name = tempImg.sprite.name;
string Path = TargetSingleSpriteFloder + name;
Sprite SingleSP = Resources.Load(Path);
//替换;
if (SingleSP == null)
{
Debug.LogError("找不到单图:" + Path );
Debug.LogError("在:" + ArrAllTargetImage[i].gameObject.name);
continue;
}
tempImg.sprite = SingleSP;
ArrAllTargetImage[i].InitInEditorMode();
}
}
#endregion
6、使用动态图集;
准备工作完成了,终于到了打包了。我在某个特定时间,比如开始战斗场景的时候触发打包操作,代码如下:
///
/// 所有的已经打包的图集;
///
Dictionary mDicAllAssetPacker = new Dictionary();
///
/// 所有的路径;
///
Dictionary> mDicAllPath = new Dictionary>();
///
/// 获取一个路径列表;
///
///
///
List GetPathList(string tag)
{
if (mDicAllPath.ContainsKey(tag))
{
return mDicAllPath[tag];
}
List mList = new List();
mDicAllPath.Add(tag, mList);
return mList;
}
///
/// 存路径,同时去重;
///
///
///
void SavePathToListWithoutRepeat(List mList, string NewPath)
{
//跳过重复;
for (int i = 0; i < mList.Count; i++)
{
if (mList[i] == NewPath) return;
}
mList.Add(NewPath);
}
///
/// 打包动态图集,只有Tag相同才会被打包;
///
///
/// 筛选参数
public void CreateAssetPacker(List< AssetPackerTargetImage> mList, string tag)
{
if (mList.Count == 0) return;
//获取所有的路径;
List mListTarget = new List();
List mListPath = GetPathList(tag);
for (int i = 0; i < mList.Count; i++)
{
if (!string.IsNullOrEmpty(mList[i].SpritePath) && mList[i].PakerTagName == tag)
{
//替换为绝对路径;
SavePathToListWithoutRepeat(mListPath, Application.dataPath + mList[i].SpritePath);
mListTarget.Add(mList[i]);
}
}
if (mListPath.Count == 0) return;
//之后开始打包图集;
AssetPacker ap = new GameObject("AssetPacker:" + tag).AddComponent();
ap.useCache = false;
ap.cacheName = tag;
ap.cacheVersion = 1;
//设定参数;
mDicAllAssetPacker.Add(tag, ap);
ap.OnProcessCompleted.AddListener(delegate ()
{
//设置图片;
for (int i = 0; i < mListTarget.Count; i++)
{
mListTarget[i].SetNewSprite(ap.GetSprite(mListTarget[i].SpriteName));
}
#if UNITY_EDITOR
Debug.Log("动态图集"+ tag + "更换完成。" + "数量:" + mListTarget.Count);
#endif
OneTagPackEnd(tag);
});
ap.AddTexturesToPack(mListPath);
//开始打图集;
ap.Process();
Debug.Log("开始打图集:" + tag + "数量:" + mListPath.Count);
}
///
/// 一个图集打包完成;
///
///
void OneTagPackEnd(string tag)
{
mListAllTagNotEnd.Remove(tag);
if (mListAllTagNotEnd.Count == 0)
{
//此时已经全部打包完成了;
Debug.Log("全部图集打包完成!");
}
}
///
/// 获取一张图片;
///
///
///
///
public Sprite GetSprite(string tag,string name)
{
if (mDicAllAssetPacker.ContainsKey(tag))
{
var Pa = mDicAllAssetPacker[tag];
return Pa.GetSprite(name);
}
return null;
}
这样就能让打包 完成的时候自动替换,接下来只要在项目需要的地方一步一步替换,那么项目就会优化很多。当然,这样优化的代价是以牺牲加载速度为前提的。不过考虑到现在的项目以效率、性能优先,所以加载耗时稍微长一点也是可以接受的。反正优化是永无止境的工作,这仅仅是一个开始。
除了正文的工作之外,发现原来DaVikingCode.AssetPacker的代码也还有几处需要修改。其中一处是在AssetPacker的OnProcessCompleted属性,需要修改如下:
public UnityEvent OnProcessCompleted = new UnityEvent();
以前是赋值为空,会导致用代码创建的时候出现空指针;
另外一处在createPack方法,将其中的 foreach (TextureToPack itemToRaster in itemsToRaster)循环语句修改如下:
//改成For循环减少GC;
for (int i = 0; i < itemsToRaster.Count; i++)
{
var itemToRaster = itemsToRaster[i];
WWW loader = new WWW("file:///" + itemToRaster.file);
yield return loader;
//打印路径以方便错误排查;
if (string.IsNullOrEmpty(loader.error))
{
textures.Add(loader.texture);
images.Add(itemToRaster.id);
}
else
{
Debug.LogWarning("路径有误:" + loader.url);
}
}
打印其加载出错的路径,方便进行错误排查;