Unity Addressableの重複アドレスや依存グループチェック機能を作る

#はじめに
Addresabblesを使うには便利機能が足りていない部分があるので自作していきます。
[Unity Addressable Asset Systemの使用方法と自動化の機能調査](https://qiita.com/Zi_su/items/4d3851f806956e87b48f)の続きとなる内容です。

使用バージョン
unity 2019.3.0b4
Addresabbles version1.5.0

##スクリプトを作る
###重複アドレスチェック
アセットにユニークなアドレスを設定しロードを簡単に書けるということですが、重複したアドレスを設定することができます。
この状態でもビルドは成功します。

アドレスが重複しているときは最後に追加されたアセットをロードしてきます。
このときも特に警告やエラーログは出ません。

アドレスの命名規則を決めておき重複しないように運用できていればいいですが、重複アドレスが無いかのチェックを行いましょう。

```cs
    /// <summary>
    /// 全アドレスの重複チェック
    /// </summary>
    [MenuItem(menuName + "/CheckAllAddress")]
    static public void CheckAllAddress()
    {
        var s = GetSettings();
        List<AddressableAssetEntry> entries = new List<AddressableAssetEntry>();
        s.GetAllAssets(entries, true);
        List<string> checkedAddress = new List<string>();
        foreach (var e in entries)
        {
            //チェック済みアドレスはコンテニュー
            if (checkedAddress.Contains(e.address)) continue;

            //全アセットで重複があるかチェック
            bool ret = CheckAddress(entries, e.address);
            if (!ret)
            {
                checkedAddress.Add(e.address);
            }
        }
    }

    /// <summary>
    /// 重複アドレスチェック
    /// </summary>
    /// <param name="entries"></param>
    /// <param name="address"></param>
    /// <returns>重複なしtrue、ありfalse</returns>
    static public bool CheckAddress(List<AddressableAssetEntry> entries, string address)
    {
        var s = GetSettings();
        var duplicateEntries = entries.FindAll(e=>e.address == address);
        if(duplicateEntries.Count > 1)
        {
            string str = "Address=" + address + System.Environment.NewLine;
            foreach (var e in duplicateEntries)
            {
                string assetname = System.IO.Path.GetFileName(e.AssetPath);
                str += "Group=" + e.parentGroup.Name + "," + "AssetName=" + assetname + System.Environment.NewLine;
            }
            Debug.LogAssertion("DuplicateAddress" + System.Environment.NewLine + str);
            return false;
        }
        return true;
    }
```

###並び替え
AddresabblesGroupsのGUI上ではGroupを並び替えることが手動でできます。
Group内をPathで並び替えはできましたがなぜかGroup自体の並び替えはできませんでした。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157391/9c59e2c5-9c05-209e-e461-70460161f1ef.png)

内部的な並びは
AddressableAssetSettings.groupsのソートすれば変わるようですがGUI上は画面が更新されない限り
変わりませんでした。
GroupNameの欄をクリックするか、インポート時やコンパイル時に反映されました。

```cs
/// <summary>
    /// 文字列順にソートする
    /// </summary>
    [MenuItem(menuName + "/Sort")]
    static public void Sort()
    {
        var s = GetSettings();
        s.groups.Sort(new GroupCompare());
    }
```
文字列の大きさ比較してソートをするだけです。

このままでは不便すぎるので、Addresabblesのソースコードを書き換えてGUI上も更新させます。
スクリプトリファレンスのGUIのページにはAPIについて載っていないので勘で探します。
https://docs.unity3d.com/Packages/com.unity.addressables@1.5/api/UnityEditor.AddressableAssets.GUI.html

