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

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

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

本期目录:ide

  • 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中:网站

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


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


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最佳食用方案

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

所以这个不是冗余,是正常现象。

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

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

Atlas没有设置AssetBundle包:

Atlas打到其中一个AssetBundle包中:

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


Mesh

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

这里若是不导入骨骼,顶点信息占用空间会减小一倍,请问导入骨骼后顶点信息增长了什么?

未导入骨骼

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

原本觉得是开启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包含某个通道的数据,就意味着每一个顶点都有一份该顶点属性。

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

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

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

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

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

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

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


UGUI

Q:个人测评报告中UGUI.Rendering.UpdateBatches占用较高,想问问是什么缘由致使的呢?

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

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

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

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

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


Script

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

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

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

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

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

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

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

封面图来源于网络

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

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

相关文章
相关标签/搜索