UnityWebRequestのダウンロード進捗率の表示をMV(R)Pで実装する

ネット上にある大容量コンテンツをダウンロードする処理は時間がかかります.

ダウンロード処理中に進捗率が数字やバーで表示されていないと,
固まって見えてしまい,ユーザエクスペリエンス的によくないです.

今回,UnityWebRequestのダウンロード進捗率の表示をMV(R)Pで実装してみました.

MV(R)P

こちらのスライドで詳しく説明しております.

www.slideshare.net

今回やりたいことは以下の画像の通り.

f:id:xrdnk:20200526123754p:plain

Model側:UnityWebRequestのダウンロード処理を行う.ダウンロード進捗率を通知する.
Presenter側:ModelとViewの橋渡し役.購読処理を行う.
View側:ボタン押下の通知を行う.ダウンロード進捗率を表示する.

ProgressModel

using System;
using UnityEngine;
using UniRx;
using UniRx.Async;
using UnityEngine.Networking;

public class ProgressModel : MonoBehaviour
{
    private ReactiveProperty<float> _downloadProgress = new ReactiveProperty<float>();
    public IReadOnlyReactiveProperty<float> DownloadProgress => _downloadProgress;

    public async void DownloadContent()
    {
        // 取得したいコンテンツのURL
        var url = "https://public-cdn.cloud.unity3d.com/hub/prod/UnityHubSetup.dmg";
        // テキストデータの取得
        var textData = await DownloadTextAsync(url);
        // テキストデータの表示
        Debug.Log(textData);
    }

    /// <summary>
    /// コンテンツのダウンロード非同期処理
    /// </summary>
    /// <param name="url">URL</param>
    /// <returns>コンテンツのテキストデータ</returns>
    private async UniTask<string> DownloadTextAsync(string url)
    {
        using (UnityWebRequest uwr = UnityWebRequest.Get(url))
        {
            // 送受信開始
            await uwr.SendWebRequest()
                    .ConfigureAwait(Progress.Create<float>(x =>
                        {
                            // 値の変化を設定
                            _downloadProgress.Value = x * 100;
                            // コンソール確認用
                            Debug.Log($"{x * 100} %");
                        }));
            // エラーハンドリング
            if (uwr.isNetworkError || uwr.isHttpError) throw new Exception(uwr.error);
            // ダウンロードしたコンテンツのテキストデータを返す
            return uwr.downloadHandler.text;
        }
    }
}

進捗の値をReactivePropertyとして保持します.設定時に今回は100倍しています.
ダウンロード処理は非同期処理なので,UniTaskを使います.
非同期処理の進捗を取得する方法として,AsyncOperationのConfigureAwaitを利用しました.

qiita.com

UniTask 2 では ConfigureAwaitは廃止予定

ちなみに,UniTask 2ではConfigureAwaitは廃止予定で,書き方が変わります.

qiita.com

以下のような書き方になる?

    /// <summary>
    /// コンテンツのダウンロード処理
    /// </summary>
    /// <param name="url">URL</param>
    /// <returns>コンテンツのテキストデータ</returns>
    private async UniTask<string> DownloadTextAsync(string url, CancellationToken token)
    {
        using (UnityWebRequest uwr = UnityWebRequest.Get(url))
        {
            // 送受信開始
            await uwr.SendWebRequest()
                    .ToUniTask(Progress.Create<float>(x =>
                        {
                            // 値の変化を設定
                            _downloadProgress.Value = x * 100;
                            // コンソール確認用
                            Debug.Log($"{x * 100} %");
                        }), token);
            // エラーハンドリング
            if (uwr.isNetworkError || uwr.isHttpError) throw new Exception(uwr.error);
            // ダウンロードしたコンテンツのテキストデータを返す
            return uwr.downloadHandler.text;
        }
    }

new Progress[T] はダメ

進捗率を取得する方法として,こんな書き方もあります.

await uwr.SendWebRequest()
                    .ConfigureAwait(new Progress<float>(x =>
                        {
                            // 値の変化を設定
                            _downloadProgress.Value = x * 100;
                            // コンソール確認用
                            Debug.Log($"{x * 100} %");
                        }));

ただこの書き方は地雷のようです.
こちらの記事のnew Progress[T] is Evilに詳細が書かれております.

neue.cc

ProgressPresenter

using UnityEngine;
using UniRx;

[RequireComponent(typeof(ProgressModel))]
[RequireComponent(typeof(ProgressView))]
public class ProgressPresenter : MonoBehaviour
{
    // Extenjectを用いる方が良い
    [SerializeField] private ProgressModel _progressModel;
    [SerializeField] private ProgressView _progressView;

    void Start()
    {
        // Viewにあるダウンロードボタンの押下時、Modelに通知する
        _progressView.OnDownloadButtonPushed.Subscribe(_ => _progressModel.DownloadContent());

        // Modelにある進捗率の値が変化した時、Viewに通知する
        _progressModel.DownloadProgress.Subscribe(_progressView.DisplayProgress);
    }
}

コードに書かれているコメントの通りです.
今回は行っておりませんが,ExtenjectでInjectした方が良い実装だと思います.

ProgressView

using System;
using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class ProgressView : MonoBehaviour
{
    [SerializeField, Tooltip("ダウンロード進捗率表示用のUI")]
    private Text _text;
    [SerializeField, Tooltip("ダウンロードボタン")]
    private Button _button;

    private IObservable<Unit> _onDownloadButtonPushed => _button.OnClickAsObservable();
    public IObservable<Unit> OnDownloadButtonPushed => _onDownloadButtonPushed;

    /// <summary>
    /// ダウンロード進捗率を整数表示する
    /// </summary>
    /// <param name="progress"></param>
    public void DisplayProgress(float progress)
    {
        _text.text = progress.ToString("F0") + " %";
    }
}

整数表示にしたいため,ToString("F0")をしております.
FillAmountを使う場合は,Model側で100倍せずに,
View側(UI側)の設定で百分率表示の調整した方が責務的に合っていると思います.

実装結果

f:id:xrdnk:20200526121126g:plain

MV(R)Pでうまく責務配置を分離しつつ,実装ができました.うれぴー.
gifでは最後の処理まで表示されていませんが,テキストデータもしっかり出ています.

f:id:xrdnk:20200526124425p:plain

参考記事

qiita.com

qiita.com

qiita.com