LoginSignup
1
1

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

Last updated at Posted at 2022-08-19

背景&概要

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

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

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

コード

WebTextureRequestSender の使い方

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

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

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

sender.AddRequest("key");

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

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

sender.AddRequest("text", data => rawImage.texture = data.Tex);

data(WebTextureRequestSender.Data) の内容
ダウンロードしたテクスチャとダウンロードの可否、エラー文が入っています。

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

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

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

key の複数設定

keyは何個でも設定できます。

sender.AddRequest("arg0", "arg1", "arg2", "arg3", data => rawImage.texture = data.Tex);

5個目からはstring[]で指定する必要があります。

sender.AddRequest(new string[]{ "arg0", "arg1", "arg2", "arg3", "arg4"} , data => rawImage.texture = data.Tex);

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

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

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

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

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

通信遅延の再現

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

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

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

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

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

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

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

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

サンプル

gif.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);
    }
}
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1