背景&概要
UnityのUGUIでTwitterのようなものを作ろうとした際、以下の問題に直面しました。
- 同じ画像を複数箇所で同時に利用する際、ダウンロードが重複して勿体ない
- ツイートのレイアウトをオブジェクトプールしている関係でたまに、前の画像反映が遅れて実行されてしまい、ユーザー名とアイコンが食い違う
そこで私は、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が非アクティブになったとき、画像の変更予約をキャンセルできるフラグです。オブジェクトプールする際にひと手間省けます。
サンプル
数字が書かれた画像を0から順番にダウンロードし、RawImageに順番に表示するサンプルです。
RawImageの反映が2周回ったら、また0からダウンロードし直します。
本当はリストビューの要素をオブジェクトプールした実装を載せたかったのですが、再現が大変なので妥協しました。
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);
}
}