Unity3D UGUI优化: 运行时打图集并使用(Using UnityRuntimeSpriteSheetsGenerator)

前言:

之前看到一个 小米超神写的关于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就是打动态图集的脚本了。

扫描二维码关注公众号,回复: 3166157 查看本文章

这个打包 脚本的工作流程,先是用代码赋值需要打包的图片(比如以.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>();
        //设置回调;
        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;

    /// <summary>
    /// 动态图集的目标图片;
    /// </summary>
    [RequireComponent(typeof(Image))]
    public class AssetPackerTargetImage : MonoBehaviour
    {
        /// <summary>
        /// 目标指引的图片;
        /// </summary>
        internal Image TargetImage;

        /// <summary>
        /// 在动态图集中的机灵图片;
        /// </summary>
        [HideInInspector]
        public Sprite AssetSprite;

        /// <summary>
        /// 图集标签名字;
        /// </summary>
        public string PakerTagName="Default";

        /// <summary>
        /// 精灵图片名字;
        /// </summary>
        public string SpriteName;

        /// <summary>
        /// 精灵图片路径;
        /// </summary>
        public string SpritePath;

        private void Start()
        {
            Init();
        }

        bool IsInited = false;

        /// <summary>
        /// 初始化
        /// </summary>
        public void Init()
        {
            if (IsInited) return;
            IsInited = true;
            //属性获取;
            TargetImage = GetComponent<Image>();
            SpriteName = TargetImage.sprite != null ? TargetImage.sprite.name : "White";
        }

        /// <summary>
        /// 设置成一张新的图片;
        /// </summary>
        /// <param name="aSp"></param>
        public void SetNewSprite(Sprite aSp)
        {
            Init();
            AssetSprite = aSp;
            TargetImage.sprite = aSp;
            SpriteName = aSp != null ? aSp.name : "";
        }


#if UNITY_EDITOR

        /// <summary>
        /// 在编辑器下的路径位置;
        /// </summary>
        [ContextMenuItem("初始化", "InitInEditorMode")]
        public string InEditorPath;

        /// <summary>
        /// 在编辑器模式下获取文件路径;
        /// </summary>
        [ExecuteInEditMode]
        public void InitInEditorMode()
        {
            //获取路径;
            Sprite mSp = GetComponent<Image>().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;


    /// <summary>
    /// 动态图集的辅助工具;
    /// </summary>
    public class AssetPackerInEditorHelper : MonoBehaviour
    {
#if UNITY_EDITOR

        /// <summary>
        /// 默认图片;
        /// </summary>
        [ContextMenuItem("自动设置", "AutoSetAllTargetImageAndInit")]
        public Sprite DefaultSprite;

        /// <summary>
        /// 图集标签名字;
        /// </summary>
        [ContextMenuItem("设置子对象名字", "SetAllName")]
        public string PakerTagName = "Default";

        /// <summary>
        /// 所有的指向图片;
        /// </summary>
        [ContextMenuItem("初始化所有", "InitAllTargetImage")]
        public AssetPackerTargetImage[] ArrAllTargetImage;

        /// <summary>
        /// 初始化所有的目标图片;
        /// </summary>
        [ExecuteInEditMode]
        void InitAllTargetImage()
        {
            //搜索中包含未激活的物品;
            ArrAllTargetImage = transform.GetComponentsInChildren<AssetPackerTargetImage>(true);
            for (int i = 0; i < ArrAllTargetImage.Length; i++)
            {
                //初始化所有;
                ArrAllTargetImage[i].InitInEditorMode();
            }
        }

        /// <summary>
        /// 自动设置所有的目标图片,并且初始化;
        /// </summary>
        [ExecuteInEditMode]
        void AutoSetAllTargetImageAndInit()
        {
            //获取所有图片;
            Image[] mArrImage = transform.GetComponentsInChildren<Image>(true);
            for (int i = 0; i < mArrImage.Length; i++)
            {
                //判定图片,如果是空,则设置为默认图片;
                if (mArrImage[i].sprite == null) mArrImage[i].sprite = DefaultSprite;
                //之后进行组件处理;
                if (mArrImage[i].GetComponent<AssetPackerTargetImage>() != null) continue;
                mArrImage[i].gameObject.AddComponent<AssetPackerTargetImage>();
            }
            //搜索中包含未激活的物品;
            ArrAllTargetImage = transform.GetComponentsInChildren<AssetPackerTargetImage>(true);
            for (int i = 0; i < ArrAllTargetImage.Length; i++)
            {
                //初始化所有;
                ArrAllTargetImage[i].InitInEditorMode();
                ArrAllTargetImage[i].PakerTagName = PakerTagName;
            }
        }

        /// <summary>
        /// 一次性设置所目标图片的名字;
        /// </summary>
        [ExecuteInEditMode]
        void SetAllName()
        {
            for (int i = 0; i < ArrAllTargetImage.Length; i++)
            {
                //初始化所有;
                ArrAllTargetImage[i].PakerTagName = PakerTagName;
            }
        }
#endif
    }

好了,有了这两个工具, 就可以进行图集打包了。

我们在编辑器下,在需要管理的图片根节点挂载脚本:AssetPackerInEditorHelper,选定好 默认图片之,在默认图片上右击自动设置就可以进行目标图片的标记了:

 

可以看到还是有很多图片需要设置的。到这里我们的准备工作就完成了。

 

5、替换成单图;

此外,有的项目中(比如我现在的项目)的UI资源并不是以一张一张的散图的形式存在在,而是在一开始就用如TexturePaker这样的第三方 工具打包成了一个完成的图集。所以在真正开始之前,还需要将在图集中的图换回散图。

同样,我在AssetPackerInEditorHelper中写一段代码进行批量替换。由于是在编辑器下运行,所以代码可以随意一点,不用考虑一些性能问题了。

        #region 图集替换成单图;

        /// <summary>
        /// 目标单图的文件夹;
        /// </summary>
        [ContextMenuItem("替换成单图", "ReplaceSpriteInPackerBySinglePacke")]
        public string TargetSingleSpriteFloder = "Resources/";

        /// <summary>
        /// 进行替换:
        /// </summary>
        [ExecuteInEditMode]
        void ReplaceSpriteInPackerBySinglePacke()
        {
            for (int i = 0; i < ArrAllTargetImage.Length; i++)
            {
                //获取用来替换的Sprite;
                Image tempImg = ArrAllTargetImage[i].GetComponent<Image>();
                string name = tempImg.sprite.name;
                string Path = TargetSingleSpriteFloder + name;
                Sprite SingleSP = Resources.Load<Sprite>(Path);
                //替换;
                if (SingleSP == null)
                {
                    Debug.LogError("找不到单图:" + Path );
                    Debug.LogError("在:" + ArrAllTargetImage[i].gameObject.name);
                    continue;
                }
                tempImg.sprite = SingleSP;
                ArrAllTargetImage[i].InitInEditorMode();
            }
        }

        #endregion

 

6、使用动态图集;

准备工作完成了,终于到了打包了。我在某个特定时间,比如开始战斗场景的时候触发打包操作,代码如下: 

        /// <summary>
        /// 所有的已经打包的图集;
        /// </summary>
        Dictionary<string, AssetPacker> mDicAllAssetPacker = new Dictionary<string, AssetPacker>();

        /// <summary>
        /// 所有的路径;
        /// </summary>
        Dictionary<string, List<string>> mDicAllPath = new Dictionary<string, List<string>>();
        
        /// <summary>
        /// 获取一个路径列表;
        /// </summary>
        /// <param name="tag"></param>
        /// <returns></returns>
        List<string> GetPathList(string tag)
        {
            if (mDicAllPath.ContainsKey(tag))
            {
                return mDicAllPath[tag];
            }
            List<string> mList = new List<string>();
            mDicAllPath.Add(tag, mList);
            return mList;
        }

        /// <summary>
        /// 存路径,同时去重;
        /// </summary>
        /// <param name="mList"></param>
        /// <param name="NewPath"></param>
        void SavePathToListWithoutRepeat(List<string> mList, string NewPath)
        {
            //跳过重复;
            for (int i = 0; i < mList.Count; i++)
            {
                if (mList[i] == NewPath) return;
            }
            mList.Add(NewPath);
        }

        /// <summary>
        /// 打包动态图集,只有Tag相同才会被打包;
        /// </summary>
        /// <param name="mList"></param>
        /// <param name="tag">筛选参数</param>
        public void CreateAssetPacker(List< AssetPackerTargetImage> mList, string tag)
        {
            if (mList.Count == 0) return;
            //获取所有的路径;
            List<AssetPackerTargetImage> mListTarget = new List<AssetPackerTargetImage>();
            List<string> 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<AssetPacker>();
            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);
        }

        /// <summary>
        /// 一个图集打包完成;
        /// </summary>
        /// <param name="tag"></param>
        void OneTagPackEnd(string tag)
        {
            mListAllTagNotEnd.Remove(tag);
            if (mListAllTagNotEnd.Count == 0)
            {
                //此时已经全部打包完成了;
                Debug.Log("全部图集打包完成!");
            }
        }

        /// <summary>
        /// 获取一张图片;
        /// </summary>
        /// <param name="tag"></param>
        /// <param name="name"></param>
        /// <returns></returns>
        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);
                }
            }

打印其加载出错的路径,方便进行错误排查;

猜你喜欢

转载自blog.csdn.net/cyf649669121/article/details/81534183