【Unity】Addressables1.8.3で.bundleのダウンロード進捗状況を取得する

# 概要

Addressables を用いて3Dアセット等をダウンロードする際に、少し時間が掛かるのでダウンロード進捗を表示したかった。

公式で `AsyncOperationHandle#PercentComplete` から取得できそうだったが、これは現バージョンだとダウンロード処理の段階では0%になってしまっていたので、 `AssetBundleProvider` 内部で利用している `UnityWebRequest#downloadProgress` から取得することにした。

## 参考

- [Addressables.DownloadDependencies: PercentComplete always returns 0 - Unity Forum](https://forum.unity.com/threads/0-7-4-addressables-downloaddependencies-percentcomplete-always-returns-0.667558/?_ga=2.205778640.2145562282.1588747407-1653854188.1542885366)
- [Networking.UnityWebRequest-downloadProgress - Unity スクリプトリファレンス](https://docs.unity3d.com/ja/current/ScriptReference/Networking.UnityWebRequest-downloadProgress.html)


# 環境

- Unity 2018.4.22f1
- Addressables 1.8.3

# 注意

`AssetBundleProvider` で利用している `UnityWebRequest` を取得するメソッドは公式には無い。
内部で `WebRequestQueue` に `UnityWebRequest` を渡しているのでここから取得できそうだが、
残念ながら `WebRequestQueue` は internal なので開発者側からは取得が出来ない。

また、Addressables のコードは直接編集することも出来なかった。
(そもそもライブラリコード改変はアップデート時に辛くなるのでやりたくない)

そこで、[リフレクション (C#) | Microsoft Docs](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/concepts/reflection) を利用して実行時に無理やり取得した。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/44388/31a90c97-8f59-56aa-5e77-b2311e0fb453.png)


WebRequestQueue.cs は Addressables で利用する通信処理のキュー。
アクティブな通信は `s_ActiveRequests` に追加されるので、こちらから取得する。

```cs
internal static class WebRequestQueue
{
    private static int s_MaxRequest = 500;
    internal static Queue<WebRequestQueueOperation> s_QueuedOperations = new Queue<WebRequestQueueOperation>();

    // TODO: ここからUnityWebRequestを取得する
    internal static List<UnityWebRequestAsyncOperation> s_ActiveRequests = new List<UnityWebRequestAsyncOperation>();

    public static WebRequestQueueOperation QueueRequest(UnityWebRequest request)
    {
        WebRequestQueueOperation queueOperation = new WebRequestQueueOperation(request);
        if (s_ActiveRequests.Count < s_MaxRequest)
        {
            var webRequestAsyncOp = request.SendWebRequest();
            webRequestAsyncOp.completed += OnWebAsyncOpComplete;
            s_ActiveRequests.Add(webRequestAsyncOp);
            queueOperation.Complete(webRequestAsyncOp);
        }
        else
            s_QueuedOperations.Enqueue(queueOperation);
        return queueOperation;
    }
    ...
}
```


実際に使ってみればわかるがリフレクションはかなり動作が遅く、Profiler を直接確認したわけでは無いがゲーム本体のパフォーマンスに影響を与える可能性も考えられる。また、こちらも未検証ではあるが ios 環境で動作するかどうかも不明なので、本来であれば公式でダウンロード進捗が取得出来るようになるのが理想である。(見落としているだけでもしかしたら可能かもしれない)

# ダウンロード進捗取得コード

改善の余地はあるが、下記のコードで正常にダウンロード進捗が取得出来ることが確認できた。

```cs
private async void Start() 
{
    var asset = Addressables.LoadAssetAsync<GameObject>("address");
    StartCoroutine(ShowProgress());
    var prefab = await asset.Task;
}

// AddressablesのPercentCompleteは正確じゃないので、内部のUnityWebRequestで進捗を表示
private IEnumerator ShowProgress()
{
    // 通信処理がアクティブになる前に実行される可能性があるので、アクティブになるまで取得し続ける
    UnityWebRequest request;
    while ((request = FetchUnityWebRequestForProvider()) == null)
    {
        yield return new WaitForSeconds(.1f);
    }
    
    // TODO: Provider内部でDisposeされるため、通信終了後に必ずArgumentExceptionが発生する
    while (!request.isDone)
    {
        Debug.Log("Now Loading... " + (request.downloadProgress * 100).ToString("g2") + "%");
        yield return new WaitForSeconds(.1f);
    }
}

// リフレクションでWebRequestQueueからUnityWebRequestを取得
private UnityWebRequest FetchUnityWebRequestForProvider()
{
    var libAssembly = Assembly.GetAssembly(typeof(AssetBundleProvider));
    var type = libAssembly.GetType("UnityEngine.ResourceManagement.WebRequestQueue");
    var field = type.GetField("s_ActiveRequests", BindingFlags.Static | BindingFlags.NonPublic);
    var requests = (List<UnityWebRequestAsyncOperation>)field?.GetValue(type);
    if (requests == null) return null;
    
    // 初回はカタログファイルのダウンロードが入るので、bundleファイルの通信のみ取得するようにする
    foreach (var request in requests)
    {
        if (request.webRequest.url.EndsWith(".bundle")) return request.webRequest;
    }
    return null;
}
```

参考: [.net - How do I get class of an internal static class in another assembly? - Stack Overflow](https://stackoverflow.com/questions/2665648/how-do-i-get-class-of-an-internal-static-class-in-another-assembly)

## 検証用のキャッシュクリアコード

Editor拡張で作成。
Tools -> Addressables Cache Clear からキャッシュ削除が可能。

```cs
public static class AddressableCacheClear
{
    [MenuItem("Tools/Addressables Cache Clear")]
    private static void Delete()
    {
        Debug.Log(Caching.ClearCache() ? "Successfully cleaned the cache。" : "キャッシュが使用されています。");
    }
}
```

# まとめ

カスタムプロバイダを作れば解決出来るかもしれないと思いましたが、`AssetBundleResource` が `internal` で面倒さを感じたのでリフレクションで実装しました。

不完全なコードではありますが、Addressables の Load 時のプログレスバーを実装しているエンジニアさんの力になれましたら幸いです。Addressabels 初心者なので間違ってたらマサカリ下さい。