草庐IT

【Unity3D】关于unity中spriteAtlas的打包方案及使用时内存状态的多组对照实验

云峰寒树 2023-07-07 原文

精灵图集(Sprite Atlas)简介

【Unity Manual】

unity项目中使用精灵(Sprite)和其他图形来创建其场景的视觉效果。这意味着单个项目中可能包含许多纹理(texture) 文件。Unity 通常会为场景中的每个纹理发出一个绘制调用(DrawCall);但是,在具有许多纹理的项目中,过多的绘制调用会占用大量资源,并会对项目的性能产生负面影响。

为了降低性能消耗,我们可以使用精灵图集(Sprite Atlas)技术,它能够将多个纹理合并成一个大纹理,当访问图集中的多个纹理时,也只需要调用一次DrawCall。

精灵图集的使用

关于Sprite Atlas 的使用可以参考–这里

在查找相关资料时由知乎文章:【Unity游戏开发】SpriteAtlas与AssetBundle最佳食用方案 得知不同的打包方案,即:

  • 对照组A&B,将散图文件夹作为一个AssetBundle包。
  • 对照组C&D,将Sprite Atlas文件作为一个AssetBundle包。

对照组实验

以下所有结论均在Unity 2021.3.11下得出


文件结构

两个AssetBundle包。

其中testImage使用了一个图集中的图元

  • A组:
    勾选Include in build
    打包散图文件夹,不打包SpriteAtlas
    使用方式:

    	_bundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testatlas.u3d");
        img1.sprite = _bundle2.LoadAsset<Sprite>("gameS-num1");
        img2.sprite = _bundle2.LoadAsset<Sprite>("gameS-num2");
    

    需要先加载atlas包,再使用prefab,否则build后 prefab无法正常显示纹理(editor下正常)

    结论:
    1. 使用两张散图,DrawCall仍记为1;
    2. AB包中除了所有散图外,仍会有一张合并后的大图;
    下图由AssetStudio分别读取AB包中的内容
    testatlas.u3d:

    testimage.u3d:

    3. 改变图元调用的次数后

        _bundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testatlas.u3d");
        img1.sprite = _bundle2.LoadAsset<Sprite>("gameS-num1");
        img2.sprite = _bundle2.LoadAsset<Sprite>("gameS-num1");
        img3.sprite = _bundle2.LoadAsset<Sprite>("gameS-num1");
    

    MemoryProfiler检查内存可知,所有关于图元 "gameS-num1"的引用在内存中有且仅有一份,也就是说,以上方式加载的图元,以单例形式存在于内存之中,不同于C组结论3 . - B组: 不勾选Include in build
    打包散图文件夹,不打包SpriteAtlas
    使用方式:

    	_bundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testatlas.u3d");
        // img1.sprite = _bundle2.LoadAsset<Sprite>("gameS-num1");
        // img2.sprite = _bundle2.LoadAsset<Sprite>("gameS-num2");
    

    结论:
    无法加载图片,加载testatlas.u3d AB时调用 SpriteAtlasManager.atlasRequested ;
    AB包中只有散图
    加载prefab时不会调用 SpriteAtlasManager.atlasRequested (与D组差异)
    prefab无法正常显示;

  • C组:
    勾选Include in build
    不打包散图文件夹,打包SpriteAtlas
    使用方式:

    	_bundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testatlas.u3d");
        atlas = _bundle2.LoadAsset<SpriteAtlas>("testAtlas");
        img1.sprite = atlas.GetSprite("gameS-num1");
        img2.sprite = atlas.GetSprite("gameS-num2");
    

    结论:
    1. 使用SpriteAtlas,调用多个图元,DrawCall记为1
    2. 由AssetStudio分别读取AB包中的内容
    testatlas.u3d:

    testimage.u3d:

    由此可见,AB包中除了所有散图外,会有一张合并后的大图;但是,prefab中也出现一份相同的图元,打包冗余

    1. 改变图元调用的次数后
        _bundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testatlas.u3d");
        atlas = _bundle2.LoadAsset<SpriteAtlas>("testAtlas");
        img1.sprite = atlas.GetSprite("gameS-num1");
        img2.sprite = atlas.GetSprite("gameS-num1");
        img3.sprite = atlas.GetSprite("gameS-num1");
    

    MemoryProfiler检查内存可知,所有关于图元 “gameS-num1"的引用都会clone出独立的一份,相对比A组造成大量的内存开销。同时由于prefab中也有一张图元,在加载AB包时,多次载入同一份图元"gameS-num1”。

  • D组:
    不勾选Include in build
    不打包散图文件夹,打包SpriteAtlas
    使用方式:

    	_bundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testatlas.u3d");
        atlas = _bundle2.LoadAsset<SpriteAtlas>("testAtlas");
        img1.sprite = atlas.GetSprite("gameS-num1");
        img2.sprite = atlas.GetSprite("gameS-num2");
    

    结论:
    prefab中出现一份散图,打包冗余
    AB包中除了所有散图外,仍会有一张合并后的大图;
    加载prefab时调用 SpriteAtlasManager.atlasRequested ;
    prefab无法正常显示

  • E组
    勾选Include in build
    将散图文件夹,SpriteAtlas打入同一个AB包中

  • F组
    不勾选Include in build
    将散图文件夹,SpriteAtlas打入同一个AB包中

