【Unity】画像のダウンロードを集約してメッセージを発行するクラス

# 背景＆概要

UnityのUGUIでTwitterのようなものを作ろうとした際、以下の問題に直面しました。

1. 同じ画像を複数箇所で同時に利用する際、ダウンロードが重複して勿体ない
2. ツイートのレイアウトをオブジェクトプールしている関係でたまに、前の画像反映が遅れて実行されてしまい、ユーザー名とアイコンが食い違う

そこで私は、UnityWebRequest と UniRx を使い、画像のダウンロードを集約してメッセージを発行できるクラス「<b>WebTextureRequestSender</b>」を作りました。
それに合わせて、IObservable\<Texture>を受け取ったらRawImageを変更するコンポーネント「<b>RawImageChangeReserver</b>」を作りました。
[AutoDisposedDictionary.cs](https://qiita.com/Yamara/items/47a62a8cce3dbc55ff54
) も合わせて利用すれば、ダウンロードとキャッシュを抑えた実装が可能になります。


# コード

https://github.com/yamara-mh/CodeStorage/blob/main/Codes/RawImageChangeReserver.cs

https://github.com/yamara-mh/CodeStorage/blob/main/Codes/RawImageChangeReserver.cs

# WebTextureRequestSender の使い方

コンストラクタでは、URLの指定に使う文字列と、エラーの際代わりに渡す画像を設定できます。

``` C#
WebTextureRequestSender sender
    = new WebTextureRequestSender("https://placehold.jp/64x64.png?text={0}", errorTexture);
```

ダウンロードの開始は下記のように行います。
同じkeyで間髪入れずに2回実行しても、ダウンロードは一回になります。

``` C#
sender.AddRequest("key");
```

:::note info
```key``` の使われ方
[コンストラクタで設定した文字列]の```{0}```にkeyが入った状態のURLから画像がダウンロードされます。上二つのコードが連続して実行された場合「"https\://placehold.jp/64x64.png?text=key"」から画像がダウンロードされます。
:::

ダウンロードの開始と同時に、完了時の処理も```Action<Data>```で設定できます。単純な用途ならこちらで十分です。同じkeyで間髪入れずに2回実行した場合、```Action<Data>```は2回実行されます。

``` C#
sender.AddRequest("text", data => rawImage.texture = data.Tex);
```
:::note info
```data(WebTextureRequestSender.Data)``` の内容
ダウンロードしたテクスチャとダウンロードの可否、エラー文が入っています。
:::

```sender.AddRequest()```の戻り値は```IObservable<Data>```になっています。完了時の処理を止める際に活用できます。

``` C#
IDisposable disposable = sender.AddRequest("text").Subscribe(data => rawImage.texture = data.Tex);

// オブジェクトプールにリリースするとき Dispose() すれば再利用されても安心です。
disposable.Dispose();
```

### key の複数設定
keyは何個でも設定できます。

``` C#
sender.AddRequest("arg0", "arg1", "arg2", "arg3", data => rawImage.texture = data.Tex);
```
5個目からは```string[]```で指定する必要があります。
``` C#
sender.AddRequest(new string[]{ "arg0", "arg1", "arg2", "arg3", "arg4"} , data => rawImage.texture = data.Tex);
```

### key を使った IObservable\<Data>の取得

```IObservable<Data>```の取得は```sender.GetObservable(key)```からでも行えますが、使えるのは1つ目のkeyだけです。

``` C#
sender.AddRequest("text");
IDisposable disposable = sender.GetObservable(key).Subscribe(data => rawImage.texture = data.Tex);
```

1つ目のkeyだけでは重複が発生してしまう場合、下記の方法を検討してください。

- URLをそのままkeyとして使う
- 1つ目のkeyをフォーマットに利用せず、識別用のものとして扱う

### 通信遅延の再現

コード中の以下のコメントアウトを切り替えると、通信遅延を再現した状態になります。また、コンソールにその旨が表示されます。

``` C#
// Code to reproduce pseudo-lag.
// completedObservable = GetCompletedObserverInPseudoLag(operation);
completedObservable = GetCompletedObserver(operation);
```

# RawImageChangeReserver コンポーネントについて

RawImageChangeReserver は、WebTextureRequestSender を活用するために作った、RawImageの変更予約コンポーネントです。
「画像のダウンロードが完了したらRawImageに反映する」処理は下記のように書けます。

``` C#
reserver.Reserve(sender.AddRequest(key).Select(data => data.Tex));
```

RawImageChangeReserver の関数を経由して画像の変更や変更予約をすれば、前の変更予約が自動でキャンセルされるので、間違った画像が反映されてしまう心配がありません。

他に、ダウンロード中に表示する画像やダウンロード失敗時に表示する画像を設定できます。

```_cancelOnDisable``` は、GameObjectが非アクティブになったとき、画像の変更予約をキャンセルできるフラグです。オブジェクトプールする際にひと手間省けます。

# サンプル

![gif.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/307985/280dc135-c751-f790-6196-1eccfff2a97d.gif)

数字が書かれた画像を0から順番にダウンロードし、RawImageに順番に表示するサンプルです。
RawImageの反映が2周回ったら、また0からダウンロードし直します。
本当はリストビューの要素をオブジェクトプールした実装を載せたかったのですが、再現が大変なので妥協しました。

``` Example.cs
using UnityEngine;
using UniRx;
using Yamara;
using UnityEngine.Networking;

public class Example : MonoBehaviour
{
    [SerializeField] RawImageChangeReserver[] _reservableImage;

    private WebTextureRequestSender _sender;
    private AutoDisposedDictionary<string, Texture> _dict;

    private int _count;

    void Start()
    {
        // URLのパラメータから画像を生成してくれるサービスを利用
        _sender = new WebTextureRequestSender("https://placehold.jp/25/000000/ffffff/64x64.png?text={0}");
        // 画像を6枚キャッシュ。超過したら2枚破棄
        _dict = new AutoDisposedDictionary<string, Texture>(6, 2, tex => Destroy(tex));
    }
    
    void Update()
    {
        if (!Input.anyKeyDown) return;

        var key = _count.ToString();
        var index = _count % _reservableImage.Length;

        // キャッシュに画像があれば使う
        if (_dict.TryGetValue(key, out Texture storeTex))
        {
            _reservableImage[index].SetTexture(storeTex);
        }
        else
        {
            // 画像のダウンロードを開始
            var observable = _sender.AddRequest(key);

            observable.Subscribe(data =>
            {
                if (data.Result != UnityWebRequest.Result.Success) return;

                // ダウンロードに成功したので、キャッシュの追加を試みる
                _dict.TryAdd(key, data.Tex);
            });

            // ダウンロード完了のイベントをRawImageChangeReserverに購読させる
            _reservableImage[index].Reserve(observable.Select(data => data.Tex));
        }

        _count = (_count + 1) % (_reservableImage.Length * 2);
    }
}
```