冗長性を回避できるために、Unityの内蔵アセットはどのようにパッケージ化すればいいのか

**今回の話題：**
冗長性を回避できるために、Unityの内蔵アセットはどのようにパッケージ化すればいいのか
SpriteAtlasの「冗長性」問題
Meshの使用メモリについて
UGUI.Rendering.UpdateBatches時間コストが高い
PluginsのDLLはどのようにPackageに影響するか

## AssetBundle
 
Q：今はAssetBundleをパッケージ化しようと思いますが、Unityの内蔵アセットはAssetBundle名を指定できますか？ AssetBundleにある内蔵アセットの冗長性は一般的にどのように解決しますか？
 
既存のデータを確認した後、内蔵アセットのパッケージ化により、AssetBundleの冗長処理では、内蔵アセットをローカルに抽出またはダウンロードし、アセットの参照関係を変更してから、パッケージがAssetBundle名を指定できるようになりました。Unityは、スクリプトがAssetBundleをパッケージ化する際に内蔵アセットのBundle名を指定することをサポートしますか。これにより、複数のアセットが同じ内蔵アセットに依存することを防ぎ、冗長性をもたらしますか？
>A：Scriptable Build Pipelineを使用して実現できます。具体的な方法については、Addressableで内蔵のShaderをパッケージ化する方法を参照してください。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/5864d176-4aab-c3b2-f6f8-09ef7aad2fe8.png)
```
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にパッケージ化します（Package名はpsです）。特殊効果に使用される内蔵アセットがすべてこのAssetBundleに含まれていることがわかります。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/ec51786e-b1eb-a928-012a-f785739d9d31.png)
SBPでパッケージ化した後は、次のようになります。
psのAssetBundleにアセットがないことがわかります。生成された内蔵AssetBundleにはpsによって使用される3つの内蔵アセットがあり、psはUnityBuiltInResourcesのAssetBundleに依存します。
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/6ce8e26c-f5b0-748d-1a59-82257a79caf2.png)
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/6d789ccf-e8f6-dd6a-813c-fc374767fe27.png)

## AssetBundle
Q：SpriteAtlasについて、UIがレンダリング時に関連するSprite情報に従って対応するSpriteAtlaを見つけるということだと思いました。各UIレンダリングが同じAtlasを使用するようになり、Draw Callを減らすことができます。したがって、理論的には、Atlasの一枚で十分です。しかし、なぜAtlasのパッケージ化したAssetBundleには、Atlasに加えて、参照されている元の画像もあるのか。UIのAssetBundleパッケージにも、参照されている元の画像があります。
 
なぜそのような「冗長性」があるのか​​、そしてImageがアトラスを参照するのか画像を参照するのか？またはその原理は何ですか？
 
元のイメージがAtlasパッケージに配置されている場合、元のイメージはAtlasPackageに含まれていますが、UIPackageには含まれていません。元のイメージがAssetBundlePackageの外部に配置されている場合、AtlasPackageとUIPackageの両方に元のイメージがあります。それは何の原理でしょうか？
 
>A1：最初にこの記事を読むことをお勧めします：
[【Unity游戏开发】SpriteAtlas与AssetBundle最佳食用方案（中国語注意）](https://www.cnblogs.com/msxh/p/14194756.html)
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/5e6fbee0-5895-2804-8ea9-acd502ec9e66.png)
SpriteAtlasを適切に使用する場合、AssetBundleを解凍すると、Textureと複数のSpriteの2つのアセットが含まれていることがわかります。Texture
はテクスチャであり、表示されるファイルサイズは大きいです。Spriteはテクスチャ全体でのスプライトのオフセット位置情報を記述したデータファイルとして理解でき、表示されるファイルサイズは小さいです。
したがって、これは冗長ではなく、正常です。
 
>A2：ただし、確かに冗長性の問題があります。Prefab1とPrefab2が同じAtlasの Spriteを参照している場合、Atlasは少なくとも1つのAssetBundleにアクティブに含まれている必要があります。そうでない場合、2つのPackageに受動的に入力され、冗長性が発生します。
AtlasはAssetBundlePackageを設定しません。
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/e6a8cde1-660a-f798-ad17-5e929bd8d374.png)
![6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/f22fe3c1-44fd-7657-b84a-d1ca54960760.png)
Atlasは、AssetBundlePackageの1つにパッケージ化します。
![7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/ef8f5e9f-b139-09dd-ca99-e0eb628f17c9.png)
![8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/cef0dac3-a86a-3750-8ec7-916555e19e55.png)

## Mesh

Q：Meshのメモリ使用量に関して、Unity 2020のMeshにはどのような情報が表示されますか？
![9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/f52afaa9-6c06-533d-edb9-fd0badc63334.png)
ボーンをインポートする
ここでボーンをインポートしないと、頂点情報が占めるスペースが2倍に減りますが、ボーンをインポートした後の頂点情報には何を増加しますか？
![10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/90c2bbf1-5df6-9c2e-d3b8-26a40f978f1a.png)
ボーンをインポートしない
2番目の問題は次のとおりです。ボーンをインポートする場合、メモリ内のMeshは0.6MBを占有します。これは、上記のInspectorに表示されるもののほぼ2倍であり、他のモデルのテストも2倍です。
![11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/834d700a-3b0d-ec86-81ff-1d80d289d608.png)
Read/Write Enabledを有効にすることが問題だと考えていましたが、このオプションがどうであれ、メモリ使用量は変わらないことがわかりました。このメモリ使用量はどのように発生しますか。また、Read/Write Enabledを有効にすると、2倍のメモリコストが発生しますか？
 
テストの結果、ボーンをインポートせずに、Read/Write Enabledを有効にする場合は、有効にしない場合の2倍のメモリが必要となります。Read/Write Enabledを有効にしないと、Inspectorと同じメモリを占有することがわかりました。
ボーンをインポート時にRead/Write Enabledが有効になっているかどうかは、Inspectorインターフェイスの表示メモリの2倍です。ボーンがインポートされた後、モデルの頂点がデフォルトで変更されると推測されます。これは、デフォルトでRead/Write Enabledを有効にするのと同じです。
 
>A：最初の質問：
InspectorパネルのVertices列は、Meshの頂点属性（またはチャネル）を指します。Meshにチャネルのデータが含まれている場合、各頂点に頂点属性があることを意味します。
![12.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/b73d9aea-b174-3072-6651-e3db717c0abc.png)
Unityによって定義された頂点チャネルは14があります。
![13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/8f888876-b12c-acaa-37e6-0e40b87b58c2.png)
ボーンがインポートされる場合、追加の頂点属性は頂点のボーンウェイトとボーンインデックスです。
![14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/7459daf8-74b0-620a-d8fd-727b9e3457bc.png)
Mesh.boneWeights()およびMesh.GetBonesPerVertex()を介してアクセスできます。
ただし、BoneWeightプロパティは、4つのfloat32と4つのint32、合計8×4=32Bytesを格納します。一つのBones indexは一つのByteに相当します。
(8×4+1)Byte/Vert x 5512Vert = 177.63KB
![15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/f5b55543-c17d-3662-8e77-9a095ea17aa0.png)
2番目の質問：ボーンアニメーションをインポートして、CPU側でスキニング計算を実行します。つまり、CPUで頂点属性を取得します。


## UGUI
 
Q：テストレポートでは、UGUI.Rendering.UpdateBatchesの使用量が多いと書かれています。それは何の原因でしょうか。
![16.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/60c59cbb-66fe-0922-d903-3d535fb8cf14.png)


 
>A1：シーンにあるTransformが変更されたUI要素が多すぎます。シーン内で変更されたCanvasが3つあり、これらの3つのCanvasの下Transformが変更された要素が312あるようです。
 
>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をトリガーします。
 
## Script

Q：下図に示すように、プロジェクトを開けるとすぐにTimeline異常が報告されます。後で、Pluginsの下のDLLに関連していることがわかります。DLLTimelineを削除すると、正常になります。では、DLLがTimeline
に影響を与えるのはなぜですか。Timelineのこれらのオーバーロードされた関数はUnityEditor.CoreModuleにあるはずだが、どうやって見つけられないでしょうか？
![17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/01795721-6488-77dd-fe2a-b3d21f8bdea0.png)


 
A：Unityスクリプトには厳密なコンパイル順序があります：
precompiled DLL -> asmdefs -> StandardAsset -> Plugins -> Plugins/Editor -> Assets -> Editor。
プリコンパイルされたDLLには、同じ名前が記述されたPlayableBehaviourクラスが含まれている可能性が高く、同じメソッドが実装されています。
![18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/653855/e5dccf60-5d34-e03f-6a09-07a6c5f0fd17.png)


このDLLは、Unityによって[Package]の[Timeline]の前にロードおよびコンパイルされます。Timelineをコンパイルすると、2つのPlayableBehaviourが検出されるため、書き換えに適した方法が見つかりません。
上記の考えに従って、エラーをローカルで再現しました。


このDLL（ConsoleApp1.dll）をPluginsディレクトリにスローすると、同じエラーが表示されます（DLLは元の質問と回答から取得できます）。

---
UWA Technologyは、モバイル/VRなど様々なゲーム開発者向け、**パフォーマンス分析**と**最適化ソリューション**及び**コンサルティングサービス**を提供している会社でございます。

今なら、UWA GOTローカルツールが**15日間に無償試用**できます！！
よければ、ぜひ！

UWA公式サイト：https://jp.uwa4d.com
UWA GOT OnlineレポートDemo：https://jp.uwa4d.com/u/got/demo.html
UWA公式ブログ：https://blog.jp.uwa4d.com