5

Unity内置资源如何打包避免冗余

 2 years ago
source link: https://blog.uwa4d.com/archives/TechSharing_249.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

这是第249篇UWA技术知识分享的推送。今天我们继续为大家精选了若干和开发、优化相关的问题,建议阅读时间10分钟,认真读完必有收获。

UWA 问答社区:answer.uwa4d.com
UWA QQ群2:793972859(原群已满员)

本期目录:

  • Unity内置资源如何打包避免冗余
  • SpriteAtlas的“冗余”问题
  • 关于Mesh占用内存的问题
  • UGUI.Rendering.UpdateBatches耗时较高
  • Plugins的DLL是如何影响Package的

AssetBundle

Q:现在打包AssetBundle,Unity中内置资源能指定AssetBundle名称吗?内置资源在AssetBundle中的冗余一般是怎么解决的?

查了现有资料,针对内置资源打包AssetBundle冗余的处理都是将内置资源提取或者下载到本地,然后修改资源的引用关系,这样打包就可以指定内置资源的AssetBundle名称。想了解下Unity现在支持脚本打包AssetBunle时指定内置资源的Bundle名称吗?从而防止多个资源依赖同一个内置资源,导致冗余吗?

A:可以使用Scriptable Build Pipeline来实现题主想要的功能,具体的方法可以参考Addressable中打包内置Shader的思路。

public static IList<IBuildTask> AssetBundleBuiltInResourcesExtraction()
    {
        var buildTasks = new List<IBuildTask>();

        // Setup
        buildTasks.Add(new SwitchToBuildPlatform());
        buildTasks.Add(new RebuildSpriteAtlasCache());

        // Player Scripts
        buildTasks.Add(new BuildPlayerScripts());
        buildTasks.Add(new PostScriptsCallback());

        // Dependency
        buildTasks.Add(new CalculateSceneDependencyData());
#if UNITY_2019_3_OR_NEWER
        buildTasks.Add(new CalculateCustomDependencyData());
#endif
        buildTasks.Add(new CalculateAssetDependencyData());
        buildTasks.Add(new StripUnusedSpriteSources());
        buildTasks.Add(new CreateBuiltInResourcesBundle("UnityBuiltInResources"));   //将CreateBuiltInShadersBundle改成自己创建的类
        buildTasks.Add(new PostDependencyCallback());

        // Packing
        buildTasks.Add(new GenerateBundlePacking());
        buildTasks.Add(new UpdateBundleObjectLayout());
        buildTasks.Add(new GenerateBundleCommands());
        buildTasks.Add(new GenerateSubAssetPathMaps());
        buildTasks.Add(new GenerateBundleMaps());
        buildTasks.Add(new PostPackingCallback());

        // Writing
        buildTasks.Add(new WriteSerializedFiles());
        buildTasks.Add(new ArchiveAndCompressBundles());
        buildTasks.Add(new AppendBundleHash());
        buildTasks.Add(new PostWritingCallback());

        // Generate manifest files
        // TODO: IMPL manifest generation

        return buildTasks;
    }
[MenuItem("AssetBundles/GenerateAB")]
    public static void GenerateAB()
    {
        var outputPath = "Assets/AssetBundles";
        if (!Directory.Exists(outputPath))
            Directory.CreateDirectory(outputPath);

        BuildTarget targetPlatform = BuildTarget.StandaloneWindows;
        var group = BuildPipeline.GetBuildTargetGroup(targetPlatform);

        var parameters = new BundleBuildParameters(targetPlatform, group, outputPath);

        var buildInput = ContentBuildInterface.GenerateAssetBundleBuilds();
        IBundleBuildContent content = new BundleBuildContent(buildInput);

        var taskList = AssetBundleBuiltInResourcesExtraction();   //创建自己的task
        ReturnCode exitCode = ContentPipeline.BuildAssetBundles(parameters, content, out result, taskList);

        if (exitCode < ReturnCode.Success)
            return;

        var manifest = ScriptableObject.CreateInstance<CompatibilityAssetBundleManifest>();
        manifest.SetResults(result.BundleInfos);
        File.WriteAllText(parameters.GetOutputFilePathForIdentifier(Path.GetFileName(outputPath) + ".manifest"), manifest.ToString());
    }