实验对照表

打包方式是否勾选Include in build纹理合并是否成功是否冗余1先加载图元是否需要后绑定2先加载prefab是否需要后绑定2内存中图元的唯一性atlas.GetSprite(“xxx”)内存中图元的唯一性_bundle2.LoadAsset(“gameS-num1”);
对文件夹打包不可用
对文件夹打包(无意义)不可用不可用
对spriteAtlas文件打包不可用
对spriteAtlas文件打包不可用
将文件夹和spriteAtlas打入同一个包
将文件夹和spriteAtlas打入同一个包

总结

综上所诉,在使用SpriteAtlas时,根据不同的情况使用不同的打包方案。需要特备注重的是否冗余,是否需要精灵在内存唯一。

  1. 正确的打包方式应该是将图集和图元文件打入同一个AB包,在包体大小不变的情况下,还能使用不同的加载图元方式。一举多得 。
  2. 当预制体使用某一个图集中的图元时,建议不勾选include in build ,此时可以根据prefab需要的纹理加载对应的图集包。
  3. 当图集仅作为动态加载某些图元时,建议勾选include in build

完整测试用代码

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.U2D;
using UnityEngine.UI;

public class AtlasTest : MonoBehaviour
{
    public Image img0;
    public Image img1;
    public Image img2;
    public Image img3;
    private SpriteAtlas atlas;
    private AssetBundle _bundle;
    private GameObject _testImage;
    private AssetBundle _bundle2;

    // Start is called before the first frame update
    void Start()
    {
        SpriteAtlasManager.atlasRequested += OnCallBack_AtlasRequested;
        SpriteAtlasManager.atlasRegistered += OnCallBack_AtlasRegistered;
    }

    private void OnCallBack_AtlasRegistered(SpriteAtlas obj)
    {
        Debug.Log("atlasRegistered: " + obj.name);
        //img1.sprite = atlas.GetSprite("gameS-num1");
        //img2.sprite = atlas.GetSprite("gameS-num2");
    }

    private void OnCallBack_AtlasRequested(string arg1, Action<SpriteAtlas> arg2)
    {
        Debug.Log("无法在运行时找到图集资源:"+ arg1);

        //_bundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testatlas.u3d");
        atlas = _bundle2.LoadAsset<SpriteAtlas>("testAtlas");
        arg2.Invoke(atlas);
    }

    public void OnClick_Start()
    {
        _bundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testimage.u3d");
        var testImage = _bundle.LoadAsset<GameObject>("testImage");
        _testImage=GameObject.Instantiate(testImage, img1.transform.parent);
    }
    public void OnClick_Start2()
    {
        _bundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testatlas.u3d");
        atlas = _bundle2.LoadAsset<SpriteAtlas>("testAtlas");
        img1.sprite = atlas.GetSprite("gameS-num1");
        img2.sprite = atlas.GetSprite("gameS-num1");
        img3.sprite = atlas.GetSprite("gameS-num1");
    }
    public void OnClick_Start3()
    {
        _bundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + @"\testatlas.u3d");
        img1.sprite = _bundle2.LoadAsset<Sprite>("gameS-num1");
        img2.sprite = _bundle2.LoadAsset<Sprite>("gameS-num1");
        img3.sprite = _bundle2.LoadAsset<Sprite>("gameS-num1");
    }
    public void OnClick_Unload()
    {
        //GameObject.Destroy(_testImage);
        //img1.sprite = null;
        //img2.sprite = null;

        _bundle?.Unload(true);
        _bundle2?.Unload(true);
    }


    // Update is called once per frame
    void Update()
    {
    }
}


打包用