Addresabbles GroupsのGUIは
AddressableAssetEntryTreeView.cs
で描画されています。
UnityEditorの[TreeView](https://docs.unity3d.com/ScriptReference/IMGUI.Controls.TreeView.Reload.html)を継承しています。
Reload関数を呼ぶことで強制的にデータを再読み込みさせられるとあるので呼べるようにしていきます。

AddressableAssetsSettingsGroupEditor.cs に
AddressableAssetEntryTreeView m_EntryTree;　というメンバでインスタンスを持っており

AddresabblesAssetsWindow.cs　に
AddressableAssetsSettingsGroupEditor m_GroupEditor;　というメンバでインスタンスを持っています。
このAddressablesAssetWindowを取得して、TreeViewのReloadを呼び出します。

AddressableAssetWindowにReload関数を追加します。
1. AddressableAssetWindowクラスをpublicにする。
2. m_GroupEditor.Reload()を呼び出すpublic Reload関数を作る
3. エディタウィンドウを取得してReload関数を呼び出す。

```cs
/// <summary>
    /// 文字列順にソートする
    /// </summary>
    [MenuItem(menuName + "/Sort")]
    static public void Sort()
    {
        var s = GetSettings();
        s.groups.Sort(new GroupCompare());
        EditorWindow.GetWindow<UnityEditor.AddressableAssets.GUI.AddressableAssetsWindow>().Reload();
    }
```

これでソートを実行するとGUI上もソートできました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157391/bcd3f1b3-a899-6052-588e-9c945540db90.png)　　![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157391/b6fa6feb-ab5d-7e18-dd27-c58f5df8c7a0.png)

###空グループ削除
アセットの管理が変わってグループが必要なくなった時に、空グループは自動で削除されてほしい場合があります。
グループのアセットエントリ数をカウントして0ならば削除するだけです。
デフォルトグループの判定もあるので0かつデフォルトグループではない時に削除します。

```cs
    /// <summary>
    /// 空グループを削除
    /// </summary>
    [MenuItem(menuName+"/Remove/EmptyGroup")]
    static public void DeleteEmptyGroup()
    {
        var s = GetSettings();
        var groups = s.groups;
        foreach (var g in groups)
        {
            if (g.entries.Count == 0 && !g.IsDefaultGroup())
            {
                s.RemoveGroup(g);
            }   
        }
    }
```

###依存関係チェック
アセットがどのグループに依存しているかをチェックする仕組みを作り、依存関係による巨大なバンドルが作られないようにします。
依存自体はAssetDatabase.GetDependenciesで取得できるので、このAPIとグループ内のエントリを調べていきます。

注意としてアセット数が千や万を超える膨大な数になると全アセット検索をしては数分かかるので、適宜フィルタリングをする必要が出てきます。

```cs
    [MenuItem("test/FindDependency")]
    static public void DependCheck()
    {
        var s = GetSettings();
        List<AddressableAssetEntry> entries = new List<AddressableAssetEntry>();
        s.GetAllAssets(entries, true);
        foreach (var e in entries)
        {
            string str = ("Group:" + e.parentGroup+  " Asset:" + e.AssetPath + System.Environment.NewLine);
            var dependencies = AssetDatabase.GetDependencies(e.AssetPath);
            foreach (var d in dependencies)
            {
                var guid = AssetDatabase.AssetPathToGUID(d);
                foreach (var g in s.groups)
                {
                    if (e.parentGroup == g) continue;

                    var entry = g.GetAssetEntry(guid);
                    if(entry != null && entry != e)
                    {
                        str += g.Name + System.Environment.NewLine;
                        str += d + System.Environment.NewLine;
                    }
                }
                
            }
            Debug.Log(str);
        }
    }
```

グループの設定はこの状態で、prefabB.prefabのスクリプトがprefabC.prefabを参照しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157391/fbafe02a-97dd-3fcb-cd30-50d71bdaa1f8.png)
検索結果のログはこうなります。GroupCに依存しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/157391/491f7138-f471-7c2c-418f-1fda74d7441d.png)



#おわりに
Addresabblesはver1.5.0になりバグも減っていると思います。
APIも十分あり処理をフックして自前の処理に置き換えることもできるので、それぞれのプロジェクトに応じて拡張をしていくことも可能です。
足りない機能はそれぞれ自作していきましょう。