下面的代码是CreateBuiltInResourcesBundle.cs里面的,从CreateBuiltInShadersBundle.cs里面复制一个类,并修改一点点代码:

        public ReturnCode Run()
        {
            HashSet<ObjectIdentifier> buildInObjects = new HashSet<ObjectIdentifier>();
            foreach (AssetLoadInfo dependencyInfo in m_DependencyData.AssetInfo.Values)
                buildInObjects.UnionWith(dependencyInfo.referencedObjects.Where(x => x.guid == k_BuiltInGuid));

            foreach (SceneDependencyInfo dependencyInfo in m_DependencyData.SceneInfo.Values)
                buildInObjects.UnionWith(dependencyInfo.referencedObjects.Where(x => x.guid == k_BuiltInGuid));

            ObjectIdentifier[] usedSet = buildInObjects.ToArray();
            Type[] usedTypes = ContentBuildInterface.GetTypeForObjects(usedSet);

            if (m_Layout == null)
                m_Layout = new BundleExplictObjectLayout();

            //Type shader = typeof(Shader);
            //for (int i = 0; i < usedTypes.Length; i++)
            //{
            //    if (usedTypes[i] != shader)
            //        continue;

            //    m_Layout.ExplicitObjectLocation.Add(usedSet[i], ShaderBundleName);
            //}

            //上面是打包内置Shader的操作,改成全部资源就可以了
            foreach (ObjectIdentifier identifier in usedSet)
            {
                m_Layout.ExplicitObjectLocation.Add(identifier, ShaderBundleName);
            }

            if (m_Layout.ExplicitObjectLocation.Count == 0)
                m_Layout = null;

            return ReturnCode.Success;
        }

测试如下:
将Unity默认的Effect做成prefab并打包成AssetBundle(包名为ps),可以看到特效用到的内置资源都在这个AssetBundle中:

1.png

使用SBP打包后,如下:
可以看到ps的AssetBundle中已经没有资源,生成的内置的AssetBundle中有ps用到的3个内置的资源,并且ps依赖UnityBuiltInResources这个AssetBundle:

2.png
3.png

感谢Xuan@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/604b26cacfa35d5b53669a6c


AssetBundle

Q:我之前对SpriteAtlas的理解是,UI在渲染的时候会根据关联的Sprite信息找到对应的SpriteAtlas,这样每个UI渲染都用了同样的Atlas,这样可以减少Draw Call。所以理论来看,应该只需要Atlas这一张图就好了。可为什么我看到打Atlas的AssetBundle包里除了Atlas图还有引用的原图。在UI的AssetBundle包里,也有引用的原图。

请问为什么有这样的“冗余”,以及到底Image引用的是图集还是图片,原理是什么?

如果Atlas包里放进原图则Atlas包里有原图但是UI包里没有,如果原图放到AssetBundle Package外面,则Atlas包和UI包里都有原图,这是什么原理?

A1:建议你先看一下这篇文章:
【Unity游戏开发】SpriteAtlas与AssetBundle最佳食用方案

4.png

在合理使用SpriteAtlas的情况下,当我们把AssetBundle包解开以后,会发现里面会包含一张Texture和若干个Sprite这两种资产。Texture是纹理,显示的文件大小较大;而Sprite可以理解为一个描述了精灵在整张纹理上的偏移量位置信息的数据文件,显示的文件大小较小。

因此这个不是冗余,是正常现象。

感谢马三小伙儿@UWA问答社区提供了回答

A2:不过确实存在一个冗余的问题:如果Prefab1和Prefab2引用了同一个Atlas的Sprite,那么这个Atlas至少要主动包含在一个AssetBundle中,否则会被动打入两个包中,造成冗余。

Atlas没有设置AssetBundle包:

5.png
6.png

Atlas打到其中一个AssetBundle包中:

7.png
8.png

感谢Prin@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/608949fa6bb31032f9791494


Q:关于Mesh占用内存的问题,在Unity 2020中能看见Mesh中包含哪些信息:

9.png
导入骨骼

这里如果不导入骨骼,顶点信息占用空间会减少一倍,请问导入骨骼后顶点信息增加了什么?

10.png
未导入骨骼

第二个问题是:在导入骨骼的情况下内存中这个Mesh占用了0.6MB刚好差不多是上面Inspector中显示的两倍,做过其它模型的测试也是两倍:

11.png