using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public class ExportAssetBundles : MonoBehaviour
{
    [MenuItem("Example/Build Asset Bundles")]
    static void BuildABs()
    {
        // Put the bundles in a folder called "ABs" within the Assets folder.
        BuildPipeline.BuildAssetBundles(Application.streamingAssetsPath, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows);
    }

    // Start is called before the first frame update
    void Start()
    {
        
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

  1. 其他引用图集中图元的prefabAB包中是否有多的图元。 ↩︎

  2. 是否调用了SpriteAtlasManager.atlasRequested 和 atlasRegistered。 ↩︎ ↩︎

有关【Unity3D】关于unity中spriteAtlas的打包方案及使用时内存状态的多组对照实验的更多相关文章

  1. ruby - 在 Ruby 程序执行时阻止 Windows 7 PC 进入休眠状态 - 2

    我需要在客户计算机上运行Ruby应用程序。通常需要几天才能完成(复制大备份文件)。问题是如果启用sleep,它会中断应用程序。否则,计算机将持续运行数周,直到我下次访问为止。有什么方法可以防止执行期间休眠并让Windows在执行后休眠吗?欢迎任何疯狂的想法;-) 最佳答案 Here建议使用SetThreadExecutionStateWinAPI函数,使应用程序能够通知系统它正在使用中,从而防止系统在应用程序运行时进入休眠状态或关闭显示。像这样的东西:require'Win32API'ES_AWAYMODE_REQUIRED=0x0

  2. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  3. ruby-on-rails - 跳过状态机方法的所有验证 - 2

    当我的预订模型通过rake任务在状态机上转换时,我试图找出如何跳过对ActiveRecord对象的特定实例的验证。我想在reservation.close时跳过所有验证!叫做。希望调用reservation.close!(:validate=>false)之类的东西。仅供引用,我们正在使用https://github.com/pluginaweek/state_machine用于状态机。这是我的预订模型的示例。classReservation["requested","negotiating","approved"])}state_machine:initial=>'requested

  4. ruby - 字符串文字中的转义状态作为 `String#tr` 的参数 - 2

    对于作为String#tr参数的单引号字符串文字中反斜杠的转义状态,我觉得有些神秘。你能解释一下下面三个例子之间的对比吗?我特别不明白第二个。为了避免复杂化,我在这里使用了'd',在双引号中转义时不会改变含义("\d"="d")。'\\'.tr('\\','x')#=>"x"'\\'.tr('\\d','x')#=>"\\"'\\'.tr('\\\d','x')#=>"x" 最佳答案 在tr中转义tr的第一个参数非常类似于正则表达式中的括号字符分组。您可以在表达式的开头使用^来否定匹配(替换任何不匹配的内容)并使用例如a-f来匹配一

  5. ruby - Net::HTTP 获取源代码和状态 - 2

    我目前正在使用以下方法获取页面的源代码:Net::HTTP.get(URI.parse(page.url))我还想获取HTTP状态,而无需发出第二个请求。有没有办法用另一种方法做到这一点?我一直在查看文档,但似乎找不到我要找的东西。 最佳答案 在我看来,除非您需要一些真正的低级访问或控制,否则最好使用Ruby的内置Open::URI模块:require'open-uri'io=open('http://www.example.org/')#=>#body=io.read[0,50]#=>"["200","OK"]io.base_ur

  6. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  7. Unity 热更新技术 | (三) Lua语言基本介绍及下载安装 - 2

    ?博客主页:https://xiaoy.blog.csdn.net?本文由呆呆敲代码的小Y原创,首发于CSDN??学习专栏推荐:Unity系统学习专栏?游戏制作专栏推荐:游戏制作?Unity实战100例专栏推荐:Unity实战100例教程?欢迎点赞?收藏⭐留言?如有错误敬请指正!?未来很长,值得我们全力奔赴更美好的生活✨------------------❤️分割线❤️-------------------------

  8. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  9. unity---接入Admob - 2

    目录1.AdmobSDK下载地址2.将下载好的unityPackagesdk导入到unity里​编辑 3.解析依赖到项目中

  10. Unity 3D 制作开关门动画,旋转门制作,推拉门制作,门把手动画制作 - 2

    Unity自动旋转动画1.开门需要门把手先动,门再动2.关门需要门先动,门把手再动3.中途播放过程中不可以再次进行操作觉得太复杂?查看我的文章开关门简易进阶版效果:如果这个门可以直接打开的话,就不需要放置"门把手"如果门把手还有钥匙需要旋转,那就可以把钥匙放在门把手的"门把手",理论上是可以无限套娃的可调整参数有:角度,反向,轴向,速度运行时点击Test进行测试自己写的代码比较垃圾,命名与结构比较拉,高手轻点喷,新手有类似的需求可以拿去做参考上代码usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;u

随机推荐