本来以为是开启Read/Write Enabled的问题,但是发现开不开启这个选项占用内存都不变。这个内存占用是怎么来的,开启Read/Write Enabled是否具体会造成两倍的内存开销?

经过测试发现,在不导入骨骼的情况下,开启Read/Write Enabled是不开启占用内存的两倍,不开启Read/Write Enabled占用内存和Inspector相同。在导入骨骼的情况下开不开启Read/Write Enabled都是Inspector界面显示内存的两倍。推测是导入了骨骼之后,默认会修改模型顶点,相当于默认开启了Read/Write Enabled。

A:第一个问题:
Inspector面板的Vertices一栏指的是Mesh的顶点属性(或通道),如果该Mesh包含某个通道的数据,就意味着每个顶点都有一份该顶点属性。

12.png

而Unity定义的顶点通道一共有14个:

13.png

如果导入了骨骼,多出来的顶点属性是顶点的骨骼权重和骨骼索引。

14.png

可通过Mesh.boneWeights()和Mesh.GetBonesPerVertex()访问。

然而,一个BoneWeight属性存了4个float32和4个int32,一共8x4=32Bytes。一个Bones index是一个Byte。

(8x4+1)Byte/Vert x 5512Vert = 177.63KB

15.png

第二个问题:导入骨骼动画,要在CPU端做蒙皮计算,就是要在CPU获取顶点属性。

感谢Prin@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/6088d9dc6bb31032f979147e


Q:我的测评报告中UGUI.Rendering.UpdateBatches占用较高,想问问是什么原因导致的呢?

16.png

A1:场景中Transform发生改变的UI元素太多了。看起来,场景中发生改变的Canvas有3个,而这3个Canvas下Transform发生改变的元素有312个。

感谢Prin@UWA问答社区提供了回答

A2:当Canvas中的UI元素触发CanvasRenderer.SyncTransform次数较多(几百次的量级)的时候,父节点UGUI.Rendering.UpdateBatches的耗时也会比较高。

测试后发现,在Unity 2018、2019和2020的版本中,调用SetActive(true)将UI元素从Deactive的状态变成Active状态,会导致UI元素所在的Canvas中的所有UI元素都触发CanvasRenderer.SyncTransform。在Unity 2017的版本中这样的操作只会影响这个SetActive(true)的元素本身,不知道是Unity的Bug还是本身就是这么设计的。不过在Unity 2018、2019和2020的版本中,可以使用设置Scale为0或者1的方法来隐藏显示UI,这样就只有Scanle变化的那个UI元素本身触发CanvasRenderer.SyncTransform了。

感谢Xuan@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/6092b91d6bb31032f97914e8


Script

Q:如下图所示,工程一打开就会报Timeline的异常,后面发现是跟Plugins下某个DLL有关,删除该DLL Timeline就能正常。那么为什么DLL会影响Timeline。Timeline这几个重载函数肯定都在UnityEditor.CoreModule里面,怎么会找不到?

17.png

A:Unity的脚本有个严格的编译顺序:
precompiled DLL -> asmdefs -> StandardAsset -> Plugins -> Plugins/Editor -> Assets -> Editor。

你的预编译DLL里面很有可能写了一个同名的PlayableBehaviour类,然后里面实现了同样的方法。

这个DLL优先于Packages下的Timeline被Unity加载编译,然后等到Timeline编译的时候,会发现有两个PlayableBehaviour,因此它就会找不到合适的方法进行重写。

按照上面这个思路,我在本地也复现了你的这种错误。

18.png

把这个DLL(ConsoleApp1.dll)扔到Plugins目录下就能看到同样的报错(DLL可戳原问答获取)。

感谢马三小伙儿@UWA问答社区提供了回答,欢迎大家转至社区交流:
https://answer.uwa4d.com/question/60962dfb6bb31032f979156a

封面图来源于网络

今天的分享就到这里。当然,生有涯而知无涯。在漫漫的开发周期中,您看到的这些问题也许都只是冰山一角,我们早已在UWA问答网站上准备了更多的技术话题等你一起来探索和分享。欢迎热爱进步的你加入,也许你的方法恰能解别人的燃眉之急;而他山之“石”,也能攻你之“玉”。

官网:www.uwa4d.com
官方技术博客:blog.uwa4d.com
官方问答社区:answer.uwa4d.com
UWA学堂:edu.uwa4d.com
官方技术QQ群:793972859(原群已满员)


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK